起因
今天项目启动时,一直提示错误缺少一个 bean。查询缘由自动注入一个 bean 时,它依赖的 bean 没有注入进来,于是便增加了 @AutoConfigureAfter 注解,但依旧提示相同的错误。最后还是靠度娘解决了问题,大佬的文章如下(略作修改)
前言
本文就讨论下使用 @AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder 三大注解控制自动配置执行顺序的正确姿势
提示:Spring Boot 的自动配置是通过 @EnableAutoConfiguration 注解驱动的,默认是开启状态。你也可以通过 spring.boot.enableautoconfiguration = false 来关闭它
配置类为何需要顺序
我们已经知道 Spring 容器它对 Bean 的初始化是无序的,我们并不能想当然的通过 @Order 注解来控制其执行顺序。一般来说,对于容器内普通的 Bean 我们只需要关注依赖关系即可,而并不需要关心其绝对的顺序,而依赖关系的管理 Spring 的是做得很好的,这不连循环依赖它都可以搞定麽
@Configuration 配置类它也是一个 Bean,但对于配置类来说,某些场景下的执行顺序是必须的,是需要得到保证的。比如很典型的一个非 A 即 B 的 case:若容器内已经存在 A 了,就不要再把 B 放进来。这种 case 即使用中文理解,就能知道对 A 的“判断”必须要放在 B 的前面,否则可能导致程序出问题
传统 Spring 和 Spring Boot 下各自的处理
- Spring 下控制配置执行顺序
在传统的 Spring Framework 里,一个 @Configuration 注解标注的类就代表一个配置类,当存在多个 @Configuration 时,他们的执行顺序是由使用者靠手动指定的,就像这样
ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class, Config2.class);
@Configuration 配置被加载进容器的方式大体上可分为两种
1. 手动。构建 ApplicationContext 时由构建者手动传入,可手动控制顺序
2. 自动。被 @ComponentScan 自动扫描进去,无法控制顺序
- Spring Boot下控制配置执行顺序
Spring Boot 下对自动配置的管理对比于 Spring 它就是黑盒,它会根据当前容器内的情况来动态的判断自动配置类的加载与否、以及加载的顺序,所以可以说:Spring Boot 的自动配置它对顺序是有强要求的。需求驱使,Spring Boot 给我们提供了 @AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder(下面统称这三个注解为“三大注解”)这三个注解来帮我们解决这种诉求
需要注意的是:三大注解是 Spring Boot 提供的而非 Spring Framework 。其中前两个是 1.0.0 就有了,@AutoConfigureOrder 属于 1.3.0 版本新增,表示绝对顺序(数字越小,优先级越高)。另外,这几个注解并不互斥,可以同时标注在同一个 @Configuration 自动配置类上
@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 { ... }
WebMvcAutoConfiguration 被加载的前提是:DispatcherServletAutoConfiguration、TaskExecutionAutoConfiguration、ValidationAutoConfiguration 这三个哥们都已经完成初始化
DispatcherServletAutoConfiguration 被加载的前提是:ServletWebServerFactoryAutoConfiguration 已经完成初始化
ServletWebServerFactoryAutoConfiguration 被加载的前提是:@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) 最高优先级,也就是说它无其它依赖,希望自己是最先被初始化的
当碰到多个配置都是最高优先级的时候,且互相之前没有关系的话,顺序也是不定的。但若互相之间存在依赖关系(如本利的 DispatcherServletAutoConfiguration 和 ServletWebServerFactoryAutoConfiguration ),那就按照相对顺序走在 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 { ... }
错误使用示例
@AutoConfigureBefore(A_SonConfig.class)
@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();
}
}
// 输出
// 配置类SonConfig构造器被执行...
// 配置类ParentConfig构造器被执行...
// 我是被自动扫描的配置,初始化啦....
三大注解使用的正确姿势
-
把 A_SonConfig 和 B_ParentConfig 挪动到 Application 扫描不到的包内,切记:一定且必须是扫描不到的包内
-
当前工程里增加配置 META-INF/spring.factories,内容为(配置里Son和Parent前后顺序对结果无影响):
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.fsx.autoconfig.A_SonConfig,com.fsx.autoconfig.B_ParentConfig
再次启动应用看看,打印输出
// 输出
// 我是被自动扫描的配置,初始化啦....
// 配置类ParentConfig构造器被执行...
// 配置类SonConfig构造器被执行...
完美。符合预期,Parent 终于在 Son 之前完成了初始化,也就是说我们的 @AutoConfigureBefore 注解生效了
使用细节注意事项
-
若你不用 @AutoConfigureBefore 这个注解,单单就想依赖于 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 类
大佬的文章粘贴完了,但我们的问题依旧在。。
因为我们的项目在做配置类时,已经对包做了隔离。所以问题并不是因为 Application 扫描的原因,并且看日志,两个有顺序的配置类确实是按照顺序加载的。那么问题就出在了别的地方
经过查看,后注入的 bean 在依赖另一个先注入 bean 时,是通过 @Autowired 注解进行注入的。也就是
public class A{
@Autowired
private B b;
}
后来改为如下配置,项目得以成功启动
public class A{
private B b;
public A(B b){
this.b = b;
}
}
先用例子做个排除吧
public class BeanA {
private BeanB beanB;
public BeanA(BeanB beanB) {
this.beanB = beanB;
}
}
public class BeanB {
}
@Configuration
public class BeanAAutoConfiguration {
@Bean
public BeanA beanA(BeanB beanB) {
System.out.println("A...");
return new BeanA(beanB);
}
@Bean
public BeanB beanB() {
System.out.println("B...");
return new BeanB();
}
}
// 此时输出的永远是 即使调换 BeanAAutoConfiguration 中 BeanA 和 BeanB 的位置
// B...
// A...
public class BeanA {
@Autowired
private BeanB beanB;
}
public class BeanB {
}
@Configuration
public class BeanAAutoConfiguration {
@Bean
public BeanA beanA(BeanB beanB) {
System.out.println("A...");
return new BeanA(beanB);
}
@Bean
public BeanB beanB() {
System.out.println("B...");
return new BeanB();
}
}
// 此时输出的是
// A...
// B...
// 如果 调换 BeanAAutoConfiguration 中 BeanA 和 BeanB 的位置则输出
// B...
// A...
例子说明了两点
- 配置中 @bean 的顺序会影响注入顺序
- 说明 @Autowired 和构造器注入会影响有关联关系的 bean ,并且 @Autowired 会导致注入顺序的错误,而构造器不会造成注入顺序的错误
@Autowired 注入的时机晚于构造器注入我是知道的,但会有什么影响我是不知道的,还是继续求助于度娘吧
-
首先是 @Bean 和 @Autowired 的区别(大佬解释的真精辟)
- @Bean 告诉 Spring 这是此类的一个实例,请保留该类,并在我询问时将其还给我
- @Autowired 说 请给我一个该类的实例
-
其次 @autowired 用在属性和构造器上的区别
- @autowired:在变量上的注入要等到类完全加载完,才会将相应的 bean 注入
- 变量:在加载类的时候按照相应顺序加载的,所以变量的加载要早于 @autowired 变量的加载
- 构造器:是在加载类的时候加载的
-
所以 @Autowired 一定要等本类构造完成后,才能从外部引用设置进来,结合测试例子来说
- 如果使用构造器,则 BeanB 的引入一定实在 BeanA 完成初始化之前的,所以无论怎样调整 Configuration 中的位置,都是 BeanB 优先于 BeanA
- 如果使用 @Autowired
- 如果是 BeanB 的位置在 BeanA 之前,则先加载 BeanB,然后等待 BeanA 调用即可
- 如果是 BeanB 的位置在 BeanA 之后,则先加载 BeanA,然后等待 BeanA 完成加载之后,在注入 BeanB
结论
- 项目的包结构要设计合理
- 推荐使用构造器注入