专栏首页BAT的乌托邦使用@AutoConfigureBefore调整配置顺序竟没生效?
原创

使用@AutoConfigureBefore调整配置顺序竟没生效?

一个人的价值体现在能够帮助多少人。自己编码好,价值能得到很好的体现。若你做出来的东西能够帮助别人开发,大大减少开发的时间,那就功德无量。 作者:A哥(YourBatman) 公众号:BAT的乌托邦(ID:BAT-utopia) 文末是否有彩蛋:有

前言

各位小伙伴大家好,我是A哥。Spring Boot是Spring家族具有划时代意义的一款产品,它发展自Spring Framework却又高于它,这种高于主要表现在其最重要的三大特性,而相较于这三大特性中更为重要的便是Spring Boot的自动配置AutoConfiguration)。与其说是自动,倒不如说是“智能”,该框架看起来好像“更聪明”了。因此它也顺理成章的成为了构建微服务的基础设施,稳坐第一宝座。

生活之道,在于取舍。编程何尝不是,任何决定都会是一把双刃剑,Spring Boot的自动配置解决了Spring Framework使用起来的众多痛点,让开发效率可以得到指数级提升(想一想,这不就是功德无量吗?)。成也萧何败也萧何,也正是因为它的太智能,倘若出了问题就会让程序员两眼一抹黑,无从下手。

瑕不掩瑜,Spring Boot前进的步伐浩浩荡荡,学就完了

这不,我就在前几天收到一个“求助”,希望使用@AutoConfigureBefore控制配置的顺序,但并未能如愿。本文就针对这个场景case稍作展开,讨论下使用@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder三大注解控制自动配置执行顺序的正确姿势

提示:Spring Boot的自动配置是通过@EnableAutoConfiguration注解驱动的,默认是开启状态。你也可以通过spring.boot.enableautoconfiguration = false来关闭它,回退到Spring Framework时代。显然这不是本文需要讨论的内容~


正文

本文将要聊的重点是Spring Boot自动配置 + 顺序控制,自动配置大家都耳熟能详,那么“首当其冲”就是知晓这个问题:配置类的执行为何需要控制顺序?


配置类为何需要顺序?

我们已经知道Spring容器它对Bean的初始化是无序的,我们并不能想当然的通过@Order注解来控制其执行顺序。一般来说,对于容器内普通的Bean我们只需要关注依赖关系即可,而并不需要关心其绝对的顺序,而依赖关系的管理Spring的是做得很好的,这不连循环依赖它都可以搞定麽。

@Configuration配置类它也是一个Bean,但对于配置类来说,某些场景下的执行顺序是必须的,是需要得到保证的。比如很典型的一个非A即B的case:若容器内已经存在A了,就不要再把B放进来。这种case即使用中文理解,就能知道对A的“判断”必须要放在B的前面,否则可能导致程序出问题。

那么针对于配置的执行顺序,传统Spring和Spring Boot下各自是如何处理的,表现如何呢?


Spring下控制配置执行顺序

在传统的Spring Framework里,一个@Configuration注解标注的类就代表一个配置类,当存在多个@Configuration时,他们的执行顺序是由使用者靠手动指定的,就像这样:

// 手动控制Config1 Config2的顺序
ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class, Config2.class);

当然,你可能就疑问了说:即使在传统Spirng里,我也从没有自己使用过AnnotationConfigApplicationContext来显示加载配置啊,都是使用@Configuration定义好配置类后,点击Run一把唆的。没错,那是因为你是在web环境下使用Spring,IoC容器是借助web容器(如Tomcat等)来驱动的,Spring对此部分封装得非常好,所以做到了对使用者几乎无感知。

关于这部分的内容,此处就不深究了,毕竟本文重点不在这嘛。但可以给出给小结论:@Configuration配置被加载进容器的方式大体上可分为两种:

  1. 手动。构建ApplicationContext时由构建者手动传入,可手动控制顺序
  2. 自动。被@ComponentScan自动扫描进去,无法控制顺序

绝大多数情况下我们都是使用自动的方式,所以在Spring下对配置的顺序并无感知。其实这也是需求驱使,因为在传统Spring下我们并无此需求,所以对它无感是合乎逻辑的。另说一句,虽然我们并不能控制Bean的顺序,但是我们是可以干涉它的,比如:控制依赖关系、提升优先级、“间接”控制执行顺序...当然喽这是后面文章的内容,敬请关注。


Spring Boot下控制配置执行顺序

Spring Boot下对自动配置的管理对比于Spring它就是黑盒,它会根据当前容器内的情况来动态的判断自动配置类的加载与否、以及加载的顺序,所以可以说:Spring Boot的自动配置它对顺序是有强要求的。需求驱使,Spring Boot给我们提供了@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder(下面统称这三个注解为“三大注解”)这三个注解来帮我们解决这种诉求。

需要注意的是:三大注解是Spring Boot提供的而非Spring Framework。其中前两个是1.0.0就有了,@AutoConfigureOrder属于1.3.0版本新增,表示绝对顺序(数字越小,优先级越高)。另外,这几个注解并不互斥,可以同时标注在同一个@Configuration自动配置类上。


