1. 背景

在一个工程中,基础框架已经定义了一个全局的异常拦截器了,然后在自己的工程中又定义了一个拦截器,
省略了部分之后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@RestControllerAdvice
public class GlobalExceptionAdvice {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionAdvice.class);

/**
* 业务异常处理,定义用户可见的异常信息,暂定返回code为1000
* @param exception
* @return
*/
@ExceptionHandler(BaseException.class)
public BaseResponse<Void> baseException(BaseException exception){
log.error("业务异常"+exception.getMessage(),exception);
return BaseResponse.failed(Constants.BUSINESS_FAIL_CODE , exception.getMessage());
}
@ExceptionHandler(Throwable.class)
public BaseResponse<Void> authorization(Throwable throwable){
log.error("未知异常:"+throwable.getMessage(),throwable);
return BaseResponse.failed("未知异常:"+throwable.getMessage());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class SecurityHandlerAdvice {

private static final Logger log = LoggerFactory.getLogger(SecurityHandlerAdvice.class);

@ExceptionHandler(InsufficientAuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public BaseResponse insufficientAuthenticationException(InsufficientAuthenticationException t){
log.error("请求的权限不足,需要提升:{}", t.getMessage(), t);
return BaseResponse.failed(HttpStatus.UNAUTHORIZED.value(), "请求的权限不足,请重新登录");
}
}

第一个@RestControllerAdvice类有一个保底的拦截@ExceptionHandler(Throwable.class),第二个是SpringSecurity中的认证异常的拦截@ExceptionHandle(InsufficientAuthenticationException.class)

工程中出现了InsufficientAuthenticationException异常,本来按照HandlerException的顺序应该是优先执行异常最接近的@ExceptionHandle(InsufficientAuthenticationException.class),但是实际却执行了@ExceptionHandler(Throwable.class)

2. 解决

在有多个 @ControllerAdvice 的情况下,需要给每个@ControllerAdvice对应的类添加顺序,来保证执行的先后顺序

如上面的两个类,添加@Order注解或者实现Ordered接口,数字越小,优先级越高

1
2
3
4
@RestControllerAdvice
@Order(10000)
public class GlobalExceptionAdvice {
}
1
2
3
4
@RestControllerAdvice
@Order(100)
public class SecurityHandlerAdvice {
}

这样添加了@Order(100)@Order(10000)之后,SecurityHandlerAdvice就会优先执行到

3. 源码分析

当发生异常时,SpringMVC会调起HandlerExceptionResolver#resolveException()来处理异常,这个接口也只有这一个方法,默认的已经加载好了ExceptionHandlerExceptionResolver这个解析器。

    1. ExceptionHandlerExceptionResolver先调用父方法doResolveHandlerMethodException()
    1. 然后再执行doResolveHandlerMethodException()
    1. 调起 getExceptionHandlerMethod(),然后内部遍历this.exceptionHandlerAdviceCache()这个LinkedHashMap来获取@ControllerAdvice对应的类
    1. 分析类中的方法使用resolver.resolveMethod(exception)会根据异常优先选取最接近的异常(及有本异常则匹配,没有则匹配父异常,一直到Throable异常)

调试的栈如下
815ef704bad8b85fc1108a5fd4bbb614

在第3步的时候就会选取一个@ControllerAdvice对应的类了,而这又是HashMap的遍历,因此需要继续寻找初始化的时候是怎么put进HashMap的。

这里分析采用倒序的方式,更容易理解

exceptionHandlerAdviceCache是初始化的时候就配置好了,因此我们要重启调试,到exceptionHandlerAdviceCache初始化的时候断点

ExceptionHandlerExceptionResolver中,只有initExceptionHandlerAdviceCache()方法会设置this.exceptionHandlerAdviceCache
断点定位到put的地方

a41347f651c926a552e5deb486b52025

可以看出来1处就是从Application中拿到ControllerAdviceBean,而ControllerAdviceBean就是封装了ControllerAdvice类的

进入ControllerAdviceBean.findAnnotatedBeans(getApplicationContext())方法

59504574ed571d37753f2cee9e66a03d

    1. 可以看出1的位置就是找到ControllerAdvice注解,
    1. 2的位置封装并添加到adviceBeans中
    1. 3的位置进行重排序OrderComparator.sort(adviceBeans)

OrderComparator.sort()是Spring中很重要的一个工具类,它主要用于对容器中一个接口多个实现的List进行排序,排序的规则是按照有@Order注解或者是实现Ordered接口,并且以数字越小越靠前。
Spring中实现了@OrederOrdered的都是通过这个注解排序的。

回到本文中,因此就明确了要有先后顺序必须要自己定义Bean的顺序。
其实大部分Spring中的注入Bean顺序的先后都是此思路。