前言
Spring AOP是一个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。
但使用原生Spring AOP实现统一的拦截是非常繁琐、困难的。而在本节,我们将使用一种简单的方式进行统一功能处理,这也是AOP的一次实战,具体如下:
统一用户登录权限验证
统一数据格式返回
统一异常处理
0 为什么需要统一功能处理?
统一功能处理是为了提高代码的可维护性、可重用性和可扩展性而进行的一种设计思想。在应用程序中,可能存在一些通用的功能需求,例如身份验证、日志记录、异常处理等。
这些功能需要在多个地方进行调用和处理,如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理。这样做有以下几个好处:
「代码复用」 :将通用功能抽取成独立的模块或组件,可以在多个地方共享使用,减少重复编写代码的工作量。
「可维护性」 :将通用功能集中处理,可以方便地对其进行修改、优化或扩展,而不需要在多个地方进行修改。
「代码整洁性」 :通过统一功能处理,可以使代码更加清晰、简洁,减少了冗余的代码。
「可扩展性」 :当需要添加新的功能时,只需要在统一功能处理的地方进行修改或扩展,而不需要在多个地方进行修改,降低了代码的耦合度。
1 统一用户登录权限验证
1.1 使用原生 Spring AOP 实现统一拦截的难点
以使用原生 Spring AOP 来实现⽤户统⼀登录验证为例,主要是使用前置通知和环绕通知实现的,具体实现如下
importorg.aspectj.lang.ProceedingJoinPoint; importorg.aspectj.lang.annotation.*; importorg.springframework.stereotype.Component; /** *@author兴趣使然黄小黄 *@version1.0 *@date2023/7/1816:37 */ @Aspect//表明此类为一个切面 @Component//随着框架的启动而启动 publicclassUserAspect{ //定义切点,这里使用Aspect表达式语法 @Pointcut("execution(*com.hxh.demo.controller.UserController.*(..))") publicvoidpointcut(){} //前置通知 @Before("pointcut()") publicvoidbeforeAdvice(){ System.out.println("执行了前置通知~"); } //环绕通知 @Around("pointcut()") publicObjectaroundAdvice(ProceedingJoinPointjoinPoint){ System.out.println("进入环绕通知~"); Objectobj=null; //执行目标方法 try{ obj=joinPoint.proceed(); }catch(Throwablee){ e.printStackTrace(); } System.out.println("退出环绕通知~"); returnobj; } }
从上述的代码示例可以看出,使用原生的 Spring AOP 实现统一拦截的难点主要有以下几个方面:
定义拦截规则非常困难。如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。
在切面类中拿到 HttpSession 比较难。
为了解决 Spring AOP 的这些问题,Spring 提供了拦截器~
1.2 使用 Spring 拦截器实现统一用户登录验证
Spring拦截器是Spring框架提供的一个功能强大的组件,用于在请求到达控制器之前或之后进行拦截和处理。拦截器可以用于实现各种功能,如身份验证、日志记录、性能监测等。
要使用Spring拦截器,需要创建一个实现了HandlerInterceptor接口的拦截器类。该接口定义了三个方法:preHandle、postHandle和afterCompletion。
preHandle方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;
postHandle方法在控制器处理完请求后执行,可以对模型和视图进行操作;
afterCompletion方法在视图渲染完成后执行,用于清理资源或记录日志。
拦截器的实现可以分为以下两个步骤:
创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中,并且设置拦截规则。
具体实现如下:
step1. 创建自定义拦截器,自定义拦截器是一个普通类,代码如下:
importorg.springframework.web.servlet.HandlerInterceptor; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; importjavax.servlet.http.HttpSession; /** *@author兴趣使然黄小黄 *@version1.0 *@date2023/7/1916:31 *统一用户登录权限验证——登录拦截器 */ publicclassLoginInterceptorimplementsHandlerInterceptor{ @Override publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{ //用户登录业务判断 HttpSessionsession=request.getSession(false); if(session!=null&&session.getAttribute("userinfo")!=null){ returntrue;//验证成功,继续controller的流程 } //可以跳转登录界面或者返回401/403没有权限码 response.sendRedirect("/login.html");//跳转到登录页面 returnfalse;//验证失败 } }
step2. 配置拦截器并设置拦截规则,代码如下:
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("/**")//拦截所有请求 .excludePathPatterns("/user/login")//不拦截的url地址 .excludePathPatterns("/user/reg") .excludePathPatterns("/**/*.html");//不拦截所有页面 } }
1.3 拦截器的实现原理及源码分析
当有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:
「拦截器实现原理的源码分析」
从上述案例实现结果的控制台的日志信息可以看出,所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现。
而所有的方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,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; } } //调用预处理 if(!mappedHandler.applyPreHandle(processedRequest,response)){ return; } //执行Controller中的业务 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); } } }
从上述源码可以看出,在执行 Controller 之前,先会调用 预处理方法 applyPreHandle,该方法源码如下:
booleanapplyPreHandle(HttpServletRequestrequest,HttpServletResponseresponse)throwsException{ for(inti=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 并执行拦截器中的 preHandle 方法,这与之前我们实现拦截器的步骤对应,如下图所示:
此时,相应的preHandle中的业务逻辑就会执行。
1.4 统一访问前缀添加
统一访问前缀的添加与登录拦截器实现类似,即给所有请求地址添加 /hxh 前缀,示例代码如下:
@Configuration publicclassAppConfigimplementsWebMvcConfigurer{ //给所有接口添加/hxh前缀 @Override publicvoidconfigurePathMatch(PathMatchConfigurerconfigurer){ configurer.addPathPrefix("/hxh",c->true); } }
另一种方式是在application配置文件中配置:
server.servlet.context-path=/hxh
2 统一异常处理
统一异常处理是指 在应用程序中定义一个公共的异常处理机制,用来处理所有的异常情况。 这样可以避免在应用程序中分散地处理异常,降低代码的复杂度和重复度,提高代码的可维护性和可扩展性。
需要考虑以下几点:
异常处理的层次结构:定义异常处理的层次结构,确定哪些异常需要统一处理,哪些异常需要交给上层处理。
异常处理的方式:确定如何处理异常,比如打印日志、返回错误码等。
异常处理的细节:处理异常时需要注意的一些细节,比如是否需要事务回滚、是否需要释放资源等
本文讲述的统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的:
@ControllerAdvice 表示控制器通知类。
@ExceptionHandler 异常处理器。
以上两个注解组合使用,表示当出现异常的时候执行某个通知,即执行某个方法事件,具体实现代码如下:
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 *统一异常处理 */ @ControllerAdvice//声明是一个异常处理器 publicclassMyExHandler{ //拦截所有的空指针异常,进行统一的数据返回 @ExceptionHandler(NullPointerException.class)//统一处理空指针异常 @ResponseBody//返回数据 publicHashMapnullException(NullPointerExceptione){ HashMap result=newHashMap<>(); result.put("code","-1");//与前端定义好的异常状态码 result.put("msg","空指针异常:"+e.getMessage());//错误码的描述信息 result.put("data",null);//返回的数据 returnresult; } }
上述代码中,实现了对所有空指针异常的拦截并进行统一的数据返回。
在实际中,常常设置一个保底,比如发生的非空指针异常,也会有保底措施进行处理,类似于 try-catch 块中使用 Exception 进行捕获,代码示例如下:
@ExceptionHandler(Exception.class) @ResponseBody publicHashMapexception(Exceptione){ HashMap result=newHashMap<>(); result.put("code","-1");//与前端定义好的异常状态码 result.put("msg","异常:"+e.getMessage());//错误码的描述信息 result.put("data",null);//返回的数据 returnresult; }
3 统一数据返回格式
为了保持 API 的一致性和易用性,通常需要使用统一的数据返回格式。 一般而言,一个标准的数据返回格式应该包括以下几个元素:
状态码:用于标志请求成功失败的状态信息;
消息:用来描述请求状态的具体信息;
数据:包含请求的数据信息;
时间戳:可以记录请求的时间信息,便于调试和监控。
实现统一的数据返回格式可以使用 @ControllerAdvice + ResponseBodyAdvice 的方式实现,具体步骤如下:
创建一个类,并添加 @ControllerAdvice 注解;
实现 ResponseBodyAdvice 接口,并重写 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 *统一数据返回格式 */ @ControllerAdvice publicclassResponseAdviceimplementsResponseBodyAdvice{ /** *此方法返回true则执行下面的beforeBodyWrite方法,反之则不执行 */ @Override publicbooleansupports(MethodParameterreturnType,ClassconverterType){ returntrue; } /** *方法返回之前调用此方法 */ @Override publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){ HashMapresult=newHashMap<>(); result.put("code",200); result.put("msg",""); result.put("data",body); returnnull; } }
但是,如果返回的 body 原始数据类型是 String ,则会出现类型转化异常,即 ClassCastException。
因此,如果原始返回数据类型为 String ,则需要使用 jackson 进行单独处理,实现代码如下:
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 *统一数据返回格式 */ @ControllerAdvice publicclassResponseAdviceimplementsResponseBodyAdvice{ @Autowired privateObjectMapperobjectMapper; /** *此方法返回true则执行下面的beforeBodyWrite方法,反之则不执行 */ @Override publicbooleansupports(MethodParameterreturnType,ClassconverterType){ returntrue; } /** *方法返回之前调用此方法 */ @Override publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){ HashMapresult=newHashMap<>(); result.put("code",200); result.put("msg",""); result.put("data",body); if(bodyinstanceofString){ //需要对String特殊处理 try{ returnobjectMapper.writeValueAsString(result); }catch(JsonProcessingExceptione){ e.printStackTrace(); } } returnresult; } }
但是,在实际业务中,上述代码只是作为保底使用,因为状态码始终返回的是200,过于死板,还需要具体问题具体分析。
审核编辑:刘清
-
处理器
+关注
关注
68文章
19155浏览量
229059 -
控制器
+关注
关注
112文章
16185浏览量
177342 -
状态机
+关注
关注
2文章
492浏览量
27470 -
SpringBoot
+关注
关注
0文章
173浏览量
167
原文标题:告别繁琐:SpringBoot 拦截器与统一功能处理
文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论