Spring Boot内置的控制配置顺序举例

为方便大家理解,我列出一个Spring Boot它自己的使用作为示例学一学。以大家最为熟悉的WebMvc的自动配置场景为例:

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration { ... }


@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration { ... }


@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class ServletWebServerFactoryAutoConfiguration { ... }

这几个配置是WebMVC的核心配置,他们之间是有顺序关系的:

  • WebMvcAutoConfiguration被加载的前提是:DispatcherServletAutoConfiguration、TaskExecutionAutoConfiguration、ValidationAutoConfiguration这三个哥们都已经完成初始化
  • DispatcherServletAutoConfiguration被加载的前提是:ServletWebServerFactoryAutoConfiguration已经完成初始化
  • ServletWebServerFactoryAutoConfiguration被加载的前提是:@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)最高优先级,也就是说它无其它依赖,希望自己是最先被初始化的 - 当碰到多个配置都是最高优先级的时候,且互相之前没有关系的话,顺序也是不定的。但若互相之间存在依赖关系(如本利的DispatcherServletAutoConfigurationServletWebServerFactoryAutoConfiguration),那就按照相对顺序走

WebMvcAutoConfiguration加载,在它之后其实还有很多配置会尝试执行,例如:

@AutoConfigureAfter(WebMvcAutoConfiguration.class)
class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration { ... }

@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class GroovyTemplateAutoConfiguration { ... }

@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { ... }

@AutoConfigureAfter(WebMvcAutoConfiguration.class)
public class LifecycleMvcEndpointAutoConfiguration { ... }

这些都很容易理解:如果都不是Web环境,加载一些模版引擎的并无必要嘛。


三大注解使用的误区(重要)

根据我的切身体会,针对这三大注解,实在有太多人把它误用了,想用但是用了却又不生效,于是就容易触发一波“骂街”操作,其实这也是我书写本文的最大动力所在:纠正你的错误使用,告诉你正确姿势。


错误使用示例

我见到的非常多的小伙伴这么来使用三大注解:我这里使用“伪代码”进行模拟

@Configuration
public class B_ParentConfig {

    B_ParentConfig() {
        System.out.println("配置类ParentConfig构造器被执行...");
    }
}

@Configuration
public class A_SonConfig {

    A_SonConfig() {
        System.out.println("配置类SonConfig构造器被执行...");
    }
}

@Configuration
public class C_DemoConfig {
    public C_DemoConfig(){
        System.out.println("我是被自动扫描的配置,初始化啦....");
    }
}

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args).close();
    }
}

通过名称能知道我想要的达到的效果是:ParentConfig先加载,SonConfig后加载。(DemoConfig作为一个参考配置,作为日志参考使用即可)

启动应用,控制台打印:

配置类SonConfig构造器被执行...
配置类ParentConfig构造器被执行...
我是被自动扫描的配置,初始化啦....

Son优先于Parent被加载了,这明显不符合要求。因此,我看到很多小伙伴就这么干:

@AutoConfigureBefore(A_SonConfig.class)
@Configuration
public class B_ParentConfig {

    B_ParentConfig() {
        System.out.println("配置类ParentConfig构造器被执行...");
    }
}

通过@AutoConfigureBefore控制,表示在A_SonConfig之前执行此配置。语义层面上看,貌似没有任何问题,再次启动应用:

配置类SonConfig构造器被执行...
配置类ParentConfig构造器被执行...
我是被自动扫描的配置,初始化啦....

what a fuck。看到没,我没骗你吧,骂街了骂街了

竟然没生效?代码不会骗人,@AutoConfigureBefore的语义也没有问题,而是你使用的姿势不对,下面我会给你正确姿势。


三大注解使用的正确姿势

针对以上case,要想达到预期效果,正确姿势只需要下面两步:

  1. A_SonConfigB_ParentConfig挪动到Application扫描不到的包内,切记:一定且必须是扫描不到的包内org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.fsx.autoconfig.A_SonConfig,com.fsx.autoconfig.B_ParentConfig再次启动应用看看,打印输出:我是被自动扫描的配置,初始化啦.... 配置类ParentConfig构造器被执行... 配置类SonConfig构造器被执行...完美。符合预期,Parent终于在Son之前完成了初始化,也就是说我们的@AutoConfigureBefore注解生效了。
  2. 当前工程里增加配置META-INF/spring.factories,内容为(配置里Son和Parent前后顺序对结果无影响):

使用细节注意事项

