【工作笔记】@AutoConfigureAfter失效问题

起因

今天项目启动时,一直提示错误缺少一个 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构造器被执行...
// 我是被自动扫描的配置,初始化啦....

三大注解使用的正确姿势

  1. 把 A_SonConfig 和 B_ParentConfig 挪动到 Application 扫描不到的包内,切记:一定且必须是扫描不到的包内

  2. 当前工程里增加配置 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 注解生效了

使用细节注意事项

  1. 若你不用 @AutoConfigureBefore 这个注解,单单就想依赖于 spring.factories 里的先后顺序的来控制实际的加载顺序,答案是不可以,控制不了,可以得到结论:Spring Boot 的自动配置均是通过 spring.factories 来指定的,它的优先级最低(执行时机是最晚的);通过扫描进来的一般都是你自己自定义的配置类,所以优先级是最高的,肯定在自动配置之前加载,从这你应该学到:若你要指定扫描的包名,请千万不要扫描到形如 org.springframework 这种包名,否则“天下大乱”(当然喽为了防止这种情况出现,Spring Boot 做了容错的。它有一个类专门检测这个 case 防止你配置错了,具体参见 ComponentScanPackageCheck 默认实现)

  2. 请尽量不要让自动配置类既被扫描到了,又放在 spring.factories 配置了,否则后者会覆盖前者,很容易造成莫名其妙的错误

  3. 小总结,对于三大注解的正确使用姿势是应该是:请使用在你的自动配置里(一般是你自定义 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;
    }
    ...
}

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

  1. AutoConfigurationImportSelector:Spring 自动配置处理器,用于加载所有的自动配置类。它实现了 DeferredImportSelector 接口:这也顺便解释了为何自动配置是最后执行的原因
  2. 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...

例子说明了两点

  1. 配置中 @bean 的顺序会影响注入顺序
  2. 说明 @Autowired 和构造器注入会影响有关联关系的 bean ,并且 @Autowired 会导致注入顺序的错误,而构造器不会造成注入顺序的错误

@Autowired 注入的时机晚于构造器注入我是知道的,但会有什么影响我是不知道的,还是继续求助于度娘吧

  • 首先是 @Bean 和 @Autowired 的区别(大佬解释的真精辟)

    1. @Bean 告诉 Spring 这是此类的一个实例,请保留该类,并在我询问时将其还给我
    2. @Autowired 说 请给我一个该类的实例
  • 其次 @autowired 用在属性和构造器上的区别

    1. @autowired:在变量上的注入要等到类完全加载完,才会将相应的 bean 注入
    2. 变量:在加载类的时候按照相应顺序加载的,所以变量的加载要早于 @autowired 变量的加载
    3. 构造器:是在加载类的时候加载的
  • 所以 @Autowired 一定要等本类构造完成后,才能从外部引用设置进来,结合测试例子来说

    1. 如果使用构造器,则 BeanB 的引入一定实在 BeanA 完成初始化之前的,所以无论怎样调整 Configuration 中的位置,都是 BeanB 优先于 BeanA
    2. 如果使用 @Autowired
      • 如果是 BeanB 的位置在 BeanA 之前,则先加载 BeanB,然后等待 BeanA 调用即可
      • 如果是 BeanB 的位置在 BeanA 之后,则先加载 BeanA,然后等待 BeanA 完成加载之后,在注入 BeanB

结论

  1. 项目的包结构要设计合理
  2. 推荐使用构造器注入

文章参考

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值