商貿(mào)網(wǎng)站管理培訓(xùn)機構(gòu)
目錄
一、用戶登錄權(quán)限驗證
1.1 SpringAOP可以進行處理嗎?
1.2?創(chuàng)建自定義攔截器
?1.3?將自定義攔截器配置到系統(tǒng)配置項中
1.4 攔截器的實現(xiàn)原理
1.4.1 實現(xiàn)原理源碼分析
1.5 統(tǒng)一訪問前綴添加
二、統(tǒng)一異常處理
2.1 為什么需要使用統(tǒng)一異常處理?
2.2 統(tǒng)一異常處理的實現(xiàn)
三、統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么需要統(tǒng)一數(shù)據(jù)返回格式?
3.2 統(tǒng)一數(shù)據(jù)返回格式的實現(xiàn)
?3.3 返回值為String類型時,應(yīng)該如何處理?
3.3.1 將 StringHttpMessageConverter 去掉。
3.3.2 在統(tǒng)一數(shù)據(jù)返回的時候,單獨處理String類型,讓其返回一個String字符串,而非 HashMap
?總結(jié):
前言:
一般Spring Boot統(tǒng)一功能處理模塊,也是AOP的實戰(zhàn)環(huán)節(jié),要實現(xiàn)課程目標有以下3個:
- 統(tǒng)一用戶登錄權(quán)限驗證
- 統(tǒng)一數(shù)據(jù)格式
- 統(tǒng)一異常處理
一、用戶登錄權(quán)限驗證
1.1 SpringAOP可以進行處理嗎?
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {// 定義切點?法 controller 包下、?孫包下所有類的所有?法@Pointcut("execution(* com.example.demo.controller..*.*(..))")public void pointcut(){ }// 前置?法@Before("pointcut()")public void doBefore(){}// 環(huán)繞?法@Around("pointcut()")public Object doAround(ProceedingJoinPoint joinPoint){Object obj = null;System.out.println("Around ?法開始執(zhí)?");try {// 執(zhí)?攔截?法obj = joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}System.out.println("Around ?法結(jié)束執(zhí)?");return obj;}
}
我們知道SpringAOP雖然就提供了對用戶登錄的處理邏輯,但是存在一些問題:
- 沒有辦法獲取HttpSession對象
- 如果要對一部分方法攔截,一部分方法不攔截,這種情況很難處理。(比如注冊和登錄方法在用戶登錄權(quán)限驗證中是不能進行攔截的)
攔截器和SpringAOP雖然都是AOP的實現(xiàn)方式,但是這兩個其實是完全不同的技術(shù)體系。
Spring提供了具體的實現(xiàn)攔截器:HandlerInterceptor,該SpringBoot 攔截器實現(xiàn)分為以下兩個步驟:
- 自定義攔截器
- 將自定義攔截器配置到系統(tǒng)配置項,并且設(shè)置合理的攔截規(guī)則
1.2?創(chuàng)建自定義攔截器
自定義攔截器繼承HandlerInterceptor后,需要重寫相對應(yīng)的方法,這里我們重寫 preHandle方法:
?代碼如下:
package com.example.demo.common;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;/*** 自定義攔截器*/
public class LoginInterceptor implements HandlerInterceptor {/*** 以下方法是調(diào)用目標方法之前執(zhí)行的方法。此方法返回boolean類型的值* 返回true標識驗證成功,程序會繼續(xù)執(zhí)行后續(xù)流程* 返回false, 表示攔截器攔截失敗, 驗證未通過, 后續(xù)的流程和目標方法不再執(zhí)行。* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 用戶登錄判斷業(yè)務(wù)HttpSession session = request.getSession(false);if (session != null && session.getAttribute("session_userinfo") != null) {// 用戶已經(jīng)登錄return true;}return false;}
}
?1.3?將自定義攔截器配置到系統(tǒng)配置項中
?重寫addInterceptors方法:
?設(shè)置攔截規(guī)則,代碼如下
package com.example.demo.config;import com.example.demo.common.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;public class MyConfig implements WebMvcConfigurer {@AutowiredLoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**") //攔截所有的url.excludePathPatterns("user/login") // url為:user/login 不進行攔截 以下同理.excludePathPatterns("user/reg").excludePathPatterns("image/**"); // image夾目錄下的所有url都不進行攔截}
}
或者使用Spring方法,通過DI注入的方式,這樣可以實現(xiàn)不用new一個實例:
首先需要將攔截器添加到spring中,也就是給其添加一個五大類注解,這里我們就使用@Component。接著就可以使用@Autowired來得到實例。
?其中:
- addPathPatterns: 表示需要攔截的URL,“**”表示攔截任意方法(也就是所有方法)。
- excludePathPatterns: 表示需要排除的URL。
?說明:以上攔截規(guī)則可以攔截此項目中的URL,包括靜態(tài)文件(圖片文件,JS和CSS等文件)。
1.4 攔截器的實現(xiàn)原理
下面我們先來看一組正常情況下的調(diào)用順序:
然而有了攔截器之后,會在調(diào)用Controller之前進行相應(yīng)的業(yè)務(wù)處理,執(zhí)行的流程如下圖所示:
1.4.1 實現(xiàn)原理源碼分析
所有的Controller執(zhí)行都會通過一個調(diào)度器DispatcherServlet來實現(xiàn),這一點可以從Spring Boot控制臺的打印信息看出,如下圖所示:
在IDEA中,通過全局搜索doDispatch,方法代碼如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {try {ModelAndView mv = null;Object dispatchException = null;try {processedRequest = this.checkMultipart(request);multipartRequestParsed = processedRequest != request;mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// 實現(xiàn)Controller的業(yè)務(wù)邏輯mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}this.applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new NestedServletException("Handler dispatch failed", var21);}this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);} catch (Throwable var23) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));}} finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}} else if (multipartRequestParsed) {this.cleanupMultipart(processedRequest);}}}
觀察DispatcherServlet中的某段代碼:
我們發(fā)現(xiàn),在執(zhí)行后續(xù)的Controller代碼之前,都會先執(zhí)行這個applyPreHandle方法,于是鼠標雙擊 applyPreHandle,得到代碼如下:
從上述源碼可以看出,在applyPreHandle中會獲取所有的攔截器HandlerInterceptor并執(zhí)行攔截器中的preHandle方法,這樣就和之前定義的攔截器對應(yīng)上了,如下圖所示:
package com.example.demo.common;import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;/*** 自定義攔截器*/
@Component
public class LoginInterceptor implements HandlerInterceptor {/*** 以下方法是調(diào)用目標方法之前執(zhí)行的方法。此方法返回boolean類型的值* 返回true標識驗證成功,程序會繼續(xù)執(zhí)行后續(xù)流程* 返回false, 表示攔截器攔截失敗, 驗證未通過, 后續(xù)的流程和目標方法不再執(zhí)行。* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 用戶登錄判斷業(yè)務(wù)HttpSession session = request.getSession(false);if (session != null && session.getAttribute("session_userinfo") != null) {// 用戶已經(jīng)登錄return true;}response.setContentType("application/json");response.setCharacterEncoding("utf8");response.getWriter().println("asdasd");return false;}
}
1.5 統(tǒng)一訪問前綴添加
所有請求地址添加api前綴:
代碼如下:
package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class AppConfig implements WebMvcConfigurer {@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {configurer.addPathPrefix("fox", c -> true);}
}
二、統(tǒng)一異常處理
2.1 為什么需要使用統(tǒng)一異常處理?
通俗來講,統(tǒng)一異常處理的主要目的是為了方便前端,讓其更好的處理后端的信息,盡量將邏輯處理這塊放置于后端,前端的目的其實主要是為用戶服務(wù)。
比如:可以跟前端約定出現(xiàn)異常報錯時候的狀態(tài)碼是多少,這樣方便前端的處理,也方便后續(xù)后端在日志文件中將其找到,并修改異常。
2.2 統(tǒng)一異常處理的實現(xiàn)
統(tǒng)一異常處理使用的是@ControllerAdvice + @ExceptionHandler 來實現(xiàn)的,@ControllerAdvice表示控制器通知類, @ExceptionHandler是異常處理器,兩個結(jié)合表示當出現(xiàn)異常的時候執(zhí)行某個通知,也就是執(zhí)行某個方法事件,具體實現(xiàn)代碼如下:
以上方法表示,如果出現(xiàn)了異常就返回給前端一個HashMap對象, 其中包含的字段如代碼定義那樣。
?注意:
方法名和返回值都是可以自定義的,另外 @ExceptionHandler()中的參數(shù)是可以選擇的,這里是Exception.class:表示的是可以在程序拋出異常的時候執(zhí)行這里的代碼,讓其返回數(shù)據(jù)給前端,如果填入的參數(shù)是NullPointerException:那么表示的是當程序出現(xiàn)空指針異常的時候,會執(zhí)行這里的代碼。
這里的實現(xiàn)邏輯和Java中的異常處理是相似的,如果開發(fā)者有對Exception和NullPointerException分別進行了處理,那么當程序出現(xiàn)NullPointerException異常的時候,還是會根據(jù)我們寫的NullPointerException執(zhí)行邏輯進行處理,并不會直接走Exception的邏輯。
示例如下:
訪問頁面后效果如下:
總結(jié):當有多個異常通知時,匹配順序為當前類及其子類向上依次匹配。
三、統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么需要統(tǒng)一數(shù)據(jù)返回格式?
統(tǒng)一數(shù)據(jù)返回格式的優(yōu)點如下,比如以下幾個:
- 方便前端程序員更好的接受和解析后端數(shù)據(jù)接口的數(shù)據(jù)
- 降低前端程序員和后端程序員的溝通成本
- 有利于項目統(tǒng)一數(shù)據(jù)的維護和修改
- 有利于后端技術(shù)部門的統(tǒng)一規(guī)范的標準制定,不會出現(xiàn)稀奇古怪的返回內(nèi)容
3.2 統(tǒng)一數(shù)據(jù)返回格式的實現(xiàn)
統(tǒng)一的數(shù)據(jù)返回格式可以使用@ControllerAdvice + ResponseBodyAdvice 的方式實現(xiàn),具體實現(xiàn)代碼如下:
package com.example.demo.common;import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import java.util.HashMap;/*** 統(tǒng)一數(shù)據(jù)格式處理*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {/*** 是否執(zhí)行 beforeBodyWrite 方法, 返回 true 就執(zhí)行, 返回 false 就不執(zhí)行* @param returnType* @param converterType* @return*/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}/*** 返回數(shù)據(jù)之前進行數(shù)據(jù)重寫* @param body 原始返回值* @param returnType* @param selectedContentType* @param selectedConverterType* @param request* @param response* @return*/@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 這里我們規(guī)定統(tǒng)一的數(shù)據(jù)返回為HashMapif (body instanceof HashMap) {return body;}// 重寫返回結(jié)果,讓其返回一個統(tǒng)一的數(shù)據(jù)格式HashMap<String, Object> result = new HashMap<>();result.put("code",200);result.put("data",body);result.put("msg","");return result;}
}
?訪問user/login1:
經(jīng)過統(tǒng)一功能處理后代碼展現(xiàn)如下:
?3.3 返回值為String類型時,應(yīng)該如何處理?
?但是如果將返回值改為String類型,按照以上的執(zhí)行邏輯,那么就無法走上述的正常數(shù)據(jù)統(tǒng)一處理:
我們發(fā)現(xiàn),當返回類型為String的時候,程序會拋出異常,從而被我們的 統(tǒng)一異常處理模塊攔截。
觀察異常信息,發(fā)現(xiàn) 拋出異常:java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String
可能會感到奇怪,為什么會拋出這個異常呢?
下面我們來看看后端返回前端時候的執(zhí)行流程:
1.? 一開始,前端訪問該網(wǎng)址時,方法返回的是 String:
2. 統(tǒng)一數(shù)據(jù)返回之前會進行處理,將 String 轉(zhuǎn)換為 HashMap:
3. 將HaspMap轉(zhuǎn)換成 application/json 字符串給前端(接口)
這個步驟有兩種情況,先判斷原Body的類型:
- 是 String 類型,那么就會使用 StringHttpMessageConverter 進行類型轉(zhuǎn)換
- 如果不是 String 類型,那么使用 HttpMessageConverter 進行類型轉(zhuǎn)換
以上報錯就是因為原始Body是String類型,所以在類型轉(zhuǎn)換時候報錯了
解決方案有如下兩種:
- 將 StringHttpMessageConverter 去掉。
- 在統(tǒng)一數(shù)據(jù)返回的時候,單獨處理String類型,讓其返回一個String字符串,而非HashMap
3.3.1 將 StringHttpMessageConverter 去掉。
在配置文件中使用以下代碼即可;
package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.List;@Configuration
public class MyConfig implements WebMvcConfigurer {/*** 移除 StringHttpMessageConverter* @param converters*/@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);}
}
?訪問地址后顯示如下:
3.3.2 在統(tǒng)一數(shù)據(jù)返回的時候,單獨處理String類型,讓其返回一個String字符串,而非 HashMap
引入ObjectMapper(ObjectMapper?是Jackson庫中的一個類,用于在Java對象(POJO,Plain Old Java Objects)和JSON數(shù)據(jù)之間進行相互轉(zhuǎn)換):
對Body為String進行單獨處理:?
訪問頁面如下所示:
?總結(jié):
本文主要介紹了統(tǒng)一用戶登錄權(quán)限的效驗,使用WebMvcConfigurer + HandlerInterceptor 來實現(xiàn)。統(tǒng)一異常處理使用 @ControllerAdvice + @ExceptionHandler 來實現(xiàn),統(tǒng)一返回值處理使用@ControllerAdvice + ResponseBodyAdvice來處理。