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 默认值与自定义
在 DispatcherServlet 的 initStrategies 源码中有一个方法是初始化 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 在启动时,DispatcherServlet 的 initStrategies 方法不执行?
在 SpringBoot load-on-startup 默认值是-1,项目启动时,默认不会初始化 DispatcherServlet,也就是不会调用 Servlet 接口的 init() 方法
- 如果需要在启动时初始化,可以通过在 application.properties 配置文件中设置如下配置项,指定启动时初始化:
# 设定 ***DispatcherServlet*** 的启动时加载优先级
spring.mvc.servlet.load-on-startup=100
- 如果 load-on-startup 保持默认值,会在首次请求 url 时,触发初始化;
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 方法依次执行 ExceptionHandlerExceptionResolver,ResponseStatusExceptionHandler,DefaultHandlerExceptionResolver 的 resolveException 方法。
2.4 AbstractHandlerExceptionResolver.resolveException
ExceptionHandlerExceptionResolver,ResponseStatusExceptionHandler,DefaultHandlerExceptionResolver 拥有共同的基类 org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver。下图是继承关系图:
因此,三者都会先调用 AbstractHandlerExceptionResolver.resolveException 方法,如下图所示:
其中,shouldApplyTo 以及 doResolveException 则是重点。
2.5 ExceptionHandlerExceptionResolver
2.5.1 shouldApplyTo
ExceptionHandlerExceptionResolver 继承的父类 org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver 对 shouldApplyTo 方法进行了覆写,如下图所示:
在本文的例子中,会进入 else if (handler instanceof HandlerMethod) {}
分支执行代码。handler 变量对应的 Bean 是 TestAPIController。
super.shouldApplyTo 即 AbstractHandlerExceptionResolver.shouldApplyTo 方法:
事实上,默认情况下,mappedHandlers 和 mappedHandlerClasses 都是 null。则 shouldApplyTo 默认情况下返回 true。
2.5.2 doResolveException
ExceptionHandlerExceptionResolver 继承的父类 AbstractHandlerMethodExceptionResolver 对 doResolveException 方法也进行了覆写,如下图所示:
AbstractHandlerMethodExceptionResolver.doResolveHandlerMethodException 是一个抽象方法,实现方法还是在 ExceptionHandlerExceptionResolver 中,源码如下:
个人认为比较重要的部分是上图红框标出的。
2.5.3 getExceptionHandlerMethod
ExceptionHandlerExceptionResolver.getExceptionHandlerMethod 的主要作用是从 添加了 @ExceptionHandler 注解的方法 中,找到能够处理给定异常的方法。
- 首先在控制器的类层次结构中搜索方法;
- 如果没有找到,它将继续搜索其他Spring容器管理的带有 @ControllerAdvice (或者子类注解 @RestControllerAdvice)的 Bean 中的方法;
源码如下图所示:
成员变量 | key | put时机 |
---|---|---|
exceptionHandlerCache | 当前出现异常的 HandlerMethod 对应的控制器类 | 每次调用 getExceptionHandlerMethod 时 |
exceptionHandlerAdviceCache | 带有 @ControllerAdvice 注解的Spring Bean | ExceptionHandlerExceptionResolver.afterPropertiesSet 初始化时 |
我们得出以下结论:当控制器和带 @ControllerAdvice 注解的 Bean 同时包含处理某一异常的方法时,优先选择控制器中的方法。
了解更多 SpringMVC之从ExceptionHandlerMethodResolver源码解析与@ExceptionHandler的使用注意点
2.5.4 参数解析器和返回值处理器
ExceptionHandlerExceptionResolver 的成员变量 :
- 参数解析器 argumentResolvers,类型为 HandlerMethodArgumentResolverComposite
- 返回值处理器 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源码解析:异常解析器,统一处理处理请求中发生的异常
- 本文主要参考这篇实践的
- 上文提供了实现 HandlerInterceptor 接口的拦截器方案
- 上文提供了实现 HandlerExceptionResolver 接口的异常解析器方案
- 个人感觉还是ExceptionHandler更简洁