版本说明
- Spring Boot 版本为 2.0.9.RELEASE
- Spring Framework 对应版本为 5.0.13.RELEASE
不同版本间可能存在差异,需要看清楚所使用的版本
问题描述
问题起源于在公司内部,我们创建了自己的自定义 web 模块的 stater,在其中有统一的异常处理,普通的异常处理我们称之为 ExceptionResolver,还有一种我们称之为 FeignExceptionResolver,专门为了处理 FeignException,为啥会单独写一个处理 FeignException 呢,主要是考虑到可能有的模块会没有引入 Feign 的包,从而造成启动报错。而这个在近期又出现了另一个问题,就是当别的一些同学乱来,在自己的应用上乱指定 scanBasePackages 导致这两个 Resolver 的加载顺序被打乱了,从而导致 FeignException 被普通异常中的搂底操作所处理,造成提示错误。当环境中有多个 @ControllerAdvice 或者 @RestControllerAdvice 注解标注的类,它们是有优先级顺序的,排在前面的先执行。
问题处理
描述清楚了问题,那就想着怎么去处理它,既然说是加载顺序造成的错误,那么我们应该想到那就需要去调整其加载顺序,而此时自然而然会想到 @Order,由于我们使用的自定义 starter,所以这些类均使用 @Bean 在 spring.factories 文件指向的类中去完成被 Spring 的管理,所以我们做了以下尝试:
- 在 @Bean 处使用 @Order,结果发现无效
- 让标注 @ControllerAdvice 或者 @RestControllerAdvice 的类实现 Ordered 接口,无效
- 在标注 @ControllerAdvice 或者 @RestControllerAdvice 的类上标注 @Order,成功解决问题
问题分析
按理说,一般情况下 @Order 和实现 Ordered 接口的效果应该是一样的,那么这里究竟是哪里出了问题呢,我们从源码入手。
一脚入坑
首先我们通过查找 @ControllerAdvice 在哪些地方被用到,很容易观察到其应该被 ExceptionHandlerExceptionResolver 所处理,再观察其变量,我们看到有这样一段代码
1 | private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<>(); |
这个数据结构也表明了,多个同时存在时应该是有优先级顺序的。
接下来我们查找这个变量被设置值的方法:
1 | private void initExceptionHandlerAdviceCache() { |
我们看到 Map 被设置值的地方为 1 处,而它的参数来源于 2 处,在 2 处查找了所有标注 @ControllerAdvice 或者 @RestControllerAdvice 的类为 ControllerAdviceBean 的 List 集合,后在 3 对其进行排序。
在找到这部分的源码,emmmm 一脚就踩坑里了,下意识以为直接看 3 中的实现,即可找到原因,好了,就默默追踪 3 吧,其调用方法为 AnnotationAwareOrderComparator.sort(adviceBeans);
1 | public static void sort(List<?> list) { |
关键在于 INSTANCE 即 public static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator(); 那么具体其实还得看父类中 compare 方法的实现,这里既然能丢给 list.sort 方法,那么必然实现了 Comparator 接口中的 compare 方法,追踪发现这里又调用了父类 OrderComparator 中的 doCompare 方法
1 | private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderSourceProvider sourceProvider) { |
通过这里的代码我们可以看到其实现应该在 getOrder 方法中,那么再次往下看
1 | private int getOrder(@Nullable Object obj, @Nullable OrderSourceProvider sourceProvider) { |
这里可以看到,真正的实现又在 findOrder 方法中实现,不得不说,Spring 的源码永远是如此绕,啊啊啊啊啊,蛋疼,继续往下点吧这里会先调用子类 AnnotationAwareOrderComparator 的 findOrder 方法
1 | protected Integer findOrder(Object obj) { |
这里就会发现它第一句注释,其实就是检查了是否实现 Ordered 接口,我们点过去瞅瞅
1 | protected Integer findOrder(Object obj) { |
看到这里,心里就开始心生疑惑,咦,这不就是如果这个类实现了 Ordered 接口重写了 getOrder 方法,这里就应该会被调用,完成排序啊,那为啥不生效,当时心里是懵逼的,这咋回事!!!
重头来过
在懵逼了一会之后,想想还是得回到 ExceptionHandlerExceptionResolver 的 initExceptionHandlerAdviceCache 方法重新开始。
1 | private void initExceptionHandlerAdviceCache() { |
再回到这里,对着这两行代码发了会呆,我终于治好了我的瞎,他排序的是 ControllerAdviceBean 那么它其中是不是做了一些处理,这时候我们点开 ControllerAdviceBean 类康康
1 | public class ControllerAdviceBean implements Ordered { |
去掉无用内容我们看到了这些,它实现了 Ordered 接口,且有个 order 字段,而重写的 getOrder 方法返回为当前这个 order 字段的值,而上面踩坑部分,也不能说一定用没有,至少连贯起来能知道最终排序其实就是排序多个 @ControllerAdvice 或者 @RestControllerAdvice 标注的类生成的 ControllerAdviceBean 中 order 字段的值,而它的来源又是什么呢,就是上面 initOrderFromBeanType 方法中 *OrderUtils.getOrder(beanType)*,好了,感觉快见到真相了。
柳暗花明
打开 OrderUtils#getOrder 方法
1 | public static Integer getOrder(Class<?> type) { |
emmmm 这里就发现极其简单了,就找了这个标注 @ControllerAdvice 或者 @RestControllerAdvice 上有没有标注 @Order 注解,或者是 @Priority 注解,并没有检查有没有实现 Ordered 接口,故而无效,单单只检查了类上有无这俩注解。看到这里也总算是搞明白了这个 Bug。
结语
- 修复 Bug 的同时如果有时间还是得明白其中发生了些啥
- 如果有规范就按规范来吧,不要瞎胡来,此 Bug 就是由于同学瞎胡来,导致我们得改通用的 starter 来帮他们修复问题