網(wǎng)站開發(fā)主管待遇優(yōu)化大師安卓版
一、用戶登錄權限效驗
用戶登錄權限的發(fā)展從之前每個方法中自己驗證用戶登錄權限,到現(xiàn)在統(tǒng)一的用戶登錄驗證處理,它是一個逐漸完善和逐漸優(yōu)化的過程。
1.1 最初用戶登錄驗證
我們先來回顧一下最初用戶登錄驗證的實現(xiàn)方法:
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某方法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
// 有 session 就獲取,沒有不會創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,業(yè)務處理
return true;
} else {
// 未登錄
return false;
}
}
/**
* 某方法 2
*/
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
// 有 session 就獲取,沒有不會創(chuàng)建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 說明已經(jīng)登錄,業(yè)務處理
return true;
} else {
// 未登錄
return false;
}
}
// 其他方法...
}
從上述代碼可以看出,每個方法中都有相同的用戶登錄驗證權限,它的缺點是:
- 每個方法中都要單獨寫用戶登錄驗證的方法,即使封裝成公共方法,也一樣要傳參調(diào)用和在方法中進行判斷。
- 添加控制器越多,調(diào)用用戶登錄驗證的方法也越多,這樣就增加了后期的修改成本和維護成本。
- 這些用戶登錄驗證的方法和接下來要實現(xiàn)的業(yè)務幾何沒有任何關聯(lián),但每個方法中都要寫一遍。
?所以提供一個公共的 AOP 方法來進行統(tǒng)一的用戶登錄權限驗證迫在眉睫。
1.2 Spring AOP 用戶統(tǒng)一登錄驗證的問題
說到統(tǒng)一的用戶登錄驗證,我們想到的第一個實現(xiàn)方案是 Spring AOP 前置通知或環(huán)繞通知來實現(xiàn),具 體實現(xiàn)代碼如下:
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 方法結束執(zhí)行");
return obj;
}
}
如果要在以上 Spring AOP 的切面中實現(xiàn)用戶登錄權限效驗的功能,有以下兩個問題:
1. 沒辦法獲取到 HttpSession 對象。2. 我們要對一部分方法進行攔截,而另一部分方法不攔截,如注冊方法和登錄方法是不攔截的,這樣的話排除方法的規(guī)則很難定義,甚至沒辦法定義。
那這樣如何解決呢?
1.3 Spring 攔截器
對于以上問題 Spring 中提供了具體的實現(xiàn)攔截器: HandlerInterceptor ,攔截器的實現(xiàn)分為以下兩個步驟:
1. 創(chuàng)建自定義攔截器,實現(xiàn) HandlerInterceptor 接口的 preHandle (執(zhí)行具體方法之前的預處理)方法。2. 將自定義攔截器加入 WebMvcConfigurer 的 addInterceptors 方法中。
1.3.1 自定義攔截器
接下來使用代碼來實現(xiàn)一個用戶登錄的權限效驗,自定義攔截器是一個普通類,具體實現(xiàn)代碼如下:
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 {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true;
}
response.setStatus(401);
return false;
}
}
1.3.2 將自定義攔截器加入到系統(tǒng)配置
將上一步中的自定義攔截器加入到系統(tǒng)配置信息中,具體實現(xiàn)代碼如下:
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 添加攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 攔截所有接口
.excludePathPatterns("/art/param11"); // 排除接口
}
}
其中:
addPathPatterns :表示需要攔截的 URL , “**” 表示攔截任意方法(也就是所有方法)。excludePathPatterns :表示需要排除的 URL 。
說明:以上攔截規(guī)則可以攔截此項目中的使用 URL ,包括靜態(tài)文件(圖片文件、 JS 和 CSS 等文件)。
排除所有的靜態(tài)資源
// 攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 攔截所有接口
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.jpg")
.excludePathPatterns("/login.html")
.excludePathPatterns("/**/login"); // 排除接口
}
1.4 攔截器實現(xiàn)原理
正常情況下的調(diào)用順序:

然而有了攔截器之后,會在調(diào)用 Controller 之前進行相應的業(yè)務處理,執(zhí)行的流程如下圖所示:
1.4.1 實現(xiàn)原理源碼分析
所有的 Controller 執(zhí)行都會通過一個調(diào)度器 DispatcherServlet 來實現(xiàn),這一點可以從 Spring Boot 控制臺的打印信息看出,如下圖所示:

