SpringBoot 统一功能处理

一、用户登录拦截器

1、拦截器实现步骤

步骤1:自定义拦截器

// 自定义拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 业务逻辑
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
            // 返回 true -> 拦截器验证成功,继续执行后续的方法
            return true;
        }
        // 返回 false -> 拦截器验证失败,不会执行后续的目标方法
        return false;
    }
}

代码解析:

  1. 通过 @Component 注解将 LoginInterceptor 类标记为一个 Spring 组件,使其成为 Spring 容器中的一个可被管理的 Bean。

  2. LoginInterceptor 类实现了 Spring 提供的拦截器接口 HandlerInterceptor,并覆盖了其中的 preHandle 方法。preHandle 方法在目标方法执行前被调用。

  3. 在 preHandle 方法中,首先通过 HttpServletRequest 获取当前请求的 HttpSession 对象。如果 HttpSession 不为 null,且其中存储的 AppVar.SESSION_KEY 属性不为 null,表示用户已登录。

  4. 如果验证成功,即用户已登录,返回 true,表示拦截器验证通过,可以继续执行后续的目标方法;如果验证失败,即用户未登录,返回 false,表示拦截器验证失败,不会执行后续的目标方法。

步骤2:将自定义拦截器配置到系统设置中,并设置拦截规则

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    // 在系统配置中添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/reg.html")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/editor.md/**")
                .excludePathPatterns("/img/**")
                .excludePathPatterns("/js/**");
    }
}

