• SpringMVC之全局异常解析之ExceptionHandler注解与RestControllerAdvice注解


    一、使用示例

    使用 @RestControllerAdvice 注解类,使用 @ExceptionHandler(JsonParseException.class) 指明要处理的全局异常。

    import com.fasterxml.jackson.core.JsonParseException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    /**
     * 功能描述: 全局异常处理器
     *
     * @author 20024968@cnsuning.com
     * @version 1.0.0
     */
    @RestControllerAdvice
    public class GlobalExceptionAdvice {
    
        @ExceptionHandler(JsonParseException.class)
        public Response<Void> requestBodyCauseException() {
            return Response.fail(Code.REQUEST_JSON_SYNTAX_ERROR);
        }
    }
    

    用到的通用响应对象 Response.java:

    import lombok.Data;
    
    @Data
    public class Response<T> {
      private final String code;
      private final String msg;
      private final T data;
    
      protected Response(String code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
      }
      
      public static <Void> Response<Void> success() {
        return new Response<>("0", "success", null);
      }
    
      public static <T> Response<T> fail(Code code) {
        return new Response<>(code.name(), code.getDesc(), null);
      }
    }
    

    用到的错误码枚举类 Code.java:

    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @AllArgsConstructor
    @Getter
    public enum Code {
      REQUEST_JSON_SYNTAX_ERROR("请求体不符合JSON语法!");
    
      private String desc;
    }
    

    本例中,发生错误的控制器 TestAPIController.java

    import lombok.AllArgsConstructor;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/test")
    @AllArgsConstructor
    public class TestAPIController {
      public Response<Void> addTest(@RequestBody TestRequest request) {
        return Response.success();
      }
    }
    

    TestRequest 可以是没有任何字段和方法的类。

    异常也很好触发,Http请求的Body用{123}这样的字符串,是一定会触发JSON语法错误的。

    二、源码分析

    2.1 异常处理入口 DispatcherServlet

    如果要跟踪异常的问题,你需要定位到 org.springframework.web.servlet.DispatcherServlet 的以下方法处:

    往下跟踪,仍然在 DispatcherServlet 中,异常的解析交给了 handlerExceptionResolvers

    2.2 handlerExceptionResolvers 默认值与自定义

    DispatcherServletinitStrategies 源码中有一个方法是初始化 handlerExceptionResolvers 的:

    即图中,红色框出的 initHandlerExceptionResolvers

    2.2.1 用 SpringBoot 启动 jar 包

    下图是 SpringBoot 自动配置时,注入 Spring 容器的 HandlerExceptionResolver 类型的 Bean。

    其中, HandlerExceptionResolverComposite 是在 WebMvcConfigurationSupport 中注入的,如下图所示:

    addDefaultHandlerExceptionResolvers 为注入 Spring 容器的 HandlerExceptionResolverComposite 设置子异常处理器集合:

    如果你有一个实现了 WebMvcConfigurer 的配置类,并且重写了 configureHandlerExceptionResolvers 方法,那就可能使默认值失效

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.HandlerExceptionResolver;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
      @Override
      public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        resolvers.add(new DefaultHandlerExceptionResolver());
      }
    }
    

    2.2.2 用 Tomcat Server 启动 war 包

    用 war 包方式启动时,如果没有自己配置注入 HandlerExceptionResolver,会用 getDefaultStrategies 来加载异常解析器:

    getDefaultStrategies 这个方法的原理是,从配置文件 DispatcherServlet.properties(与 DispatcherServlet.class 在同一级目录下)中读取默认配置,再用反射实例化“策略”对象:

    如上图所示,红框部分是默认的异常解析器。

    *为什么 SpringBoot 在启动时,DispatcherServletinitStrategies 方法不执行?

    在 SpringBoot load-on-startup 默认值是-1,项目启动时,默认不会初始化 DispatcherServlet,也就是不会调用 Servlet 接口的 init() 方法

    • 如果需要在启动时初始化,可以通过在 application.properties 配置文件中设置如下配置项,指定启动时初始化:
    # 设定 ***DispatcherServlet*** 的启动时加载优先级
    spring.mvc.servlet.load-on-startup=100 
    
    • 如果 load-on-startup 保持默认值,会在首次请求 url 时,触发初始化;

    参考自spring boot 设置启动时初始化DispatcherServlet

    2.3 HandlerExceptionResolverComposite.resolveException

    接章节 2.1 中的 DispatcherServlet.processHandlerException 方法,遍历 handlerExceptionResolvers 列表中的 HandlerExceptionResolver,并执行它的 resolveException 方法。

    • org.springframework.boot.web.servlet.error.DefaultErrorAttributes 首先执行,代码比较简单,就不分析了;
    • 接着执行 org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException 方法;

    如上图所示,HandlerExceptionResolverComposite.resolveException 方法依次执行 ExceptionHandlerExceptionResolverResponseStatusExceptionHandlerDefaultHandlerExceptionResolverresolveException 方法。

    2.4 AbstractHandlerExceptionResolver.resolveException

    ExceptionHandlerExceptionResolverResponseStatusExceptionHandlerDefaultHandlerExceptionResolver 拥有共同的基类 org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver。下图是继承关系图:

    因此,三者都会先调用 AbstractHandlerExceptionResolver.resolveException 方法,如下图所示:

    其中,shouldApplyTo 以及 doResolveException 则是重点。

    2.5 ExceptionHandlerExceptionResolver

    2.5.1 shouldApplyTo

    ExceptionHandlerExceptionResolver 继承的父类 org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolvershouldApplyTo 方法进行了覆写,如下图所示:

    在本文的例子中,会进入 else if (handler instanceof HandlerMethod) {} 分支执行代码。handler 变量对应的 Bean 是 TestAPIController

    super.shouldApplyToAbstractHandlerExceptionResolver.shouldApplyTo 方法:

    事实上,默认情况下,mappedHandlersmappedHandlerClasses 都是 null。则 shouldApplyTo 默认情况下返回 true

    2.5.2 doResolveException

    ExceptionHandlerExceptionResolver 继承的父类 AbstractHandlerMethodExceptionResolverdoResolveException 方法也进行了覆写,如下图所示:

    AbstractHandlerMethodExceptionResolver.doResolveHandlerMethodException 是一个抽象方法,实现方法还是在 ExceptionHandlerExceptionResolver 中,源码如下:

    个人认为比较重要的部分是上图红框标出的。

    2.5.3 getExceptionHandlerMethod

    ExceptionHandlerExceptionResolver.getExceptionHandlerMethod 的主要作用是从 添加了 @ExceptionHandler 注解的方法 中,找到能够处理给定异常的方法。

    1. 首先在控制器的类层次结构中搜索方法;
    2. 如果没有找到,它将继续搜索其他Spring容器管理的带有 @ControllerAdvice (或者子类注解 @RestControllerAdvice)的 Bean 中的方法;

    源码如下图所示:

    成员变量 key put时机
    exceptionHandlerCache 当前出现异常的 HandlerMethod 对应的控制器类 每次调用 getExceptionHandlerMethod 时
    exceptionHandlerAdviceCache 带有 @ControllerAdvice 注解的Spring Bean ExceptionHandlerExceptionResolver.afterPropertiesSet 初始化时

    我们得出以下结论:当控制器和带 @ControllerAdvice 注解的 Bean 同时包含处理某一异常的方法时,优先选择控制器中的方法。

    了解更多 SpringMVC之从ExceptionHandlerMethodResolver源码解析与@ExceptionHandler的使用注意点

    2.5.4 参数解析器和返回值处理器

    ExceptionHandlerExceptionResolver 的成员变量 :

    1. 参数解析器 argumentResolvers,类型为 HandlerMethodArgumentResolverComposite
    2. 返回值处理器 returnValueHandlers,类型为 HandlerMethodReturnValueHandlerComposite

    两者都是在 afterPropertiesSet() 初始化的,源码如下:

    了解更多 SpringMVC之从ExceptionHandlerExceptionResolver源码解析之参数解析器和返回值处理器

    2.5.5 invokeAndHandle

    • exceptionHandlerMethod 的类型是 ServletInvocableHandlerMethod ,控制器的 HandlerMethod 也是用的这个类型;
    • arguments 的类型是 Throwable[],入参异常 exception 的所有上层 Cause 异常都保存在这个数组中;

    接下来的处理逻辑就和 @RequestMapping 注解的方法的处理逻辑相同了,这里就不扩展分析了~

    2.6 ResponseStatusExceptionResolver

    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver 的父类是 AbstractHandlerExceptionResolver,它的 doResolveException 比较简单:

    ResponseStatusExceptionResolver主要用来处理如下异常

    • 抛出的异常类型继承自 ResponseStatusException
    • 抛出的异常类型被 @ResponseStatus 注解标记
    import org.springframework.http.HttpStatus;
    import org.springframework.web.server.ResponseStatusException;
    
    public class TestResponseStatusException extends ResponseStatusException {
        
      public TestResponseStatusException(HttpStatus status) {
        super(status);
      }
    }
    

    或者

    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.ResponseStatus;
    
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public class UnauthorizedException extends RuntimeException {
        public UnauthorizedException(String message) {
            super(message);
        }
    }
    

    然后,在控制器代码中抛出 throw new TestResponseStatusException(HttpStatus.BAD_REQUEST); 或者 thrown new UnauthorizedException("Not Allowed");

    参考文档

    Spring MVC源码解析:异常解析器,统一处理处理请求中发生的异常

    • 本文主要参考这篇实践的

    SpringMVC之异常解析器、拦截器、上传解析器的使用

    • 上文提供了实现 HandlerInterceptor 接口的拦截器方案
    • 上文提供了实现 HandlerExceptionResolver 接口的异常解析器方案
    • 个人感觉还是ExceptionHandler更简洁
  • 相关阅读:
    JAVA多线程编程设计模式:总结
    JAVA多线程编程设计模式:Half-sync/Half-async(半同步/半异步)模式
    JAVA多线程编程设计模式:Pipeline(流水线)模式
    尚学堂Spring视频教程(七):AOP XML
    尚学堂Spring视频教程(六):AOP Annotation
    JAVA基础知识:网络
    JAVA基础知识:IO
    JAVA基础知识:容器
    Java基础知识:代理
    尚学堂Spring视频教程(五):Spring AOP
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/16009629.html
Copyright © 2020-2023  润新知