而所有方法都會執(zhí)行 DispatcherServlet 中的 doDispatch 調(diào)度方法,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;
}
}
// 調(diào)用預處理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 執(zhí)行 Controller 中的業(yè)務
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);
}
}
}
從上述源碼可以看出在開始執(zhí)行 Controller 之前,會先調(diào)用 預處理方法 applyPreHandle ,而
applyPreHandle 方法的實現(xiàn)源碼如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response)
throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++)
{
// 獲取項目中使用的攔截器 HandlerInterceptor
HandlerInterceptor interceptor =
(HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
從上述源碼可以看出,在 applyPreHandle 中會獲取所有的攔截器 HandlerInterceptor 并執(zhí)行攔截器中的 preHandle 方法,這樣就會咱們前面定義的攔截器對應上了,如下圖所示:
?
此時用戶登錄權限的驗證方法就會執(zhí)行,這就是攔截器的實現(xiàn)原理。
1.4.2 攔截器小結
通過上面的源碼分析,我們可以看出, Spring 中的攔截器也是通過動態(tài)代理和環(huán)繞通知的思想實現(xiàn)的,大體的調(diào)用流程如下:

1.5 擴展:統(tǒng)一訪問前綴添加
所有請求地址添加 api 前綴:
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 所有的接口添加 api 前綴
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}
其中第二個參數(shù)是一個表達式,設置為 true 表示啟動前綴。
二、統(tǒng)一異常處理
統(tǒng)一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來實現(xiàn)的, @ControllerAdvice 表示控制器通知類,@ExceptionHandler 是異常處理器,兩個結合表示當出現(xiàn)異常的時候執(zhí)行某個通知,也就是執(zhí)行某個方法事件,具體實現(xiàn)代碼如下:
import java.util.HashMap;
@ControllerAdvice
public class ErrorAdive {
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handler(Exception e) {
HashMap<String, Object> map = new HashMap<>();
map.put("success", 0);
map.put("status", 1);
map.put("msg", e.getMessage());
return map;
}
}
PS :方法名和返回值可以自定義,其中最重要的是 @ExceptionHandler(Exception.class) 注解。
以上方法表示,如果出現(xiàn)了異常就返回給前端一個 HashMap 的對象,其中包含的字段如代碼中定義的那樣。
我們可以針對不同的異常,返回不同的結果,比以下代碼所示:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public Object exceptionAdvice(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("success", -1);
result.put("message", "總的異常信息:" + e.getMessage());
result.put("data", null);
return result;
}
@ExceptionHandler(NullPointerException.class)
public Object nullPointerexceptionAdvice(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("success", -1);
result.put("message", "空指針異常:" + e.getMessage());
result.put("data", null);
return result;
}
}
當有多個異常通知時,匹配順序為當前類及其子類向上依次匹配,案例演示。
在 UserController 中設置一個空指針異常,實現(xiàn)代碼如下:
@RestController
@RequestMapping("/u")
public class UserController {
@RequestMapping("/index")
public String index() {
Object obj = null;
int i = obj.hashCode();
return "Hello,User Index.";
}
}
以上程序的執(zhí)行結果如下:

三、統(tǒng)一數(shù)據(jù)返回格式
3.1 為什么需要統(tǒng)一數(shù)據(jù)返回格式?
統(tǒng)一數(shù)據(jù)返回格式的優(yōu)點有很多,比如以下幾個:
1. 方便前端程序員更好的接收和解析后端數(shù)據(jù)接口返回的數(shù)據(jù)。2. 降低前端程序員和后端程序員的溝通成本,按照某個格式實現(xiàn)就行了,因為所有接口都是這樣返回的。3. 有利于項目統(tǒng)一數(shù)據(jù)的維護和修改。4. 有利于后端技術部門的統(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)代碼如下:
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;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 內(nèi)容是否需要重寫(通過此方法可以選擇性部分控制器和方法進行重寫)
* 返回 true 表示重寫
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 方法返回之前調(diào)用此方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest
request,
ServerHttpResponse response) {
// 構造統(tǒng)一返回對象
HashMap<String, Object> result = new HashMap<>();
result.put("success", 1);
result.put("message", "");
result.put("data", body);
return result;
}
}
3.3 @ControllerAdvice 源碼分析
通過對 @ControllerAdvice 源碼的分析我們可以知道上面統(tǒng)一異常和統(tǒng)一數(shù)據(jù)返回的執(zhí)行流程,我們先從 @ControllerAdvice 的源碼看起,點擊 @ControllerAdvice 實現(xiàn)源碼如下:

從上述源碼可以看出 @ControllerAdvice 派生于 @Component 組件,而所有組件初始化都會調(diào)用
InitializingBean 接口。
所以接下來我們來看 InitializingBean 有哪些實現(xiàn)類?在查詢的過程中我們發(fā)現(xiàn)了,其中 Spring MVC 中的實現(xiàn)子類是 RequestMappingHandlerAdapter ,它里面有一個方法 afterPropertiesSet() 方法,表示所有的參數(shù)設置完成之后執(zhí)行的方法,如下圖所示:

而這個方法中有一個 initControllerAdviceCache 方法,查詢此方法的源碼如下:
總結
本篇博客介紹了
- 統(tǒng)一用戶登錄權限的效驗使用 WebMvcConfigurer+ HandlerInterceptor來實現(xiàn)
- 統(tǒng)一異常 處理使用 @ControllerAdvice + @ExceptionHandler 來實現(xiàn)
- 統(tǒng)一返回值處理使用 @ControllerAdvice + ResponseBodyAdvice 來處理。