代码解析:

  1. 通过 @Autowired 注解将 LoginInterceptor 注入到 AppConfig 类中,该拦截器在上面已经定义好了。

  2. 在 addInterceptors 方法中,通过 registry.addInterceptor(loginInterceptor) 将 LoginInterceptor 拦截器添加到拦截器链中。

  3. 使用 addPathPatterns 方法设置需要拦截的 URL,这里使用 “/**” 表示拦截所有的请求。

  4. 使用 excludePathPatterns 方法设置不需要拦截的 URL,这些路由在拦截器中会被忽略。这里排除了一些静态资源和特定路径,比如登录页、注册页、CSS 文件、图片文件和 JavaScript
    文件。

2、拦截器实现原理

本质上 Spring 中的拦截器也是通过动态代理和环绕通知的 思想 来实现的。在拦截器中,可以通过实现 HandlerInterceptor 接口并重写 preHandlepostHandleafterCompletion 方法来实现环绕通知的功能。

SpringBoot 统一功能处理_第1张图片

通过阅读源码我们可以看到,在 Spring 中所有 Controller 的执行都会通过一个核心调度器 DispatcherServlet 来实现,所有的请求方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,doDispatch 方法中有一系列的事件处理方法,而在开始执行 Controller 中的目标方法 之前,会先调用预处理方法 applyPreHandle,在 applyPreHandle 方法中会获取所有拦截器 HandlerInterceptor 并执行拦截器中的 preHandle 方法。如果拦截器中有一个返回了 false 那么后续的流程就不会执行了。

二、统一异常处理

通过使用 @RestControllerAdvice(@ControllerAdvice+@ResponseBody) 注解和 @ExceptionHandler 注解结合使用,可以实现全局的或是针对特定异常的统一异常处理,并将处理结果以统一的数据格式返回给客户端。

@RestControllerAdvice
public class ExceptionAdvice {
    // 仅限于空指针异常的异常处理
    @ExceptionHandler(NullPointerException.class)
    public ResultAjax doNullPointException(NullPointerException e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("异常"+e.getMessage());
        return resultAjax;
    }
    
    // 适用于所有异常的异常处理
    @ExceptionHandler(Exception.class)
    public ResultAjax doException(Exception e) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(-1);
        resultAjax.setMsg("异常"+e.getMessage());
        return resultAjax;
    }
}

代码解析:

  1. @RestControllerAdvice 注解表示该类是一个全局控制器增强器,并且结合了 @ControllerAdvice 和 @ResponseBody 注解的功能。

  2. @ExceptionHandler 注解标注异常处理的方法。搭配 @RestControllerAdvice 注解可以在发生异常时统一处理异常并返回数据。

  3. doNullPointException 方法使用 @ExceptionHandler(NullPointerException.class) 注解来指定它处理的异常类型为
    NullPointerException。当发生空指针异常时,该方法会被调用。在方法体内,创建一个 ResultAjax
    对象并设置相应的错误信息,然后将其返回。

  4. doException 方法没有指定特定的异常类型,因此它将会处理所有类型的异常。当发生任何异常时,该方法会被调用。它的处理逻辑与 doNullPointException 方法类似,也是创建一个 ResultAjax 对象并设置错误信息,然后返回。

三、统一数据返回格式

统一数据的返回格式可以降低前端程序员和后端程序员的沟通成本,方便前端程序员更好的接收和解析后端数据接口返回的数据。

一般情况下,我们可以创建一个统一返回对象,提供一些成功和失败的返回接口,后续返回的数据直接调用接口即可返回约定的统一对象。具体实现如下:

// 定义统一返回对象
@Data
public class ResultAjax {
    // 状态码
    private int code;
    // 状态码的描述信息
    private String msg;
    // 返回数据
    private Object data;

    // 返回成功对象
    public static ResultAjax succ(Object data) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(200);
        resultAjax.setMsg("");
        resultAjax.setData(data);
        return resultAjax;
    }
    public static ResultAjax succ(String msg, Object data) {
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(200);
        resultAjax.setMsg(msg);
        resultAjax.setData(data);
        return resultAjax;
    }
    // 返回失败对象
    public static ResultAjax fail(int code,String msg){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(code);
        resultAjax.setMsg(msg);
        resultAjax.setData(null);
        return resultAjax;
    }

    public static ResultAjax fail(int code,String msg,Object data){
        ResultAjax resultAjax = new ResultAjax();
        resultAjax.setCode(code);
        resultAjax.setMsg(msg);
        resultAjax.setData(data);
        return resultAjax;
    }
}

之后业务中所有返回类型都设置为上述定义的统一返回对象:

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/hello")
    public ResultAjax sayHello(){
        return ResultAjax.succ("hello");
    }
    @RequestMapping("/hi")
    public ResultAjax sayHi(){
        return ResultAjax.succ("hi");
    }
}

当然虽然做出了上面的约定,但也不能保证在之后的业务代码不会误用其他返回类型,这个时候就需要使用到统一返回值的保底策略了。可以在 @ControllerAdvice 注解的类中实现 ResponseBodyAdvice 接口,对所有控制器方法的返回值进行统一的处理。

// 执行统一返回数据的保底策略
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    //     * true -> 才会调用 beforeBodyWrite 方法,
    //     * 反之则永远不会调用 beforeBodyWrite 方法
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        // 对返回值进行判断
        // 如果返回值和统一返回格式一致直接返回
        if (body instanceof ResultAjax) {
            return body;
        }
        // 对字符串返回格式进行单独判断处理
        if (body instanceof String) {
            ResultAjax resultAjax = ResultAjax.succ(body);
            try {
                return objectMapper.writeValueAsString(resultAjax);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        // 其他情况
        return ResultAjax.succ(body);
    }
}

代码解析:

  1. @ControllerAdvice 是一个注解,用于声明一个类为全局控制器增强器。在 @ControllerAdvice 注解的类中实现 ResponseBodyAdvice 接口,对所有控制器方法的返回值进行统一的处理
  2. 实现 supports 方法,判断当前返回类型是否需要进行响应体重写处理。由于这里返回 true,因此所有返回值都会被拦截并进行响应体重写处理。
  3. 实现 beforeBodyWrite 方法,对所有返回值进行统一的响应体处理。

四、@ControllerAdvice 实现原理(了解)

通过上面统一异常处理和统一数据返回格式的介绍,我们发现二者都使用到了 @ControllerAdvice 这个注解,下面我们简单介绍一下它的底层是怎么实现的:

@ControllerAdvice 它更像是一个全局的拦截器,可以对控制器的行为进行统一的处理和管理:

当我们点击 @ControllerAdvice 的源码,可以看到 @ControllerAdvice 同样派生于 @Component 组件,而所有组件初始化都会调用 InitializingBean 接口,其中 Spring MVC 中的实现的子类中有一个 afterPropertiesSet() 方法,表示所有的参数设置完成之后执行的方法,这个方法中又有一个 initControllerAdviceCache 方法,当程序执行到特定事件发生的时候,比如返回数据前或发生异常时,Spring会根据规则查找所有使用了@ControllerAdvice 注解的类,并调用其中对应的 Advice 方法来执行相应的业务逻辑。

你可能感兴趣的:(SSM,框架,spring,boot,后端,java)