0


【Spring Boot】拦截器与统一功能处理:统一登录验证、统一异常处理与统一数据返回格式

前言

 Spring AOP是一个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。但使用原生Spring AOP实现统一的拦截是非常繁琐、困难的。而在本节,我们将使用一种简单的方式进行统一功能处理,这也是AOP的一次实战,具体如下:

  • 统一用户登录权限验证
  • 统一数据格式返回
  • 统一异常处理

文章目录


0 为什么需要统一功能处理?

统一功能处理是为了提高代码的可维护性、可重用性和可扩展性而进行的一种设计思想。在应用程序中,可能存在一些通用的功能需求,例如身份验证、日志记录、异常处理等。这些功能需要在多个地方进行调用和处理,如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理。这样做有以下几个好处:

  1. 代码复用:将通用功能抽取成独立的模块或组件,可以在多个地方共享使用,减少重复编写代码的工作量。
  2. 可维护性:将通用功能集中处理,可以方便地对其进行修改、优化或扩展,而不需要在多个地方进行修改。
  3. 代码整洁性:通过统一功能处理,可以使代码更加清晰、简洁,减少了冗余的代码。
  4. 可扩展性:当需要添加新的功能时,只需要在统一功能处理的地方进行修改或扩展,而不需要在多个地方进行修改,降低了代码的耦合度。

表情


1 统一用户登录权限验证

1.1 使用原生 Spring AOP 实现统一拦截的难点

在文章:【Spring】Spring AOP入门及实现原理剖析 中,我们了解到如何使用 AOP 进行面向切面编程。以使用原生 Spring AOP 来实现⽤户统⼀登录验证为例, 主要是使用前置通知和环绕通知实现的,具体实现如下:

importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.*;importorg.springframework.stereotype.Component;/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/18 16:37
 */@Aspect// 表明此类为一个切面@Component// 随着框架的启动而启动publicclassUserAspect{// 定义切点, 这里使用 Aspect 表达式语法@Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")publicvoidpointcut(){}// 前置通知@Before("pointcut()")publicvoidbeforeAdvice(){System.out.println("执行了前置通知~");}// 环绕通知@Around("pointcut()")publicObjectaroundAdvice(ProceedingJoinPoint joinPoint){System.out.println("进入环绕通知~");Object obj =null;// 执行目标方法try{
            obj = joinPoint.proceed();}catch(Throwable e){
            e.printStackTrace();}System.out.println("退出环绕通知~");return obj;}}

从上述的代码示例可以看出,使用原生的 Spring AOP 实现统一拦截的难点主要有以下几个方面:

  1. 定义拦截规则非常困难。如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。
  2. 在切面类中拿到 HttpSession 比较难。

为了解决 Spring AOP 的这些问题,Spring 提供了拦截器~

1.2 使用 Spring 拦截器实现统一用户登录验证

Spring拦截器是Spring框架提供的一个功能强大的组件,用于在请求到达控制器之前或之后进行拦截和处理。拦截器可以用于实现各种功能,如身份验证、日志记录、性能监测等。

要使用Spring拦截器,需要创建一个实现了

HandlerInterceptor

接口的拦截器类。该接口定义了三个方法:

preHandle

postHandle

afterCompletion

preHandle

方法在请求到达控制器之前执行,可以用于进行身份验证、参数校验等;

postHandle

方法在控制器处理完请求后执行,可以对模型和视图进行操作;

afterCompletion

方法在视图渲染完成后执行,用于清理资源或记录日志。

拦截器的实现可以分为以下两个步骤:

  1. 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。
  2. 将自定义拦截器加入 WebMvcConfigureraddInterceptors 方法中,并且设置拦截规则。

具体实现如下:

step1. 创建自定义拦截器,自定义拦截器是一个普通类,代码如下:

