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更简洁
posted @ 2022-03-17 15:05  极客子羽  阅读(781)  评论(0编辑  收藏  举报