前言
Spring AOP是一個(gè)基于面向切面編程的框架,用于將橫切性關(guān)注點(diǎn)(如日志記錄、事務(wù)管理)與業(yè)務(wù)邏輯分離,通過(guò)代理對(duì)象將這些關(guān)注點(diǎn)織入到目標(biāo)對(duì)象的方法執(zhí)行前后、拋出異常或返回結(jié)果時(shí)等特定位置執(zhí)行,從而提高程序的可復(fù)用性、可維護(hù)性和靈活性。
但使用原生Spring AOP實(shí)現(xiàn)統(tǒng)一的攔截是非常繁瑣、困難的。而在本節(jié),我們將使用一種簡(jiǎn)單的方式進(jìn)行統(tǒng)一功能處理,這也是AOP的一次實(shí)戰(zhàn),具體如下:
統(tǒng)一用戶登錄權(quán)限驗(yàn)證
統(tǒng)一數(shù)據(jù)格式返回
統(tǒng)一異常處理
0 為什么需要統(tǒng)一功能處理?
統(tǒng)一功能處理是為了提高代碼的可維護(hù)性、可重用性和可擴(kuò)展性而進(jìn)行的一種設(shè)計(jì)思想。在應(yīng)用程序中,可能存在一些通用的功能需求,例如身份驗(yàn)證、日志記錄、異常處理等。
這些功能需要在多個(gè)地方進(jìn)行調(diào)用和處理,如果每個(gè)地方都單獨(dú)實(shí)現(xiàn)這些功能,會(huì)導(dǎo)致代碼冗余、難以維護(hù)和重復(fù)勞動(dòng)。通過(guò)統(tǒng)一功能處理的方式,可以將這些通用功能抽取出來(lái),以統(tǒng)一的方式進(jìn)行處理。這樣做有以下幾個(gè)好處:
「代碼復(fù)用」 :將通用功能抽取成獨(dú)立的模塊或組件,可以在多個(gè)地方共享使用,減少重復(fù)編寫(xiě)代碼的工作量。
「可維護(hù)性」 :將通用功能集中處理,可以方便地對(duì)其進(jìn)行修改、優(yōu)化或擴(kuò)展,而不需要在多個(gè)地方進(jìn)行修改。
「代碼整潔性」 :通過(guò)統(tǒng)一功能處理,可以使代碼更加清晰、簡(jiǎn)潔,減少了冗余的代碼。
「可擴(kuò)展性」 :當(dāng)需要添加新的功能時(shí),只需要在統(tǒng)一功能處理的地方進(jìn)行修改或擴(kuò)展,而不需要在多個(gè)地方進(jìn)行修改,降低了代碼的耦合度。
1 統(tǒng)一用戶登錄權(quán)限驗(yàn)證
1.1 使用原生 Spring AOP 實(shí)現(xiàn)統(tǒng)一攔截的難點(diǎn)
以使用原生 Spring AOP 來(lái)實(shí)現(xiàn)?戶統(tǒng)?登錄驗(yàn)證為例,主要是使用前置通知和環(huán)繞通知實(shí)現(xiàn)的,具體實(shí)現(xiàn)如下
importorg.aspectj.lang.ProceedingJoinPoint; importorg.aspectj.lang.annotation.*; importorg.springframework.stereotype.Component; /** *@author興趣使然黃小黃 *@version1.0 *@date2023/7/1816:37 */ @Aspect//表明此類(lèi)為一個(gè)切面 @Component//隨著框架的啟動(dòng)而啟動(dòng) publicclassUserAspect{ //定義切點(diǎn),這里使用Aspect表達(dá)式語(yǔ)法 @Pointcut("execution(*com.hxh.demo.controller.UserController.*(..))") publicvoidpointcut(){} //前置通知 @Before("pointcut()") publicvoidbeforeAdvice(){ System.out.println("執(zhí)行了前置通知~"); } //環(huán)繞通知 @Around("pointcut()") publicObjectaroundAdvice(ProceedingJoinPointjoinPoint){ System.out.println("進(jìn)入環(huán)繞通知~"); Objectobj=null; //執(zhí)行目標(biāo)方法 try{ obj=joinPoint.proceed(); }catch(Throwablee){ e.printStackTrace(); } System.out.println("退出環(huán)繞通知~"); returnobj; } }
從上述的代碼示例可以看出,使用原生的 Spring AOP 實(shí)現(xiàn)統(tǒng)一攔截的難點(diǎn)主要有以下幾個(gè)方面:
定義攔截規(guī)則非常困難。如注冊(cè)?法和登錄?法是不攔截的,這樣的話排除?法的規(guī)則很難定義,甚?沒(méi)辦法定義。
在切面類(lèi)中拿到 HttpSession 比較難。
為了解決 Spring AOP 的這些問(wèn)題,Spring 提供了攔截器~
1.2 使用 Spring 攔截器實(shí)現(xiàn)統(tǒng)一用戶登錄驗(yàn)證
Spring攔截器是Spring框架提供的一個(gè)功能強(qiáng)大的組件,用于在請(qǐng)求到達(dá)控制器之前或之后進(jìn)行攔截和處理。攔截器可以用于實(shí)現(xiàn)各種功能,如身份驗(yàn)證、日志記錄、性能監(jiān)測(cè)等。
要使用Spring攔截器,需要?jiǎng)?chuàng)建一個(gè)實(shí)現(xiàn)了HandlerInterceptor接口的攔截器類(lèi)。該接口定義了三個(gè)方法:preHandle、postHandle和afterCompletion。
preHandle方法在請(qǐng)求到達(dá)控制器之前執(zhí)行,可以用于進(jìn)行身份驗(yàn)證、參數(shù)校驗(yàn)等;
postHandle方法在控制器處理完請(qǐng)求后執(zhí)行,可以對(duì)模型和視圖進(jìn)行操作;
afterCompletion方法在視圖渲染完成后執(zhí)行,用于清理資源或記錄日志。
攔截器的實(shí)現(xiàn)可以分為以下兩個(gè)步驟:
創(chuàng)建自定義攔截器,實(shí)現(xiàn) HandlerInterceptor 接口的 preHandle(執(zhí)行具體方法之前的預(yù)處理)方法。
將自定義攔截器加入 WebMvcConfigurer 的 addInterceptors 方法中,并且設(shè)置攔截規(guī)則。
具體實(shí)現(xiàn)如下:
step1. 創(chuàng)建自定義攔截器,自定義攔截器是一個(gè)普通類(lèi),代碼如下:
importorg.springframework.web.servlet.HandlerInterceptor; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; importjavax.servlet.http.HttpSession; /** *@author興趣使然黃小黃 *@version1.0 *@date2023/7/1916:31 *統(tǒng)一用戶登錄權(quán)限驗(yàn)證——登錄攔截器 */ publicclassLoginInterceptorimplementsHandlerInterceptor{ @Override publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{ //用戶登錄業(yè)務(wù)判斷 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ returntrue;//驗(yàn)證成功,繼續(xù)controller的流程 } //可以跳轉(zhuǎn)登錄界面或者返回401/403沒(méi)有權(quán)限碼 response.sendRedirect("/login.html");//跳轉(zhuǎn)到登錄頁(yè)面 returnfalse;//驗(yàn)證失敗 } }
step2. 配置攔截器并設(shè)置攔截規(guī)則,代碼如下:
importorg.springframework.context.annotation.Configuration; importorg.springframework.web.servlet.config.annotation.InterceptorRegistry; importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** *@author興趣使然黃小黃 *@version1.0 *@date2023/7/1916:51 */ @Configuration publicclassAppConfigimplementsWebMvcConfigurer{ @Override publicvoidaddInterceptors(InterceptorRegistryregistry){ registry.addInterceptor(newLoginInterceptor()) .addPathPatterns("/**")//攔截所有請(qǐng)求 .excludePathPatterns("/user/login")//不攔截的url地址 .excludePathPatterns("/user/reg") .excludePathPatterns("/**/*.html");//不攔截所有頁(yè)面 } }
1.3 攔截器的實(shí)現(xiàn)原理及源碼分析
當(dāng)有了攔截器后,會(huì)在調(diào)用 Controller 之前進(jìn)行相應(yīng)的業(yè)務(wù)處理,執(zhí)行的流程如下圖所示:
「攔截器實(shí)現(xiàn)原理的源碼分析」
從上述案例實(shí)現(xiàn)結(jié)果的控制臺(tái)的日志信息可以看出,所有的 Controller 執(zhí)?都會(huì)通過(guò)?個(gè)調(diào)度器 DispatcherServlet 來(lái)實(shí)現(xiàn)。
而所有的方法都會(huì)執(zhí)行 DispatcherServlet 中的 doDispatch 調(diào)度方法,doDispatch 源碼如下:
protectedvoiddoDispatch(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{ HttpServletRequestprocessedRequest=request; HandlerExecutionChainmappedHandler=null; booleanmultipartRequestParsed=false; WebAsyncManagerasyncManager=WebAsyncUtils.getAsyncManager(request); try{ try{ ModelAndViewmv=null; ObjectdispatchException=null; try{ processedRequest=this.checkMultipart(request); multipartRequestParsed=processedRequest!=request; mappedHandler=this.getHandler(processedRequest); if(mappedHandler==null){ this.noHandlerFound(processedRequest,response); return; } HandlerAdapterha=this.getHandlerAdapter(mappedHandler.getHandler()); Stringmethod=request.getMethod(); booleanisGet=HttpMethod.GET.matches(method); if(isGet||HttpMethod.HEAD.matches(method)){ longlastModified=ha.getLastModified(request,mappedHandler.getHandler()); if((newServletWebRequest(request,response)).checkNotModified(lastModified)&&isGet){ return; } } //調(diào)用預(yù)處理 if(!mappedHandler.applyPreHandle(processedRequest,response)){ return; } //執(zhí)行Controller中的業(yè)務(wù) mv=ha.handle(processedRequest,response,mappedHandler.getHandler()); if(asyncManager.isConcurrentHandlingStarted()){ return; } this.applyDefaultViewName(processedRequest,mv); mappedHandler.applyPostHandle(processedRequest,response,mv); }catch(Exceptionvar20){ dispatchException=var20; }catch(Throwablevar21){ dispatchException=newNestedServletException("Handlerdispatchfailed",var21); } this.processDispatchResult(processedRequest,response,mappedHandler,mv,(Exception)dispatchException); }catch(Exceptionvar22){ this.triggerAfterCompletion(processedRequest,response,mappedHandler,var22); }catch(Throwablevar23){ this.triggerAfterCompletion(processedRequest,response,mappedHandler,newNestedServletException("Handlerprocessingfailed",var23)); } }finally{ if(asyncManager.isConcurrentHandlingStarted()){ if(mappedHandler!=null){ mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest,response); } }elseif(multipartRequestParsed){ this.cleanupMultipart(processedRequest); } } }
從上述源碼可以看出,在執(zhí)行 Controller 之前,先會(huì)調(diào)用 預(yù)處理方法 applyPreHandle,該方法源碼如下:
booleanapplyPreHandle(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{ for(inti=0;i
在上述源碼中,可以看出,在 applyPreHandle 中會(huì)獲取所有攔截器 HandlerInterceptor 并執(zhí)行攔截器中的 preHandle 方法,這與之前我們實(shí)現(xiàn)攔截器的步驟對(duì)應(yīng),如下圖所示:
此時(shí),相應(yīng)的preHandle中的業(yè)務(wù)邏輯就會(huì)執(zhí)行。
1.4 統(tǒng)一訪問(wèn)前綴添加
統(tǒng)一訪問(wèn)前綴的添加與登錄攔截器實(shí)現(xiàn)類(lèi)似,即給所有請(qǐng)求地址添加 /hxh 前綴,示例代碼如下:
@Configuration publicclassAppConfigimplementsWebMvcConfigurer{ //給所有接口添加/hxh前綴 @Override publicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){ configurer.addPathPrefix("/hxh",c->true); } }
另一種方式是在application配置文件中配置:
server.servlet.context-path=/hxh
2 統(tǒng)一異常處理
統(tǒng)一異常處理是指 在應(yīng)用程序中定義一個(gè)公共的異常處理機(jī)制,用來(lái)處理所有的異常情況。 這樣可以避免在應(yīng)用程序中分散地處理異常,降低代碼的復(fù)雜度和重復(fù)度,提高代碼的可維護(hù)性和可擴(kuò)展性。
需要考慮以下幾點(diǎn):
異常處理的層次結(jié)構(gòu):定義異常處理的層次結(jié)構(gòu),確定哪些異常需要統(tǒng)一處理,哪些異常需要交給上層處理。
異常處理的方式:確定如何處理異常,比如打印日志、返回錯(cuò)誤碼等。
異常處理的細(xì)節(jié):處理異常時(shí)需要注意的一些細(xì)節(jié),比如是否需要事務(wù)回滾、是否需要釋放資源等
本文講述的統(tǒng)一異常處理使用的是 @ControllerAdvice + @ExceptionHandler 來(lái)實(shí)現(xiàn)的:
@ControllerAdvice 表示控制器通知類(lèi)。
@ExceptionHandler 異常處理器。
以上兩個(gè)注解組合使用,表示當(dāng)出現(xiàn)異常的時(shí)候執(zhí)行某個(gè)通知,即執(zhí)行某個(gè)方法事件,具體實(shí)現(xiàn)代碼如下:
importorg.springframework.web.bind.annotation.ControllerAdvice; importorg.springframework.web.bind.annotation.ExceptionHandler; importorg.springframework.web.bind.annotation.ResponseBody; importjava.util.HashMap; /** *@author興趣使然黃小黃 *@version1.0 *@date2023/7/1918:27 *統(tǒng)一異常處理 */ @ControllerAdvice//聲明是一個(gè)異常處理器 publicclassMyExHandler{ //攔截所有的空指針異常,進(jìn)行統(tǒng)一的數(shù)據(jù)返回 @ExceptionHandler(NullPointerException.class)//統(tǒng)一處理空指針異常 @ResponseBody//返回?cái)?shù)據(jù) publicHashMapnullException(NullPointerExceptione){ HashMap result=newHashMap<>(); result.put("code","-1");//與前端定義好的異常狀態(tài)碼 result.put("msg","空指針異常:"+e.getMessage());//錯(cuò)誤碼的描述信息 result.put("data",null);//返回的數(shù)據(jù) returnresult; } }
上述代碼中,實(shí)現(xiàn)了對(duì)所有空指針異常的攔截并進(jìn)行統(tǒng)一的數(shù)據(jù)返回。
在實(shí)際中,常常設(shè)置一個(gè)保底,比如發(fā)生的非空指針異常,也會(huì)有保底措施進(jìn)行處理,類(lèi)似于 try-catch 塊中使用 Exception 進(jìn)行捕獲,代碼示例如下:
@ExceptionHandler(Exception.class) @ResponseBody publicHashMapexception(Exceptione){ HashMap result=newHashMap<>(); result.put("code","-1");//與前端定義好的異常狀態(tài)碼 result.put("msg","異常:"+e.getMessage());//錯(cuò)誤碼的描述信息 result.put("data",null);//返回的數(shù)據(jù) returnresult; }
3 統(tǒng)一數(shù)據(jù)返回格式
為了保持 API 的一致性和易用性,通常需要使用統(tǒng)一的數(shù)據(jù)返回格式。 一般而言,一個(gè)標(biāo)準(zhǔn)的數(shù)據(jù)返回格式應(yīng)該包括以下幾個(gè)元素:
狀態(tài)碼:用于標(biāo)志請(qǐng)求成功失敗的狀態(tài)信息;
消息:用來(lái)描述請(qǐng)求狀態(tài)的具體信息;
數(shù)據(jù):包含請(qǐng)求的數(shù)據(jù)信息;
時(shí)間戳:可以記錄請(qǐng)求的時(shí)間信息,便于調(diào)試和監(jiān)控。
實(shí)現(xiàn)統(tǒng)一的數(shù)據(jù)返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式實(shí)現(xiàn),具體步驟如下:
創(chuàng)建一個(gè)類(lèi),并添加 @ControllerAdvice 注解;
實(shí)現(xiàn) ResponseBodyAdvice 接口,并重寫(xiě) supports 和 beforeBodyWrite 方法。
示例代碼如下:
importorg.springframework.core.MethodParameter; importorg.springframework.http.MediaType; importorg.springframework.http.server.ServerHttpRequest; importorg.springframework.http.server.ServerHttpResponse; importorg.springframework.web.bind.annotation.ControllerAdvice; importorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; importjava.util.HashMap; /** *@author興趣使然黃小黃 *@version1.0 *@date2023/7/1918:59 *統(tǒng)一數(shù)據(jù)返回格式 */ @ControllerAdvice publicclassResponseAdviceimplementsResponseBodyAdvice{ /** *此方法返回true則執(zhí)行下面的beforeBodyWrite方法,反之則不執(zhí)行 */ @Override publicbooleansupports(MethodParameterreturnType,ClassconverterType){ returntrue; } /** *方法返回之前調(diào)用此方法 */ @Override publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){ HashMapresult=newHashMap<>(); result.put("code",200); result.put("msg",""); result.put("data",body); returnnull; } }
但是,如果返回的 body 原始數(shù)據(jù)類(lèi)型是 String ,則會(huì)出現(xiàn)類(lèi)型轉(zhuǎn)化異常,即 ClassCastException。
因此,如果原始返回?cái)?shù)據(jù)類(lèi)型為 String ,則需要使用 jackson 進(jìn)行單獨(dú)處理,實(shí)現(xiàn)代碼如下:
importcom.fasterxml.jackson.core.JsonProcessingException; importcom.fasterxml.jackson.databind.ObjectMapper; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.core.MethodParameter; importorg.springframework.http.MediaType; importorg.springframework.http.server.ServerHttpRequest; importorg.springframework.http.server.ServerHttpResponse; importorg.springframework.web.bind.annotation.ControllerAdvice; importorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; importjava.util.HashMap; /** *@author興趣使然黃小黃 *@version1.0 *@date2023/7/1918:59 *統(tǒng)一數(shù)據(jù)返回格式 */ @ControllerAdvice publicclassResponseAdviceimplementsResponseBodyAdvice{ @Autowired privateObjectMapperobjectMapper; /** *此方法返回true則執(zhí)行下面的beforeBodyWrite方法,反之則不執(zhí)行 */ @Override publicbooleansupports(MethodParameterreturnType,ClassconverterType){ returntrue; } /** *方法返回之前調(diào)用此方法 */ @Override publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){ HashMapresult=newHashMap<>(); result.put("code",200); result.put("msg",""); result.put("data",body); if(bodyinstanceofString){ //需要對(duì)String特殊處理 try{ returnobjectMapper.writeValueAsString(result); }catch(JsonProcessingExceptione){ e.printStackTrace(); } } returnresult; } }
但是,在實(shí)際業(yè)務(wù)中,上述代碼只是作為保底使用,因?yàn)闋顟B(tài)碼始終返回的是200,過(guò)于死板,還需要具體問(wèn)題具體分析。
審核編輯:劉清
-
處理器
+關(guān)注
關(guān)注
68文章
19265瀏覽量
229686 -
控制器
+關(guān)注
關(guān)注
112文章
16339瀏覽量
177860 -
狀態(tài)機(jī)
+關(guān)注
關(guān)注
2文章
492瀏覽量
27533 -
SpringBoot
+關(guān)注
關(guān)注
0文章
173瀏覽量
177
原文標(biāo)題:告別繁瑣:SpringBoot 攔截器與統(tǒng)一功能處理
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論