importorg.springframework.web.servlet.HandlerInterceptor;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjavax.servlet.http.HttpSession;/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:31
 * 统一用户登录权限验证 —— 登录拦截器
 */publicclassLoginInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{// 用户登录业务判断HttpSession session = 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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:51
 */@ConfigurationpublicclassAppConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(newLoginInterceptor()).addPathPatterns("/**")// 拦截所有请求.excludePathPatterns("/user/login")// 不拦截的 url 地址.excludePathPatterns("/user/reg").excludePathPatterns("/**/*.html");// 不拦截所有页面}}

1.3 拦截器的实现原理及源码分析

当有了拦截器后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:
执行流程
拦截器实现原理的源码分析
从上述案例实现结果的控制台的日志信息可以看出,所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现。
实现结果
而所有的方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 源码如下:

protectedvoiddoDispatch(HttpServletRequest request,HttpServletResponse response)throwsException{HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler =null;boolean multipartRequestParsed =false;WebAsyncManager asyncManager =WebAsyncUtils.getAsyncManager(request);try{try{ModelAndView mv =null;Object 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((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(Exception var20){
                dispatchException = var20;}catch(Throwable var21){
                dispatchException =newNestedServletException("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,newNestedServletException("Handler processing failed", var23));}}finally{if(asyncManager.isConcurrentHandlingStarted()){if(mappedHandler !=null){
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}elseif(multipartRequestParsed){this.cleanupMultipart(processedRequest);}}}

从上述源码可以看出,在执行 Controller 之前,先会调用 预处理方法 applyPreHandle,该方法源码如下:

booleanapplyPreHandle(HttpServletRequest request,HttpServletResponse response)throwsException{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);returnfalse;}}returntrue;}

在上述源码中,可以看出,在 applyPreHandle 中会获取所有拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法,这与之前我们实现拦截器的步骤对应,如下图所示:
实现细节
此时,相应的preHandle中的业务逻辑就会执行。

1.4 统一访问前缀添加

统一访问前缀的添加与登录拦截器实现类似,即给所有请求地址添加 /hxh 前缀,示例代码如下:

@ConfigurationpublicclassAppConfigimplementsWebMvcConfigurer{// 给所有接口添加 /hxh 前缀@OverridepublicvoidconfigurePathMatch(PathMatchConfigurer configurer){
        configurer.addPathPrefix("/hxh", c ->true);}}

另一种方式是在application配置文件中配置:

server.servlet.context-path=/hxh

2 统一异常处理

统一异常处理是指 在应用程序中定义一个公共的异常处理机制,用来处理所有的异常情况。 这样可以避免在应用程序中分散地处理异常,降低代码的复杂度和重复度,提高代码的可维护性和可扩展性。

需要考虑以下几点:

  1. 异常处理的层次结构:定义异常处理的层次结构,确定哪些异常需要统一处理,哪些异常需要交给上层处理。
  2. 异常处理的方式:确定如何处理异常,比如打印日志、返回错误码等。
  3. 异常处理的细节:处理异常时需要注意的一些细节,比如是否需要事务回滚、是否需要释放资源等

本文讲述的统一异常处理使用的是

@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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:27
 * 统一异常处理
 */@ControllerAdvice// 声明是一个异常处理器publicclassMyExHandler{// 拦截所有的空指针异常, 进行统一的数据返回@ExceptionHandler(NullPointerException.class)// 统一处理空指针异常@ResponseBody// 返回数据publicHashMap<String,Object>nullException(NullPointerException e){HashMap<String,Object> result =newHashMap<>();
        result.put("code","-1");// 与前端定义好的异常状态码
        result.put("msg","空指针异常: "+ e.getMessage());// 错误码的描述信息
        result.put("data",null);// 返回的数据return result;}}

上述代码中,实现了对所有空指针异常的拦截并进行统一的数据返回。

在实际中,常常设置一个保底,比如发生的非空指针异常,也会有保底措施进行处理,类似于 try-catch 块中使用 Exception 进行捕获,代码示例如下:

@ExceptionHandler(Exception.class)// 异常处理保底措施@ResponseBody// 返回数据publicHashMap<String,Object>exception(Exception e){HashMap<String,Object> result =newHashMap<>();
    result.put("code","-1");// 与前端定义好的异常状态码
    result.put("msg","异常: "+ e.getMessage());// 错误码的描述信息
    result.put("data",null);// 返回的数据return result;}

3 统一数据返回格式

为了保持 API 的一致性和易用性,通常需要使用统一的数据返回格式。 一般而言,一个标准的数据返回格式应该包括以下几个元素:

  • 状态码:用于标志请求成功失败的状态信息;
  • 消息:用来描述请求状态的具体信息;
  • 数据:包含请求的数据信息;
  • 时间戳:可以记录请求的时间信息,便于调试和监控。

实现统一的数据返回格式可以使用

@ControllerAdvice

+

ResponseBodyAdvice

的方式实现,具体步骤如下:

  1. 创建一个类,并添加 @ControllerAdvice 注解;
  2. 实现 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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */@ControllerAdvicepublicclassResponseAdviceimplementsResponseBodyAdvice{/**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */@Overridepublicbooleansupports(MethodParameter returnType,Class converterType){returntrue;}/**
     * 方法返回之前调用此方法
     */@OverridepublicObjectbeforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class selectedConverterType,ServerHttpRequest request,ServerHttpResponse response){HashMap<String,Object> result =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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */@ControllerAdvicepublicclassResponseAdviceimplementsResponseBodyAdvice{@AutowiredprivateObjectMapper objectMapper;/**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */@Overridepublicbooleansupports(MethodParameter returnType,Class converterType){returntrue;}/**
     * 方法返回之前调用此方法
     */@OverridepublicObjectbeforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class selectedConverterType,ServerHttpRequest request,ServerHttpResponse response){HashMap<String,Object> result =newHashMap<>();
        result.put("code",200);
        result.put("msg","");
        result.put("data", body);if(body instanceofString){// 需要对 String 特殊处理try{return objectMapper.writeValueAsString(result);}catch(JsonProcessingException e){
                e.printStackTrace();}}return result;}}

但是,在实际业务中,上述代码只是作为保底使用,因为状态码始终返回的是200,过于死板,还需要具体问题具体分析。


写在最后

本文被 JavaEE编程之路 收录点击订阅专栏 , 持续更新中。
 以上便是本文的全部内容啦!创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

在这里插入图片描述

标签: spring boot java 后端

本文转载自: https://blog.csdn.net/m0_60353039/article/details/131810657
版权归原作者 兴趣使然黄小黄 所有, 如有侵权,请联系我们删除。

“【Spring Boot】拦截器与统一功能处理:统一登录验证、统一异常处理与统一数据返回格式”的评论:

还没有评论