本篇將要學(xué)習(xí) Spring Boot 統(tǒng)一功能處理模塊,這也是 AOP 的實(shí)戰(zhàn)環(huán)節(jié)
用戶登錄權(quán)限的校驗(yàn)實(shí)現(xiàn)接口 HandlerInterceptor + WebMvcConfigurer
異常處理使用注解 @RestControllerAdvice + @ExceptionHandler
數(shù)據(jù)格式返回使用注解 @ControllerAdvice 并且實(shí)現(xiàn)接口 @ResponseBodyAdvice
1. 統(tǒng)一用戶登錄權(quán)限效驗(yàn)
用戶登錄權(quán)限的發(fā)展完善過程
最初用戶登錄效驗(yàn): 在每個方法中獲取 Session 和 Session 中的用戶信息,如果存在用戶,那么就認(rèn)為登錄成功了,否則就登錄失敗了
第二版用戶登錄效驗(yàn): 提供統(tǒng)一的方法,在每個需要驗(yàn)證的方法中調(diào)用統(tǒng)一的用戶登錄身份效驗(yàn)方法來判斷
第三版用戶登錄效驗(yàn): 使用 Spring AOP 來統(tǒng)一進(jìn)行用戶登錄效驗(yàn)
第四版用戶登錄效驗(yàn): 使用 Spring 攔截器來實(shí)現(xiàn)用戶的統(tǒng)一登錄驗(yàn)證
1.1 最初用戶登錄權(quán)限效驗(yàn)
@RestController @RequestMapping("/user") publicclassUserController{ @RequestMapping("/a1") publicBooleanlogin(HttpServletRequestrequest){ //有Session就獲取,沒有就不創(chuàng)建 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ //說明已經(jīng)登錄,進(jìn)行業(yè)務(wù)處理 returntrue; }else{ //未登錄 returnfalse; } } @RequestMapping("/a2") publicBooleanlogin2(HttpServletRequestrequest){ //有Session就獲取,沒有就不創(chuàng)建 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ //說明已經(jīng)登錄,進(jìn)行業(yè)務(wù)處理 returntrue; }else{ //未登錄 returnfalse; } } }
這種方式寫的代碼,每個方法中都有相同的用戶登錄驗(yàn)證權(quán)限,缺點(diǎn)是:
每個方法中都要單獨(dú)寫用戶登錄驗(yàn)證的方法,即使封裝成公共方法,也一樣要傳參調(diào)用和在方法中進(jìn)行判斷
添加控制器越多,調(diào)用用戶登錄驗(yàn)證的方法也越多,這樣就增加了后期的修改成功和維護(hù)成功
這些用戶登錄驗(yàn)證的方法和現(xiàn)在要實(shí)現(xiàn)的業(yè)務(wù)幾乎沒有任何關(guān)聯(lián),但還是要在每個方法中都要寫一遍,所以提供一個公共的 AOP 方法來進(jìn)行統(tǒng)一的用戶登錄權(quán)限驗(yàn)證是非常好的解決辦法。
1.2 Spring AOP 統(tǒng)一用戶登錄驗(yàn)證
統(tǒng)一用戶登錄驗(yàn)證,首先想到的實(shí)現(xiàn)方法是使用 Spring AOP 前置通知或環(huán)繞通知來實(shí)現(xiàn)
@Aspect//當(dāng)前類是一個切面
@Component
publicclassUserAspect{
//定義切點(diǎn)方法Controller包下、子孫包下所有類的所有方法
@Pointcut("execution(*com.example.springaop.controller..*.*(..))")
publicvoidpointcut(){}
//前置通知
@Before("pointcut()")
publicvoiddoBefore(){}
//環(huán)繞通知
@Around("pointcut()")
publicObjectdoAround(ProceedingJoinPointjoinPoint){
Objectobj=null;
System.out.println("Around方法開始執(zhí)行");
try{
obj=joinPoint.proceed();
}catch(Throwablee){
e.printStackTrace();
}
System.out.println("Around方法結(jié)束執(zhí)行");
returnobj;
}
}
但如果只在以上代碼 Spring AOP 的切面中實(shí)現(xiàn)用戶登錄權(quán)限效驗(yàn)的功能,有這樣兩個問題:
沒有辦法得到 HttpSession 和 Request 對象
我們要對一部分方法進(jìn)行攔截,而另一部分方法不攔截,比如注冊方法和登錄方法是不攔截的,也就是實(shí)際的攔截規(guī)則很復(fù)雜,使用簡單的 aspectJ 表達(dá)式無法滿足攔截的需求
1.3 Spring 攔截器
針對上面代碼 Spring AOP 的問題,Spring 中提供了具體的實(shí)現(xiàn)攔截器:HandlerInterceptor,攔截器的實(shí)現(xiàn)有兩步:
1.創(chuàng)建自定義攔截器,實(shí)現(xiàn) Spring 中的 HandlerInterceptor 接口中的 preHandle方法
2.將自定義攔截器加入到框架的配置中,并且設(shè)置攔截規(guī)則
給當(dāng)前的類添加 @Configuration 注解
實(shí)現(xiàn) WebMvcConfigurer 接口
重寫 addInterceptors 方法
注意:一個項目中可以同時配置多個攔截器
(1)創(chuàng)建自定義攔截器
/**
*@Description:自定義用戶登錄的攔截器
*@Date2023/2/1313:06
*/
@Component
publicclassLoginInterceptimplementsHandlerInterceptor{
//返回true表示攔截判斷通過,可以訪問后面的接口
//返回false表示攔截未通過,直接返回結(jié)果給前端
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,
Objecthandler)throwsException{
//1.得到HttpSession對象
HttpSessionsession=request.getSession(false);
if(session!=null&&session.getAttribute("userinfo")!=null){
//表示已經(jīng)登錄
returntrue;
}
//執(zhí)行到此代碼表示未登錄,未登錄就跳轉(zhuǎn)到登錄頁面
response.sendRedirect("/login.html");
returnfalse;
}
}
(2)將自定義攔截器添加到系統(tǒng)配置中,并設(shè)置攔截的規(guī)則
addPathPatterns:表示需要攔截的 URL,**表示攔截所有?法
excludePathPatterns:表示需要排除的 URL
說明:攔截規(guī)則可以攔截此項?中的使? URL,包括靜態(tài)?件(圖??件、JS 和 CSS 等?件)。
/**
*@Description:將自定義攔截器添加到系統(tǒng)配置中,并設(shè)置攔截的規(guī)則
*@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 練習(xí):登錄攔截器
要求
登錄、注冊頁面不攔截,其他頁面都攔截
當(dāng)?shù)卿洺晒懭?session 之后,攔截的頁面可正常訪問
在 1.3 中已經(jīng)創(chuàng)建了自定義攔截器 和 將自定義攔截器添加到系統(tǒng)配置中,并設(shè)置攔截的規(guī)則
(1)下面創(chuàng)建登錄和首頁的 html

(2)創(chuàng)建 controller 包,在包中創(chuàng)建 UserController,寫登錄頁面和首頁的業(yè)務(wù)代碼
@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)運(yùn)行程序,訪問頁面,對比登錄前和登錄后的效果


1.5 攔截器實(shí)現(xiàn)原理
有了攔截器之后,會在調(diào)? Controller 之前進(jìn)?相應(yīng)的業(yè)務(wù)處理,執(zhí)?的流程如下圖所示

實(shí)現(xiàn)原理源碼分析
所有的 Controller 執(zhí)行都會通過一個調(diào)度器 DispatcherServlet 來實(shí)現(xiàn)

而所有方法都會執(zhí)行 DispatcherServlet 中的 doDispatch 調(diào)度?法,doDispatch 源碼分析如下:

通過源碼分析,可以看出,Sping 中的攔截器也是通過動態(tài)代理和環(huán)繞通知的思想實(shí)現(xiàn)的
1.6 統(tǒng)一訪問前綴添加
所有請求地址添加 api 前綴,c 表示所有
@Configuration
publicclassAppConfigimplementsWebMvcConfigurer{
//所有的接口添加api前綴
@Override
publicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){
configurer.addPathPrefix("api",c->true);
}
}

2. 統(tǒng)一異常處理
給當(dāng)前的類上加 @ControllerAdvice 表示控制器通知類
給方法上添加 @ExceptionHandler(xxx.class),表示異常處理器,添加異常返回的業(yè)務(wù)代碼
@RestController
@RequestMapping("/user")
publicclassUserController{
@RequestMapping("/index")
publicStringindex(){
intnum=10/0;
return"HelloIndex";
}
}
在 config 包中,創(chuàng)建 MyExceptionAdvice 類
@RestControllerAdvice//當(dāng)前是針對Controller的通知類(增強(qiáng)類)
publicclassMyExceptionAdvice{
@ExceptionHandler(ArithmeticException.class)
publicHashMaparithmeticExceptionAdvice(ArithmeticExceptione){
HashMapresult=newHashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg","算出異常:"+e.getMessage());
returnresult;
}
}
也可以這樣寫,效果是一樣的
@ControllerAdvice
publicclassMyExceptionAdvice{
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
publicHashMaparithmeticExceptionAdvice(ArithmeticExceptione){
HashMapresult=newHashMap<>();
result.put("state",-1);
result.put("data",null);
result.put("msg","算數(shù)異常:"+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"; }

但是需要考慮的一點(diǎn)是,如果每個異常都這樣寫,那么工作量是非常大的,并且還有自定義異常,所以上面這樣寫肯定是不好的,既然是異常直接寫 Exception 就好了,它是所有異常的父類,如果遇到不是前面寫的兩種異常,那么就會直接匹配到 Exception
當(dāng)有多個異常通知時,匹配順序?yàn)楫?dāng)前類及其?類向上依次匹配
@ExceptionHandler(Exception.class) publicHashMapexceptionAdvice(Exceptione){ HashMap result=newHashMap<>(); result.put("state",-1); result.put("data",null); result.put("msg","異常:"+e.getMessage()); returnresult; }
可以看到優(yōu)先匹配的還是前面寫的 空指針異常

3. 統(tǒng)一數(shù)據(jù)格式返回
3.1 統(tǒng)一數(shù)據(jù)格式返回的實(shí)現(xiàn)
1.給當(dāng)前類添加 @ControllerAdvice
2.實(shí)現(xiàn) ResponseBodyAdvice 重寫其方法
supports 方法,此方法表示內(nèi)容是否需要重寫(通過此?法可以選擇性部分控制器和方法進(jìn)行重寫),如果要重寫返回 true
beforeBodyWrite 方法,方法返回之前調(diào)用此方法
@ControllerAdvice
publicclassMyResponseAdviceimplementsResponseBodyAdvice{
//返回一個boolean值,true表示返回數(shù)據(jù)之前對數(shù)據(jù)進(jìn)行重寫,也就是會進(jìn)入beforeBodyWrite方法
//返回false表示對結(jié)果不進(jìn)行任何處理,直接返回
@Override
publicbooleansupports(MethodParameterreturnType,ClassconverterType){
returntrue;
}
//方法返回之前調(diào)用此方法
@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 源碼的分析我們可以知道上面統(tǒng)一異常和統(tǒng)一數(shù)據(jù)返回的執(zhí)行流程
(1)先看 @ControllerAdvice 源碼

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

(3)而這個方法中有一個 initControllerAdviceCache 方法,查詢此方法

發(fā)現(xiàn)這個方法在執(zhí)行時會查找使用所有的 @ControllerAdvice 類,發(fā)送某個事件時,調(diào)用相應(yīng)的 Advice 方法,比如返回數(shù)據(jù)前調(diào)用統(tǒng)一數(shù)據(jù)封裝,比如發(fā)生異常是調(diào)用異常的 Advice 方法實(shí)現(xiàn)的。
審核編輯:劉清
-
控制器
+關(guān)注
關(guān)注
114文章
17791瀏覽量
193204 -
URL
+關(guān)注
關(guān)注
0文章
142瀏覽量
16224 -
CSS
+關(guān)注
關(guān)注
0文章
110瀏覽量
15506 -
API接口
+關(guān)注
關(guān)注
1文章
114瀏覽量
11248 -
SpringBoot
+關(guān)注
關(guān)注
0文章
177瀏覽量
688
原文標(biāo)題:SpringBoot 統(tǒng)一功能處理:用戶登錄權(quán)限校驗(yàn)-攔截器、異常處理、數(shù)據(jù)格式返回
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
springboot集成mqtt
SpringBoot應(yīng)用啟動運(yùn)行run方法
怎樣去設(shè)計一個基于springboot+freemark+jpa+MySQL的在線電影訂票系統(tǒng)
Springboot是如何獲取自定義異常并進(jìn)行返回的
公司這套架構(gòu)統(tǒng)一處理try...catch真香!
什么是 SpringBoot?
SpringBoot的核心注解1
SpringBoot的核心注解2
springboot統(tǒng)一異常處理
SpringBoot攔截器與統(tǒng)一功能處理實(shí)戰(zhàn)
如何使用SpringBoot、Vue2.0、MySQL開發(fā)一套云診所系統(tǒng)?
SpringBoot統(tǒng)一功能處理
評論