场景还原

@ControllerAdvice在spring中用于全局的异常拦截和处理。Filter则过滤请求,多用于请求鉴权。它们之间的关系如下:

                                        

结构

因为spring设计原因,Filter和Interceptor中抛出的异常,@ControllerAdvice则捕获不到,自然也就处理不了。

解决方案

手动注入HandlerExceptionResolver对象,调用resolveException(),自己处理异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
// do something
} catch (UnauthorizedException e) {
logger.info(e.getMessage());
resolver.resolveException((HttpServletRequest) request, (HttpServletResponse) response, null, e);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}

HandlerExceptionResolver.resolveException()调用逻辑如下:

1
2
3
4
5
HandlerExceptionResolverComposite.resolveException()
for循环
-> ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(); // 执行异常处理方法
-> ExceptionHandlerExceptionResolver.getExceptionHandlerMethod(); //遍历所有的@ControllerAdvice对象,然后根据异常找到异常处理的方法
-> 然后调用异常处理的方法

resolver.resolveException 失效情况

这种解决方案,在一些情况下,也不能正常工作。

存储标注@ControllerAdvice对象是在ExceptionHandlerExceptionResolver.exceptionHandlerAdviceCache中的。该变量初始化逻辑如下:

1
2
3
4
ExceptionHandlerExceptionResolver.afterPropertiesSet()
-> initExceptionHandlerAdviceCache()
-> 从applicationContext中获取标注@ControllerAdvice的对象
-> put进exceptionHandlerAdviceCache

也就是说,spring在创建ExceptionHandlerExceptionResolver对象时,会调用afterPropertiesSet()对exceptionHandlerAdviceCache初始化。

而上述解决方案失效的情况,则是因为exceptionHandlerAdviceCache为空,也就是说没有把标注了@ControllerAdvice的对象放入该字段中。自然也就找不到异常处理的方法。

寻找原因

在向exceptionHandlerAdviceCache中put元素后,打印了一条日志Detected @ExceptionHandler methods in xxxxx。但是在日志中搜索“Detected @ExceptionHandler methods in ”发现并没有发现该日志输出,也就是说spring启动时确实没有初始化exceptionHandlerAdviceCache。

发现这个现象后,自然想到是不是因为WebMvcConfigurationSupport的原因。在我们项目自定义了MessageConverter,是通过集成WebMvcConfigurationSupport实现的。

为了排除问题,我们注释了继承的WebMvcConfigurationSupport的对象。日志中打印了“Detected @ExceptionHandler methods in ”。然后resolver.resolveException()正常运行。

问题肯定跟WebMvcConfigurationSupport有关,在WebMvcConfigurationSupport中搜索ExceptionHandler,找到问题的根源:addDefaultHandlerExceptionResolvers()。该方法逻辑如下:

  1. new 一个ExceptionHandlerExceptionResolver对象
  2. 手动调用afterPropertiesSet()
  3. 将对象保存,用于后续异常处理使用

该对象是有WebMvcConfigurationSupport手动new出来的,不由spring管理,所以需要手动调用afterPropertiesSet()。而在afterPropertiesSet()中初始化exceptionHandlerAdviceCache的时候,会先从applicationContext中获取标注@ControllerAdvice的对象。但是该对象不由spring管理,applicationContext自然也为null,更加获取不到标注@ControllerAdvice的对象。所以最后exceptionHandlerAdviceCache为空。

解决方案

使用@EnableWebMvc注解。提供三种使用方式:

  1. 直接加该注解,相当于WebMvcConfigurationSupport的配置
  2. 如果要自定义配置,使用@EnableWebMvc + WebMvcConfigurer 实现
  3. 删除@EnableWebMvc注解,使用WebMvcConfigurationSupport or DelegatingWebMvcConfiguration。这两个类都会存在上面说的坑。