在Spring Boot中应用运行时可能会引起各种各样的异常,如果直接让这些异常返回给前端,就会影响用户体验。因此我们可以自定义 全局异常处理 来提前捕获异常,并对返回结果进行处理,让前端收到更加友好的提示。
Spring Boot自定义全局异常处理
首先是一个最简单的ControllerAdvice实例,捕获Exception异常类并对结果进行处理。
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception exception) {
return "程序运行发生异常";
}
}
@RestControllerAdvice
class ExceptionControllerAdvice {
@ExceptionHandler(Exception::class)
fun exceptionHandler(exception: Exception): String {
return "程序运行发生异常"
}
}
在上面两段代码中,我们首先定义了异常处理的类,并添加了 RestControllerAdvice
注解,表明这是一个用于异常处理的类。然后在里面定义了方法 exceptionHandler
,并添加了 ExceptionHandler
注解,表明这个方法处理 Exception
类型的异常;并在方法的参数中捕获这个异常,并对其进行处理,将结果作为响应返回。
RestControllerAdvice
的定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
// 属性定义省略
}
可以看到, RestControllerAdvice
同时继承了 ControllerAdvice
注解和 ResponseBody
注解,因此在使用时不必额外添加 ResponseBody
注解。
ExceptionHandler
的定义如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective(ExceptionHandlerReflectiveProcessor.class)
public @interface ExceptionHandler {
Class<? extends Throwable>[] value() default {};
}
它的 value
属性的类型是一个数组,意味着我们可以让一个方法处理多个异常类。
结论:对于自定义Spring Boot的全局异常处理,有以下步骤:
- 定义异常处理的类,并在类上添加注解
RestControllerAdvice
。 - 定义异常处理的方法,在上面添加注解
ExceptionHandler
并在注解中标注想要捕获的异常类。 - 在异常处理方法的参数中捕获异常,对异常进行处理,将处理后的结果返回给前端。
多个ControllerAdvice的执行顺序
考虑以下场景,有一个多模块应用,其中Common模块定义了一个全局异常处理类,里面有一个方法,捕获 Exception
类的异常。另一个Authentication模块又定义了一个全局异常处理类,立面有一个方法,捕获 UsernameNotFoundException
类型的异常。想当然地思考,这两个异常肯定会被分别处理,但是实际情况确是所有的异常处理都走了Common模块的异常处理类,按照 Exception
异常处理逻辑执行,而没有按照 UsernameNotFoundException
异常的处理逻辑执行。这其实就是ControllerAdvice和ExceptionHandler的执行顺序问题。为了解决这个问题,我们可以在异常处理类上面添加 Order
注解,将Authentication模块的异常处理类设置高优先级,代码示例如下:
// UsernameNotFound的异常处理
// 两条注解二选一即可
@Order(Ordered.HIGHEST_PRECEDENCE)
@Priority(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class AuthenticationControllerAdvice {
@ExceptionHandler(UsernameNotFoundException.class)
public String exceptionHandler(UsernameNotFoundException exception) {
return "Username Not Found";
}
}
// Exception的异常处理
@RestControllerAdvice
public class CommonControllerAdvice {
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception exception) {
return "Exception";
}
}
// 两条注解二选一即可
@Order(Ordered.HIGHEST_PRECEDENCE)
@Priority(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
class AuthenticationControllerAdvice {
@ExceptionHandler(UsernameNotFoundException::class)
fun exceptionHandler(exception: UsernameNotFoundException): String {
return "Username Not Found";
}
}
// Exception的异常处理
@RestControllerAdvice
class CommonControllerAdvice {
@ExceptionHandler(Exception::class)
fun exceptionHandler(exception: Exception): String {
return "Exception";
}
}
分析思路
点击 ControllerAdvice
,查看它的使用情况,发现它实际上被一个 ControllerAdviceBean
封装了起来,继续下去,可以看到被保存在了 ExceptionHandlerExceptionResolver
之中,保存的变量定义如下:
public class ExceptionHandlerExceptionResolver {
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>();
}
这里需要记住, LinkedHashMap
是有序集合,它的Key是ControllerAdviceBean,Value是ExceptionHandlerResolver,保存了这个ControllerAdviceBean所能够处理的所有异常类型和对应的方法。其初始化过程如下:
public class ExceptionHandlerExceptionResolver {
private void initExceptionHandlerAdviceCache() {
// 省略无关代码
// 加载所有标注ControllerAdvice的注解
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
// 遍历列表
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
// 获取其中的异常处理方法,如果里面定义了方法,就添加到缓存中
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
}
}
}
查看 ControllerAdviceBean.findAnnotatedBeans
方法,其定义如下:
public class ControllerAdviceBean implements Ordered {
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
// 获取BeanFactory过程省略
List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class)) {
// 省略部分代码,这个ControllerAdviceBean的构造方法是关键
adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));
}
}
}
OrderComparator.sort(adviceBeans) // 排序过程在这里
return adviceBeans;
}
}
在添加完 adviceBeans
之后对其进行了排序。继续追踪,发现它调用了 OrderComparator.sort
方法,定义如下:
public class OrderComparator implements Comparator<Object> {
public static void sort(List<?> list) {
if (list.size() > 1) {
list.sort(INSTANCE); // INSTANCE就是OrderComparator的单例实例
}
}
// 在Java中,list.sort会调用Comparator的compare方法
@Override
public int compare(@Nullable Object o1, @Nullable Object o2) {
return doCompare(o1, o2, null);
}
// doCompare方法的定义,省略无关内容
private int doCompare(Object o1, Object o2, OrderSourceProvider sourceProvider) {
int i1 = getOrder(o1, sourceProvider);
int i2 = getOrder(o2, sourceProvider);
return Integer.compare(i1, i2);
}
// getOrder方法,由于传入的参数是null,因此我们省略无关内容
private int getOrder(Object obj, OrderSourceProvider sourceProvider) {
Integer order = null;
// 这里省略if判断
order = findOrder(obj);
return (order != null ? order : getOrder(obj));
}
// getOrder的另一个重载
protected int getOrder(Object obj) {
if (obj != null) {
Integer order = findOrder(obj);
if (order != null) {
return order;
}
}
return Ordered.LOWEST_PRECEDENCE;
}
// 我们可以看到两个getOrder方法最终都调用了findOrder方法
protected Integer findOrder(Object obj) {
return (obj instanceof Ordered ordered ? ordered.getOrder() : null);
}
如果这个Bean是 Ordered
类型的,那么就调用其中的 getOrder
方法获取它的顺序,这样我们就要回到 ControllerAdviceBean
中,这个方法太过复杂,不过幸好作者给我们留下了足够的文档(使用IntelliJ Idea的Download Source功能即可实现)。
在Spring Framework 5.3之后,Bean的加载顺序是通过以下算法(即getOrder方法)获取并缓存起来。但是 ControllerAdvice
被配置为一个具有作用域的Bean(例如每次请求都构造一个新的Bean),它的顺序不会被立即解析。因此使用 Ordered
接口将不会生效。因此就会通过注解来获取顺序,主要是 Order
和 Priority
这两个注解。注意: Primary
注解不会改变同一个Bean类型的集合中各个Bean的顺序,因此使用 Primary
注解也不会生效。
对于 Order
和 Primary
注解,Spring Boot会使用 OrderUtils.getOrder
方法。如果没有定义顺序,则 getOrder
方法将返回最低的优先级。至此,ControllerAdvice的顺序来源已经明确。在这之后,我们需要分析其调用过程。
ExceptionHandlerExceptionResolver.getExceptionHandlerMethod
方法负责根据异常返回对应的ExceptionHandler,第一部分是Controller内部定义的ExceptionHandler,因此我们跳过。直接查看后面的遍历过程:
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
// 这里是关键
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
这里一路追踪,最后找到一个名为 resolveMethodByExceptionType
的方法中。这个方法首先从缓存中查找,如果在缓存中没找到,就在之前初始化ControllerAdviceBean中定义的ExceptionHandlerMethodResolver中寻找。这里按照两个异常的深度排序,找出深度最低的一个。这里的 深度最低 指的是和对应异常的类型最为接近,即它的本身会比它的父类更为接近。找到方法之后将其封装好并返回。至此,ControllerAdvice执行顺序问题得到了确定。