针对此使用姿势,虽然很正确,并不是完全没有“副作用”的,有如下细节平时也需要引起注意:

  • 若你不用@AutoConfigureBefore这个注解,单单就想依赖于spring.factories里的先后顺序的来控制实际的加载顺序,答案是不可以,控制不了
  • 例子中有个小细节:我每次都故意输出了我是被自动扫描的配置,初始化啦....这句话,可以发现被扫描进去配置实例化是在它前面(见错误示例),而通过spring.factories方式进去是在它的后面(见正确姿势)
  • 从这个小细节可以衍生得到结论:Spring Boot的自动配置均是通过spring.factories来指定的,它的优先级最低(执行时机是最晚的);通过扫描进来的一般都是你自己自定义的配置类,所以优先级是最高的,肯定在自动配置之前加载 - 从这你应该学到:若你要指定扫描的包名,请千万不要扫描到形如org.springframework这种包名,否则“天下大乱”(当然喽为了防止这种情况出现,Spring Boot做了容错的。它有一个类专门检测这个case防止你配置错了,具体参见ComponentScanPackageCheck默认实现)
  • 请尽量不要让自动配置类既被扫描到了,又放在spring.factories配置了,否则后者会覆盖前者,很容易造成莫名其妙的错误

小总结,对于三大注解的正确使用姿势是应该是:请使用在你的自动配置里(一般是你自定义starter时使用),而不是使用在你业务工程中的@Configuration里,因为那会毫无效果。

三大注解解析时机浅析

为了更好的辅助理解,加强记忆,本文将这三大注解解析时机简要的絮叨一下,知道了它被解析的时机,自然就很好解释为何你那么写是无效的喽。

这三个注解的解析都是交给AutoConfigurationSorter来排序、处理的,做法类似于AnnotationAwareOrderComparator去解析排序@Order注解。核心代码如下:

class AutoConfigurationSorter {
	
	// 唯一给外部调用的方法:返回排序好的Names,因此返回的是个List嘛(ArrayList)
	List<String> getInPriorityOrder(Collection<String> classNames) {
		...
		// 先按照自然顺序排一波
		Collections.sort(orderedClassNames);
		// 在按照@AutoConfigureBefore这三个注解排一波
		orderedClassNames = sortByAnnotation(classes, orderedClassNames);
		return orderedClassNames;
	}
	...
}

此排序器被两个地方使用到:

  • AutoConfigurationImportSelector:Spring自动配置处理器,用于加载所有的自动配置类。它实现了DeferredImportSelector接口:这也顺便解释了为何自动配置是最后执行的原因~
  • AutoConfigurations:表示自动配置@Configuration类。

这个排序的“解析/排序”过程还是比较复杂的,本文点到为止,观其大意即可。你可以简单粗暴的记住结论:@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder这三个注解只能作用于自动配置类,而不能是自定义的@Configuration配置类。


总结

关于Spring Boot自动配置顺序相关的三大注解@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder就先介绍到这了,本文主要用意是为了帮助大家规范此些“常用注解”的使用,规避一些误区,端正使用姿势,避免犯错时又丈二和尚。

我看到不少文章、生产上的代码都使用错了(估计有没有效果自己的都不知道,又或者刚好歪打正着确实是在xxx后面执行而以为生效了),希望本文能帮助到你。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用@AutoConfigureBefore调整配置顺序竟没生效?

    各位小伙伴大家好,我是A哥。Spring Boot是Spring家族具有划时代意义的一款产品,它发展自Spring Framework却又高于它,这种高于主要表...

    YourBatman
  • Spring配置类深度剖析-总结篇(手绘流程图,可白嫖)

    各位好,我是A哥。最近写了好几篇关于Spring @Configuration的文章,收录在Spring配置类专栏里,这是本公众号的第一个专栏(虽然CSDN里已...

    YourBatman
  • 配置类需要标注@Configuration却不知原因?那这次就不能给你涨薪喽

    各位小伙伴大家好,我是A哥。这是继上篇文章:真懂Spring的@Configuration配置类?你可能自我感觉太良好 的原理/源码解释篇。按照本公众号的定位,...

    YourBatman
  • 使用@AutoConfigureBefore调整配置顺序竟没生效?

    各位小伙伴大家好,我是A哥。Spring Boot是Spring家族具有划时代意义的一款产品,它发展自Spring Framework却又高于它,这种高于主要表...

    YourBatman
  • 象棋游戏~~~

    WolframChina
  • Spring Boot 从入门到实践系列教程(3)- Spring注解

    在本快速教程中,我们将探讨org.springframework.boot.autoconfigure和org.springframework.boot.aut...

    王炸
  • 【Java编程实战】Metasploit_Java后门运行原理分析以及实现源码级免杀与JRE精简化

    某日午睡迷迷糊糊梦到Metasploit里有个Java平台的远控载荷,梦醒后,打开虚拟机,在框架中搜索到了这个载荷

    重生信息安全
  • Log4j 2.0在开发中的高级使用详解—介绍篇(一)

    Log4j终于迎来了首个apache版本。Log4j 2 是 Log4j 的升级版本,该版本比起其前任来说有着显著的改进,包含很多在 Logback 中的改进...

    业余草
  • StringBuilder,String与StringBuffer 的异同

    用户1665735
  • 科技生态5.0——5G带来生活和行业生态大变革

    如果时光能够倒流,我们将能深刻感知人类生活的曲线变化之快。让生活的时钟回到20多年前,回到书信往来的时代,把时间的长河拉长,从点到线我们就能感知到科技发展变化之...

    机器思维研究院

扫码关注云+社区

领取腾讯云代金券