本篇將要學習 Spring Boot 統一功能處理模塊,這也是 AOP 的實戰環節
用戶登錄權限的校驗實現接口 HandlerInterceptor + WebMvcConfigurer
異常處理使用注解 @RestControllerAdvice + @ExceptionHandler
數據格式返回使用注解 @ControllerAdvice 并且實現接口 @ResponseBodyAdvice
1. 統一用戶登錄權限效驗
用戶登錄權限的發展完善過程
最初用戶登錄效驗: 在每個方法中獲取 Session 和 Session 中的用戶信息,如果存在用戶,那么就認為登錄成功了,否則就登錄失敗了
第二版用戶登錄效驗: 提供統一的方法,在每個需要驗證的方法中調用統一的用戶登錄身份效驗方法來判斷
第三版用戶登錄效驗: 使用 Spring AOP 來統一進行用戶登錄效驗
第四版用戶登錄效驗: 使用 Spring 攔截器來實現用戶的統一登錄驗證
1.1 最初用戶登錄權限效驗
@RestController @RequestMapping("/user") publicclassUserController{ @RequestMapping("/a1") publicBooleanlogin(HttpServletRequestrequest){ //有Session就獲取,沒有就不創建 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ //說明已經登錄,進行業務處理 returntrue; }else{ //未登錄 returnfalse; } } @RequestMapping("/a2") publicBooleanlogin2(HttpServletRequestrequest){ //有Session就獲取,沒有就不創建 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ //說明已經登錄,進行業務處理 returntrue; }else{ //未登錄 returnfalse; } } }
這種方式寫的代碼,每個方法中都有相同的用戶登錄驗證權限,缺點是:
每個方法中都要單獨寫用戶登錄驗證的方法,即使封裝成公共方法,也一樣要傳參調用和在方法中進行判斷
添加控制器越多,調用用戶登錄驗證的方法也越多,這樣就增加了后期的修改成功和維護成功
這些用戶登錄驗證的方法和現在要實現的業務幾乎沒有任何關聯,但還是要在每個方法中都要寫一遍,所以提供一個公共的 AOP 方法來進行統一的用戶登錄權限驗證是非常好的解決辦法。
1.2 Spring AOP 統一用戶登錄驗證
統一用戶登錄驗證,首先想到的實現方法是使用 Spring AOP 前置通知或環繞通知來實現
@Aspect//當前類是一個切面 @Component publicclassUserAspect{ //定義切點方法Controller包下、子孫包下所有類的所有方法 @Pointcut("execution(*com.example.springaop.controller..*.*(..))") publicvoidpointcut(){} //前置通知 @Before("pointcut()") publicvoiddoBefore(){} //環繞通知 @Around("pointcut()") publicObjectdoAround(ProceedingJoinPointjoinPoint){ Objectobj=null; System.out.println("Around方法開始執行"); try{ obj=joinPoint.proceed(); }catch(Throwablee){ e.printStackTrace(); } System.out.println("Around方法結束執行"); returnobj; } }
但如果只在以上代碼 Spring AOP 的切面中實現用戶登錄權限效驗的功能,有這樣兩個問題:
沒有辦法得到 HttpSession 和 Request 對象
我們要對一部分方法進行攔截,而另一部分方法不攔截,比如注冊方法和登錄方法是不攔截的,也就是實際的攔截規則很復雜,使用簡單的 aspectJ 表達式無法滿足攔截的需求
1.3 Spring 攔截器
針對上面代碼 Spring AOP 的問題,Spring 中提供了具體的實現攔截器:HandlerInterceptor,攔截器的實現有兩步:
1.創建自定義攔截器,實現 Spring 中的 HandlerInterceptor 接口中的 preHandle方法
2.將自定義攔截器加入到框架的配置中,并且設置攔截規則
給當前的類添加 @Configuration 注解
實現 WebMvcConfigurer 接口
重寫 addInterceptors 方法
注意:一個項目中可以同時配置多個攔截器
(1)創建自定義攔截器
/** *@Description:自定義用戶登錄的攔截器 *@Date2023/2/1313:06 */ @Component publicclassLoginInterceptimplementsHandlerInterceptor{ //返回true表示攔截判斷通過,可以訪問后面的接口 //返回false表示攔截未通過,直接返回結果給前端 @Override publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse, Objecthandler)throwsException{ //1.得到HttpSession對象 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ //表示已經登錄 returntrue; } //執行到此代碼表示未登錄,未登錄就跳轉到登錄頁面 response.sendRedirect("/login.html"); returnfalse; } }
(2)將自定義攔截器添加到系統配置中,并設置攔截的規則
addPathPatterns:表示需要攔截的 URL,**表示攔截所有?法
excludePathPatterns:表示需要排除的 URL
說明:攔截規則可以攔截此項?中的使? URL,包括靜態?件(圖??件、JS 和 CSS 等?件)。
/** *@Description:將自定義攔截器添加到系統配置中,并設置攔截的規則 *@Date2023/2/1313:13 */ @Configuration publicclassAppConfigimplementsWebMvcConfigurer{ @Resource privateLoginInterceptloginIntercept; @Override publicvoidaddInterceptors(InterceptorRegistryregistry){ //registry.addInterceptor(newLoginIntercept());//可以直接new也可以屬性注入 registry.addInterceptor(loginIntercept). addPathPatterns("/**").//攔截所有url excludePathPatterns("/user/login").//不攔截登錄注冊接口 excludePathPatterns("/user/reg"). excludePathPatterns("/login.html"). excludePathPatterns("/reg.html"). excludePathPatterns("/**/*.js"). excludePathPatterns("/**/*.css"). excludePathPatterns("/**/*.png"). excludePathPatterns("/**/*.jpg"); } }
1.4 練習:登錄攔截器
要求
登錄、注冊頁面不攔截,其他頁面都攔截
當登錄成功寫入 session 之后,攔截的頁面可正常訪問
在 1.3 中已經創建了自定義攔截器 和 將自定義攔截器添加到系統配置中,并設置攔截的規則
(1)下面創建登錄和首頁的 html
(2)創建 controller 包,在包中創建 UserController,寫登錄頁面和首頁的業務代碼
@RestController @RequestMapping("/user") publicclassUserController{ @RequestMapping("/login") publicbooleanlogin(HttpServletRequestrequest,Stringusername,Stringpassword){ booleanresult=false; if(StringUtils.hasLength(username)&&StringUtils.hasLength(password)){ if(username.equals("admin")&&password.equals("admin")){ HttpSessionsession=request.getSession(); session.setAttribute("userinfo","userinfo"); returntrue; } } returnresult; } @RequestMapping("/index") publicStringindex(){ return"HelloIndex"; } }
(3)運行程序,訪問頁面,對比登錄前和登錄后的效果
1.5 攔截器實現原理
有了攔截器之后,會在調? Controller 之前進?相應的業務處理,執?的流程如下圖所示
實現原理源碼分析
所有的 Controller 執行都會通過一個調度器 DispatcherServlet 來實現
而所有方法都會執行 DispatcherServlet 中的 doDispatch 調度?法,doDispatch 源碼分析如下:
通過源碼分析,可以看出,Sping 中的攔截器也是通過動態代理和環繞通知的思想實現的
1.6 統一訪問前綴添加
所有請求地址添加 api 前綴,c 表示所有
@Configuration publicclassAppConfigimplementsWebMvcConfigurer{ //所有的接口添加api前綴 @Override publicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){ configurer.addPathPrefix("api",c->true); } }
2. 統一異常處理
給當前的類上加 @ControllerAdvice 表示控制器通知類
給方法上添加 @ExceptionHandler(xxx.class),表示異常處理器,添加異常返回的業務代碼
@RestController @RequestMapping("/user") publicclassUserController{ @RequestMapping("/index") publicStringindex(){ intnum=10/0; return"HelloIndex"; } }
在 config 包中,創建 MyExceptionAdvice 類
@RestControllerAdvice//當前是針對Controller的通知類(增強類) publicclassMyExceptionAdvice{ @ExceptionHandler(ArithmeticException.class) publicHashMaparithmeticExceptionAdvice(ArithmeticExceptione){ HashMap result=newHashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg","算出異常:"+e.getMessage()); returnresult; } }
也可以這樣寫,效果是一樣的
@ControllerAdvice publicclassMyExceptionAdvice{ @ExceptionHandler(ArithmeticException.class) @ResponseBody publicHashMaparithmeticExceptionAdvice(ArithmeticExceptione){ HashMap result=newHashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg","算數異常:"+e.getMessage()); returnresult; } }
如果再有一個空指針異常,那么上面的代碼是不行的,還要寫一個針對空指針異常處理器
@ExceptionHandler(NullPointerException.class) publicHashMapnullPointerExceptionAdvice(NullPointerExceptione){ HashMap result=newHashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg","空指針異常異常:"+e.getMessage()); returnresult; } @RequestMapping("/index") publicStringindex(HttpServletRequestrequest,Stringusername,Stringpassword){ Objectobj=null; System.out.println(obj.hashCode()); return"HelloIndex"; }
但是需要考慮的一點是,如果每個異常都這樣寫,那么工作量是非常大的,并且還有自定義異常,所以上面這樣寫肯定是不好的,既然是異常直接寫 Exception 就好了,它是所有異常的父類,如果遇到不是前面寫的兩種異常,那么就會直接匹配到 Exception
當有多個異常通知時,匹配順序為當前類及其?類向上依次匹配
@ExceptionHandler(Exception.class) publicHashMapexceptionAdvice(Exceptione){ HashMap result=newHashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg","異常:"+e.getMessage()); returnresult; }
可以看到優先匹配的還是前面寫的 空指針異常
3. 統一數據格式返回
3.1 統一數據格式返回的實現
1.給當前類添加 @ControllerAdvice
2.實現 ResponseBodyAdvice 重寫其方法
supports 方法,此方法表示內容是否需要重寫(通過此?法可以選擇性部分控制器和方法進行重寫),如果要重寫返回 true
beforeBodyWrite 方法,方法返回之前調用此方法
@ControllerAdvice publicclassMyResponseAdviceimplementsResponseBodyAdvice{ //返回一個boolean值,true表示返回數據之前對數據進行重寫,也就是會進入beforeBodyWrite方法 //返回false表示對結果不進行任何處理,直接返回 @Override publicbooleansupports(MethodParameterreturnType,ClassconverterType){ returntrue; } //方法返回之前調用此方法 @Override publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){ HashMapresult=newHashMap<>(); result.put("state",1); result.put("data",body); result.put("msg",""); returnresult; } } @RestController @RequestMapping("/user") publicclassUserController{ @RequestMapping("/login") publicbooleanlogin(HttpServletRequestrequest,Stringusername,Stringpassword){ booleanresult=false; if(StringUtils.hasLength(username)&&StringUtils.hasLength(password)){ if(username.equals("admin")&&password.equals("admin")){ HttpSessionsession=request.getSession(); session.setAttribute("userinfo","userinfo"); returntrue; } } returnresult; } @RequestMapping("/reg") publicintreg(){ return1; } }
3.2 @ControllerAdvice 源碼分析
通過對 @ControllerAdvice 源碼的分析我們可以知道上面統一異常和統一數據返回的執行流程
(1)先看 @ControllerAdvice 源碼
可以看到 @ControllerAdvice 派生于 @Component 組件而所有組件初始化都會調用 InitializingBean 接口
(2)下面查看 initializingBean 有哪些實現類
在查詢過程中發現,其中 Spring MVC 中的實現子類是 RequestMappingHandlerAdapter,它里面有一個方法 afterPropertiesSet()方法,表示所有的參數設置完成之后執行的方法
(3)而這個方法中有一個 initControllerAdviceCache 方法,查詢此方法
發現這個方法在執行時會查找使用所有的 @ControllerAdvice 類,發送某個事件時,調用相應的 Advice 方法,比如返回數據前調用統一數據封裝,比如發生異常是調用異常的 Advice 方法實現的。
審核編輯:劉清
-
控制器
+關注
關注
112文章
16394瀏覽量
178481 -
URL
+關注
關注
0文章
139瀏覽量
15370 -
CSS
+關注
關注
0文章
109瀏覽量
14391 -
API接口
+關注
關注
1文章
84瀏覽量
10463 -
SpringBoot
+關注
關注
0文章
173瀏覽量
184
原文標題:SpringBoot 統一功能處理:用戶登錄權限校驗-攔截器、異常處理、數據格式返回
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論