前言🍭
??????SSM專欄更新中,各位大佬覺得寫得不錯,支持一下,感謝了!??????
Spring + Spring MVC + MyBatis_冷兮雪的博客-CSDN博客
本章是講Spring Boot 統?功能處理模塊,也是 AOP 的實戰環節,要實現的目標有以下 3 個:
- 使用攔截器實現用戶登錄權限的統一驗證;
- 統?數據格式返回;
- 統?異常處理。
一、用戶登錄權限效驗🍭
1、最初用戶登錄驗證🍉
用戶登錄權限的發展從之前每個方法中自己驗證用戶登錄權限,到現在統?的用戶登錄驗證處理,它是?個逐漸完善和逐漸優化的過程。
@RestController
@RequestMapping("/user")
public class UserController {/*** 某?法 1*/@RequestMapping("/m1")public Object method(HttpServletRequest request) {// 有 session 就獲取,沒有不會創建HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 說明已經登錄,業務處理return true;} else {// 未登錄return false;}}/*** 某?法 2*/@RequestMapping("/m2")public Object method2(HttpServletRequest request) {// 有 session 就獲取,沒有不會創建HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 說明已經登錄,業務處理return true;} else {// 未登錄return false;}}// 其他?法。。。
}
從上述代碼可以看出,每個方法中都有相同的用戶登錄驗證權限,它的缺點是:
- 每個方法中都要單獨寫用戶登錄驗證的方法,即使封裝成公共方法,也?樣要傳參調用和在方法中進行判斷。
- 添加控制器越多,調用用戶登錄驗證的方法也越多,這樣就增加了后期的修改成本和維護成本。
- 這些用戶登錄驗證的方法和接下來要實現的業務幾何沒有任何關聯,但每個方法中都要寫?遍。 所以提供?個公共的 AOP 方法來進行統?的用戶登錄權限驗證迫在眉睫。
2、Spring AOP 用戶統?登錄驗證的問題🍉
說到統?的用戶登錄驗證,我們想到的第?個實現方案是 Spring AOP 前置通知或環繞通知來實現,具體實現代碼如下:
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() {}// 環繞?法@Around("pointcut()")public Object doAround(ProceedingJoinPoint joinPoint) {Object obj = null;System.out.println("Around ?法開始執?");try {// 執?攔截?法obj = joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}System.out.println("Around 方法結束執行");return obj;}
}
如果要在以上 Spring AOP 的切面中實現用戶登錄權限效驗的功能,有以下兩個問題:
- 定義攔截的規則(表達式)非常難。(我們要對?部分方法進行攔截,而另?部分方法不攔截,如注冊方法和登錄方法是不攔截的,這樣 的話排除方法的規則很難定義,甚至沒辦法定義)。
-
在切面類中拿到 HttpSession 比較難
Spring 攔截器🍉
對于以上問題 Spring 中提供了具體的實現攔截器:HandlerInterceptor,攔截器的實現分為以下兩個步驟:
- 創建自定義攔截器,實現 HandlerInterceptor 接口的 preHandle(執行具體方法之前的預處理方法。
- 將自定義攔截器加入WebMvcConfigurer 的 addInterceptors 方法中。
具體實現如下:
目錄結構:
Ⅰ、實現攔截器🍓
關鍵步驟:
a.實現 HandlerInterceptor 接口
b.重寫 preHeadler 方法,在方法中編寫自己的業務代碼
package com.example.demo.config;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 {/*** 此方法返回一個 boolean,如果為 true 表示驗證成功,可以繼續執行后續流程如果是 false 表示驗證失敗,后面的流程不能執行了* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//用戶登錄業務判斷HttpSession session=request.getSession(false);if (session != null && session.getAttribute("userinfo") != null) {// 說明用戶已經登錄return true;}// 可以調整到登錄頁面 or 返回一個 401/403 沒有權限碼response.sendRedirect("/login.html");
// response.setStatus(403);return false;}
}
Ⅱ、將攔截器添加到配置文件中,并且設置攔截的規則🍓
package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class AppConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 攔截所有請求.excludePathPatterns("/user/login") // 排除的url地址(不攔截的url地址).excludePathPatterns("/user/reg");}
}
其中:
- addPathPatterns:表示需要攔截的 URL,**表示攔截任意方法(也就是所有方法)。
- excludePathPatterns:表示需要排除的 URL。
說明:以上攔截規則可以攔截此項目中的使用?URL,包括靜態文件(圖片文件、JS 和 CSS 等文件)。
排除所有的靜態資源:
// 攔截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 攔截所有接?.excludePathPatterns("/**/*.js").excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.jpg").excludePathPatterns("/login.html").excludePathPatterns("/**/login"); // 排除接?}
UserController:
package com.example.demo.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class UserController {/* @GetMappingpublic String getMethod(){return "執行GET請求!";}@PostMappingpublic String postMethod(){return "執行POST請求!";}*/@RequestMapping("/getuser")public String getUser() {System.out.println("執行了 get User~");return "get user";}@RequestMapping("/login")public String login() {System.out.println("執行了 login~");return "login~";}@RequestMapping("/reg")public String reg() {System.out.println("執行了 reg~");return "reg~";}
}
Ⅲ、啟動項目:🍓
不攔截:
?攔截:http://localhost:8080/user/getuser
?為什么會顯示重定向次數過多?
這是因為這個login.html頁面也被攔截了,所以它去訪問時候就會一直重定向重定向去訪問。
解決方法:
Ⅳ、攔截器實現原理🍓
正常情況下的調用順序:
然而有了攔截器之后,會在調用?Controller 之前進行相應的業務處理,執行的流程如下圖所示:
?所有的 Controller 執行都會通過?個調度器 DispatcherServlet 來實現,這?點可以從 Spring Boot 控制臺的打印信息看出,如下圖所示:
而所有方法都會執行?DispatcherServlet 中的 doDispatch 調度方法,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;Exception 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;}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);}}}
從上述源碼可以看出在開始執行?Controller 之前,會先調用?預處理方法 applyPreHandle,而applyPreHandle 方法的實現源碼如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 獲取項?中使?的攔截器 HandlerInterceptorHandlerInterceptor 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 并執行攔截器中的 preHandle 方法,這樣就和咱們前面定義的攔截器對應上了,如下圖所示:
此時用戶登錄權限的驗證方法就會執行,這就是攔截器的實現原理。 ?
二、統?異常處理🍭
Ⅰ、實現🍓
統一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來實現的,@ControllerAdvice 表示控制器通知類,@ExceptionHandler 是異常處理器,兩個結合表示當出現異常的時候執行某個通知, 也就是執行某個方法事件,具體實現代碼如下:
package com.example.demo.config;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 MyExHandler {/*** 攔截所有的空指針異常,進行統一的數據返回*/@ExceptionHandler(NullPointerException.class)public HashMap<String, Object> nullException(NullPointerException e) {HashMap<String, Object> result = new HashMap<>();result.put("code", "-1");result.put("msg", "空指針異常:" + e.getMessage()); // 錯誤碼的描述信息result.put("data", null);return result;}@ExceptionHandler(Exception.class)//保底的默認異常public HashMap<String, Object> exception(Exception e) {HashMap<String, Object> result = new HashMap<>();result.put("code", "-1");result.put("msg", "異常:" + e.getMessage()); // 錯誤碼的描述信息result.put("data", null);return result;}}
Ⅱ、添加異常🍓
UserController
@RequestMapping("/login")public String login() {Object obj = null;obj.hashCode();System.out.println("執行了 login~");return "login~";}@RequestMapping("/reg")public String reg() {int num = 10 / 0;System.out.println("執行了 reg~");return "reg~";}
Ⅲ、啟動程序:🍓
login:
?reg:
?方法名和返回值可以自定義,其中最重要的是 @ExceptionHandler(Exception.class) 注解。?
以上方法表示,如果出現了異常就返回給前端?個 HashMap 的對象,其中包含的字段如代碼中定義的那樣。 我們可以針對不同的異常,返回不同的結果。?
三、統一數據返回格式🍭
1.創建一個類,并添加 @ControllerAdvice
2.實現ResponseBodyAdvice接口,并重寫supports和beforeBodywrite (統一對象就是此方法中實現的)
1、為什么需要統一數據返回格式?🍉
統一數據返回格式的優點有很多,比如以下幾個:
- 方便前端程序員更好的接收和解析后端數據接口返回的數據。
- 降低前端程序員和后端程序員的溝通成本,按照某個格式實現就行了,因為所有接口都是這樣返回的。
- 有利于項目統?數據的維護和修改。
- 有利于后端技術部門的統?規范的標準制定,不會出現稀奇古怪的返回內容。
2、統一數據返回格式的實現🍉
Ⅰ、實現🍓
統?的數據返回格式可以使用?@ControllerAdvice + ResponseBodyAdvice 的方式實現。
package com.example.demo.config;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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 {@Autowiredprivate ObjectMapper objectMapper;/*** 此方法返回 true 則執行下面 beforeBodyWrite 方法* 反之則不執行*/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {HashMap<String, Object> result = new HashMap<>();result.put("code", 200);result.put("msg", "");result.put("data", body);// 需要特殊處理,因為 String 在轉換的時候會報錯if (body instanceof String) {try {return objectMapper.writeValueAsString(result);} catch (JsonProcessingException e) {e.printStackTrace();}}return result;}
}
如果沒有對Sting進行特殊處理,就會有異常出現:?
Ⅱ、添加實現類🍓
UserController
@RequestMapping("/getnum")public Integer getNumber() {return new Random().nextInt(10);}@RequestMapping("/getuser")public String getUser() {System.out.println("執行了 get User~");return "get user";}
Ⅲ、啟動程序🍓
Ⅳ、總結🍓
上面只是以一種簡單的方式來向大家介紹如何去統一數據返回格式,在實際工作中并不會這樣去使用
而是將統?的返回格式(包含:status、data、msg 字段)進行封裝成一個類:
根據不同操作成功和操作失敗的情況,重寫了不同的方法:
package com.example.demo.common;import lombok.Data;import java.io.Serializable;/*** 統一數據格式返回*/
@Data
public class AjaxResult implements Serializable {//支持序列化// 狀態碼private Integer code;// 狀態碼描述信息private String msg;// 返回的數據private Object data;/*** 操作成功返回的結果,需要 data*/public static AjaxResult success(Object data) {AjaxResult result = new AjaxResult();result.setCode(200);result.setMsg("");result.setData(data);return result;}//重載successpublic static AjaxResult success(int code, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg("");result.setData(data);return result;}public static AjaxResult success(int code, String msg, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}/*** 返回失敗結果*/public static AjaxResult fail(int code, String msg) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(null);return result;}public static AjaxResult fail(int code, String msg, Object data) {AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}
}
?
?