调整多个ControllerAdvice的执行顺序

问题描述

问题起源于在公司内部,我们创建了自己的自定义web模块的stater,在其中有统一的异常处理,普通的异常处理我们称之为ExceptionResolver,还有一种我们称之为FeignExceptionResolver,专门为了处理FeignException,为啥会单独写一个处理FeignException呢,主要是考虑到可能有的模块会没有引入Feign的包,从而造成启动报错。而这个在近期又出现了另一个问题,就是当别的一些同学乱来,在自己的应用上乱指定scanBasePackages导致这两个Resolver的加载顺序被打乱了,从而导致FeignException被普通异常中的搂底操作所处理,造成提示错误。当环境中有多个@ControllerAdvice或者@RestControllerAdvice注解标注的类,它们是有优先级顺序的,排在前面的先执行

问题处理

描述清楚了问题,那就想着怎么去处理它,既然说是加载顺序造成的错误,那么我们应该想到那就需要去调整其加载顺序,而此时自然而然会想到@Order,由于我们使用的自定义starter,所以这些类均使用@Beanspring.factories文件指向的类中去完成被Spring的管理,所以我们做了以下尝试:

  • @Bean处使用@Order,结果发现无效
  • 让标注@ControllerAdvice或者@RestControllerAdvice的类实现Ordered接口,无效
  • 在标注@ControllerAdvice或者@RestControllerAdvice的类上标注@Order,成功解决问题

问题分析

按理说,一般情况下@Order和实现Ordered接口的效果应该是一样的,那么这里究竟是哪里出了问题呢,我们从源码入手。

一脚入坑

首先我们通过查找@ControllerAdvice在哪些地方被用到,很容易观察到其应该被ExceptionHandlerExceptionResolver所处理,再观察其变量,我们看到有这样一段代码

private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
            new LinkedHashMap<>();

这个数据结构也表明了,多个同时存在时应该是有优先级顺序的。

接下来我们查找这个变量被设置值的方法:

private void initExceptionHandlerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for exception mappings: " + getApplicationContext());
        }

  // 2. put 前的参数来源
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  // 3. 对多个 ControllerAdviceBean 进行排序
        AnnotationAwareOrderComparator.sort(adviceBeans);

        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
            }
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
            if (resolver.hasExceptionMappings()) {
        // 1. 变量被设置值
                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
                if (logger.isInfoEnabled()) {
                    logger.info("Detected @ExceptionHandler methods in " + adviceBean);
                }
            }
            if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                this.responseBodyAdvice.add(adviceBean);
                if (logger.isInfoEnabled()) {
                    logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
                }
            }
        }
}

我们看到Map被设置值的地方为1处,而它的参数来源于2处,在2处查找了所有标注@ControllerAdvice或者@RestControllerAdvice的类为ControllerAdviceBeanList集合,后在3对其进行排序。

在找到这部分的源码,emmmm一脚就踩坑里了,下意识以为直接看3中的实现,即可找到原因,好了,就默默追踪3吧,其调用方法为AnnotationAwareOrderComparator.sort(adviceBeans);

public static void sort(List<?> list) {
        if (list.size() > 1) {
            list.sort(INSTANCE);
        }
}

关键在于INSTANCEpublic static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator();那么具体其实还得看父类中compare方法的实现,这里既然能丢给list.sort方法,那么必然实现了Comparator接口中的compare方法,追踪发现这里又调用了父类OrderComparator中的doCompare方法

private int doCompare(@Nullable Object o1, @Nullable Object o2, @Nullable OrderSourceProvider sourceProvider) {
        boolean p1 = (o1 instanceof PriorityOrdered);
        boolean p2 = (o2 instanceof PriorityOrdered);
        if (p1 && !p2) {
            return -1;
        }
        else if (p2 && !p1) {
            return 1;
        }

        int i1 = getOrder(o1, sourceProvider);
        int i2 = getOrder(o2, sourceProvider);
        return Integer.compare(i1, i2);
}

通过这里的代码我们可以看到其实现应该在getOrder方法中,那么再次往下看

private int getOrder(@Nullable Object obj, @Nullable OrderSourceProvider sourceProvider) {
        Integer order = null;
        if (obj != null && sourceProvider != null) {
            Object orderSource = sourceProvider.getOrderSource(obj);
            if (orderSource != null) {
                if (orderSource.getClass().isArray()) {
                    Object[] sources = ObjectUtils.toObjectArray(orderSource);
                    for (Object source : sources) {
                        order = findOrder(source);
                        if (order != null) {
                            break;
                        }
                    }
                }
                else {
                    order = findOrder(orderSource);
                }
            }
        }
        return (order != null ? order : getOrder(obj));
}

这里可以看到,真正的实现又在findOrder方法中实现,不得不说,Spring的源码永远是如此绕,啊啊啊啊啊,蛋疼,继续往下点吧这里会先调用子类AnnotationAwareOrderComparatorfindOrder方法

protected Integer findOrder(Object obj) {
        // Check for regular Ordered interface
        Integer order = super.findOrder(obj);
        if (order != null) {
            return order;
        }

        // Check for @Order and @Priority on various kinds of elements
        if (obj instanceof Class) {
            return OrderUtils.getOrder((Class<?>) obj);
        }
        else if (obj instanceof Method) {
            Order ann = AnnotationUtils.findAnnotation((Method) obj, Order.class);
            if (ann != null) {
                return ann.value();
            }
        }
        else if (obj instanceof AnnotatedElement) {
            Order ann = AnnotationUtils.getAnnotation((AnnotatedElement) obj, Order.class);
            if (ann != null) {
                return ann.value();
            }
        }
        else {
            order = OrderUtils.getOrder(obj.getClass());
            if (order == null && obj instanceof DecoratingProxy) {
                order = OrderUtils.getOrder(((DecoratingProxy) obj).getDecoratedClass());
            }
        }

        return order;
}

这里就会发现它第一句注释,其实就是检查了是否实现Ordered接口,我们点过去瞅瞅

protected Integer findOrder(Object obj) {
        return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : null);
}

看到这里,心里就开始心生疑惑,咦,这不就是如果这个类实现了Ordered接口重写了getOrder方法,这里就应该会被调用,完成排序啊,那为啥不生效,当时心里是懵逼的,这咋回事!!!

重头来过

在懵逼了一会之后,想想还是得回到ExceptionHandlerExceptionResolverinitExceptionHandlerAdviceCache方法重新开始。

private void initExceptionHandlerAdviceCache() {
        .
    .
    .
  // 2. put 前的参数来源
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  // 3. 对多个 ControllerAdviceBean 进行排序
        AnnotationAwareOrderComparator.sort(adviceBeans);
    .
    .
    .
}

再回到这里,对着这两行代码发了会呆,我终于治好了我的瞎,他排序的是ControllerAdviceBean那么它其中是不是做了一些处理,这时候我们点开ControllerAdviceBean类康康

public class ControllerAdviceBean implements Ordered {

  .
  .
  .
    private final int order;

    /**
     * Returns the order value extracted from the {@link ControllerAdvice}
     * annotation, or {@link Ordered#LOWEST_PRECEDENCE} otherwise.
     */
    @Override
    public int getOrder() {
        return this.order;
    }

    private static int initOrderFromBean(Object bean) {
        return (bean instanceof Ordered ? ((Ordered) bean).getOrder() : initOrderFromBeanType(bean.getClass()));
    }

    private static int initOrderFromBeanType(@Nullable Class<?> beanType) {
        Integer order = null;
        if (beanType != null) {
            order = OrderUtils.getOrder(beanType);
        }
        return (order != null ? order : Ordered.LOWEST_PRECEDENCE);
    }
    .
    .
    .
}

去掉无用内容我们看到了这些,它实现了Ordered接口,且有个order字段,而重写的getOrder方法返回为当前这个order字段的值,而上面踩坑部分,也不能说一定用没有,至少连贯起来能知道最终排序其实就是排序多个@ControllerAdvice或者@RestControllerAdvice标注的类生成的ControllerAdviceBeanorder字段的值,而它的来源又是什么呢,就是上面initOrderFromBeanType方法中OrderUtils.getOrder(beanType),好了,感觉快见到真相了。

柳暗花明

打开OrderUtils#getOrder方法

public static Integer getOrder(Class<?> type) {
        Object cached = orderCache.get(type);
        if (cached != null) {
            return (cached instanceof Integer ? (Integer) cached : null);
        }
        Order order = AnnotationUtils.findAnnotation(type, Order.class);
        Integer result;
        if (order != null) {
            result = order.value();
        }
        else {
            result = getPriority(type);
        }
        orderCache.put(type, (result != null ? result : NOT_ANNOTATED));
        return result;
}

emmmm这里就发现极其简单了,就找了这个标注@ControllerAdvice或者@RestControllerAdvice上有没有标注@Order注解,或者是@Priority注解,并没有检查有没有实现Ordered接口,故而无效,单单只检查了类上有无这俩注解。看到这里也总算是搞明白了这个Bug

结语

  • 修复Bug的同时如果有时间还是得明白其中发生了些啥
  • 如果有规范就按规范来吧,不要瞎胡来,此Bug就是由于同学瞎胡来,导致我们得改通用的starter来帮他们修复问题
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容