• 理解SpringBoot的自动装配


    1、前言

    SpringBoot 最核心的功能就是自动装配,Starter 作为 SpringBoot 的核心功能之一,基于自动配置代码提供了自动配置模块及依赖的能力,让软件集成变得简单、易用。使用 SpringBoot 时,我们只需引人对应的 Starter,SpringBoot 启动时便会自动加载相关依赖,集成相关功能,这便是 SpringBoot 的自动装配功能。


    简单概括其自动配置的原理:

    @SpringBootAppliction组合注解中的@EnableAutoConfiguration注解开启自动配置,加载 spring.factories 文件中注册的各种 AutoConfiguration 配置类,当其 @Conditional 条件注解生效时,实例化该配置类中定义的 Bean,并注入 Spring 上下文。


    SpringBoot 自动装配过程涉及以下主要内容:
    @EnableAutoConfiguration: 扫描类路径下的META-INF/spring.factories文件,并加载其中中注册的 AutoConfiguration 配置类,开启自动装配;
    spring.factories:配置文件,位于 jar 包的 META-INF 目录下,按照指定格式注册了 AutoConfiguration 配置类;
    AutoConfiguration 类:自动配置类,SpringBoot 的大量以 xxxAutoConfiguration 命名的自动配置类,定义了三方组件集成 Spring 所需初始化的 Bean 和条件;
    @Conditional 条件注解及其行生注解:使用在 AutoConfiguration 类上,设置了配置类的实例化条件;
    Starter:三方组件的依赖及配置,包括 SpringBoot 预置组件和自定义组件,往往会包含 spring.factories 文件、AutoConfiguration 类和其他配置类。
    其功能间的作用关系如下图:

    24dd037c1ef4464ca65da51f248fb0ea.png 

    2、@SpringBootApplication 注解

    SpringBoot 项目创建完成会默认生成一个xxxApplication的入口类,通过该类的 main 方法即可启动 SpringBoot 项目,通过调用 SpringApplication 的静态方法 run 作为 SpringBoot 项目的入口,代码如下:

    1. @SpringBootApplication
    2. public class LearnSpringBootApplication {
    3. public static void main(String[] args) {
    4. SpringApplication.run(LearnSpringBootApplication.class, args);
    5. }
    6. }

    在 SpringBoot 入口类上,唯一的注解就是 @SpringBootApplication。它是 SpringBoot 项目的核心注解,用于开启自动配置,更准确地说,是其组合的@EnableAutoConfiguration开启了自动装配功能,@SpringBootApplication的部分源码如下:

    1. @SpringBootConfiguration
    2. @EnableAutoConfiguration
    3. @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    4. @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    5. public @interface SpringBootApplication {
    6. // 排除指定的自动配置类
    7. @AliasFor(annotation = EnableAutoConfiguration.class)
    8. Class[] exclude() default {};
    9. // 通过类名的方式排除自动配置类
    10. @AliasFor(annotation = EnableAutoConfiguration.class)
    11. String[] excludeName() default {};
    12. // 指定扫描的包名,激活包下 @Component 等注解组件的初始化
    13. @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    14. String[] scanBasePackages() default {};
    15. // 扫描指定类所在包下的所有组件
    16. @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    17. Class[] scanBasePackageClasses() default {};
    18. @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
    19. Class nameGenerator() default BeanNameGenerator.class;
    20. // 指定是否代理 @Bean 方法以强制执行 bean 的生命周期行为
    21. @AliasFor(annotation = Configuration.class)
    22. boolean proxyBeanMethods() default true;
    23. }

    @SpringBootApplication注解中组合了@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan,其中@SpringBootConfiguration就是@Configuration,在实践过程中也可以使用这3个注解来替代@SpringBootApplication
    上面说到@EnableAutoConfiguration用来开启自动装配功能,接下来重点看下该注解。

    3、理解 @EnableAutoConfiguration

    @EnableAutoConfiguration的主要功能是在启动 Spring 应用程序上下文时进行自动配置,它会尝试配置项目可能需要的 Bean。自动配置通常是基于项目 classpath 中引入的类和已定义的 Bean 来实现的。在此过程中,被自动配置的组件来自项目自身和项目依赖的 jar 包中。

    1. @Target(ElementType.TYPE)
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Inherited
    5. @AutoConfigurationPackage
    6. @Import(AutoConfigurationImportSelector.class)
    7. public @interface EnableAutoConfiguration {
    8. String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    9. // 排除指定的自动配置类
    10. Class<?>[] exclude() default {};
    11. // 通过类名的方式排除自动配置类
    12. String[] excludeName() default {};
    13. }

    在 SpringBoot 启动应用上下文的时候,@EnableAutoConfiguration会尝试配置项目可能需要的 Bean,但在实践中如果不需要自定配置相关的 Bean,则可以通过exclude/excludeName让自动化配置类失效,比如不希望数据源(DataSource)自动配置:

    1. @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    2. public class LearnSpringBootApplication {
    3. }

    另外,注意注解类上的@AutoConfigurationPackage,通过阅读其 JavaDoc 可知:当basePackages或者basePackageClasses没有指定的话, 被@EnableAutoConfiguration注解的配置类所在的包就是扫描的基础包,这也是为什么@SpringBootApplication的主配置类要放在顶级 package 下。

    4、理解 AutoConfigurationImportSelector

    @EnableAutoConfiguration的自动配置功能是通过@Import注解导入的ImportSelector来完成的。 @Import(AutoConfigurationlmportSelector.class)@EnableAutoConfiguration注解的组成部分,也是自动配置功能的核心实现者。下面讲解@Import的基本使用方法和ImportSelector的实现类AutoConfigurationlmportSelector

    4.1 @Import 注解

    @Import注解,提供了导入配置类的功能。SpringBoot 的源代码中,有大量的EnableXXX类都使用了该注解,了解@Import有助于我们理解 SpringBoot 的自动装配,@Import有以下三个用途:

    1. 通过@Import引入@Configuration注解的类;
    2. 导入实现了ImportSelectorImportBeanDefinitionRegistrar的类;
    3. 通过@lmport导入普通的POJO

    4.2 AutoConfigurationlmportSelector 实现类

    @Import的许多功能都需要借助接口ImportSelector来实现,ImportSelector决定可引人哪些 @Configuration的注解类,ImportSelector接口源码如下:

    1. public interface ImportSelector {
    2. String[] selectImports(AnnotationMetadata importingClassMetadata);
    3. }

    ImportSelector接口只提供了一个参数为AnnotationMetadata的方法,返回的结果为一个字符串数组。其中参数AnnotationMetadata内包含了被@Import注解的类的注解信息。在selectimports方法内可根据具体实现决定返回哪些配置类的全限定名,将结果以字符串数组的形式返回。
    如果实现了接口ImportSelector的类的同时又实现了以下4个Aware接口,那么 Spring 保证在调用ImportSelector之前会先调用Aware接口的方法。这4个接口为:EnvironmentAwareBeanFactoryAware、 BeanClassLoaderAwareResourceLoaderAware

    AutoConfigurationlmportSelector的源代码中就实现了这4个接口:

    1. public class AutoConfigurationImportSelector
    2. implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    3. }

    AutoConfigurationlmportSelector实现了ImportSelector的子接口 DeferredlmportSelectorDeferredlmportSelector接口与ImportSelector的区别是,前者会在所有的 @Configuration 类加载完成之后再加载返回的配置类,而ImportSelector 是在加载完 @Configuration 类之前先去加载返回的配置类。
    其加载流程如图所示:

    b3f6970a3cfb486c80b21236247c3b35.png 

    AutoConfigurationImportSelector实现的selectImports方法如下:

    1. public String[] selectImports(AnnotationMetadata annotationMetadata) {
    2. // 自动配置开关
    3. // 检查自动配置是否开启是通过读取环境变量 spring.boot.enableautoconfiguration 确定的,默认为 true
    4. if (!isEnabled(annotationMetadata)) {
    5. return NO_IMPORTS;
    6. }
    7. // 返回封装后的自动配置信息
    8. AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    9. // 返回符合条件的配置类
    10. return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    11. }
    12. protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    13. if (!isEnabled(annotationMetadata)) {
    14. return EMPTY_ENTRY;
    15. }
    16. AnnotationAttributes attributes = getAttributes(annotationMetadata);
    17. // 过 SpringFactoriesLoader 类提供的方法加载类路径中 META-INF 目录下的 spring.factories 文件中针对 EnableautoConfiguration 的注册配置类
    18. List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    19. // 对获得的注册配置类集合进行去重处理,防止多个项目引入同样的配置类
    20. configurations = removeDuplicates(configurations);
    21. // 获得注解中被 exclude 或 excludeName 所排除的类的集合
    22. Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    23. // 检查被排除类是否可实例化,是否被自动注册配置所使用,不符合条件则抛出异常
    24. checkExcludedClasses(configurations, exclusions);
    25. // 从自动配置类集合中去除被排除的类
    26. configurations.removeAll(exclusions);
    27. // 检查配置类的注解是否符合 spring.factories 文件中 AutoConfigurationImportFilter 指定的注解检查条件
    28. configurations = getConfigurationClassFilter().filter(configurations);
    29. // 将饰选完成的配置类和排查的配置类构建为事件类,并传入监听器。监听器的配置在于 spring.factories 文件中,通过 AutoConfigurationImportlistener 指定
    30. fireAutoConfigurationImportEvents(configurations, exclusions);
    31. return new AutoConfigurationEntry(configurations, exclusions);
    32. }

    getAutoConfigurationEntry()方法获取配置类的过程比较复杂,涉及到配置类的检查、去重、监听,就不一一展开了。关键点在于getCandidateConfigurations方法中使用SpringFactoriesLoader加载META-INF/spring.factories文件中配置的EnableAutoConfiguration类型的自动配置类:

    1. List<String> configurations =
    2. SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());

    看下META-INF/spring.factories文件的部分内容:

    a778b1eec9b94525b938013ce16b6452.png 

    SpringFactoriesLoader 加载了这些xxxAutoConfiguration配置类,通过上面提到的加载、过滤、实例化的过程后,就自动开启某一些具体的功能。

    4.3 @Conditional 条件注解

    前面我们看了AutoConfigurationlmportSelector是如何进行自动配置类的读取和筛选的,打开每一个自动配置类,我们都会看到@Conditional或其行生的条件注解,其作用是限定自动配置类的生效条件,下面讲一下@Conditional注解原理。
    @Conditional可根据是否满足指定的条件来决定是否进行 Bean 的实例化及装配(比如,设定当类路径下包含某个类的时候才会对注解的类进行实例化操作),就是根据一些特定条件来控制 Bean 实例化的行为,@Conditional代码如下:

    1. @Target({ElementType.TYPE, ElementType.METHOD})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. public @interface Conditional {
    5. Class[] value();
    6. }

    @Conditional 注解唯一的元素属性是接口 Condition 的数组,只有在数组中指定的所有Conditionmatehes方法都返回true的情况下,被注解的类才会被加载:

    1. @FunctionalInterface
    2. public interface Condition {
    3. // 决定条件是否匹配
    4. boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    5. }

    matches方法的第一个参数为ConditionContext,可通过该接口提供的方法来获得 Spring 应用的上下文信息;matches方法的第二个参数为AnnotatedTypeMetadata,该接口提供了访问特定类或方法的注解功能,可以用来检查带有@Bean注解的方法上是否还有其他注解。

    4.4 @Conditional 的衍生注解

    SpringBoot 提供了很多基于@Conditional注解的衍生注解,它们适用不同的场景并提供了不同的功能,这些注解均位于spring-boot-autoconfigure项目的org.springframework.boot.autoconfigure.condition包下,比如:
    @ConditionalOnBean:在容器中有指定 Bean 的条件下;
    @ConditionalOnClass: 在 classpath 类路径下有指定类的条件下;
    @ConditionalOnMissingBean: 当容器里没有指定 Bean 的条件时;
    @ConditionalOnMissingClass:当类路径下没有指定类的条件时;
    @ConditionalOnProperty:在指定的属性有指定值的条件下;
    @ConditionalOnWebApplication:在项目是一个 Web 项目的条件下;
    阅读以上这些注解的源码可以看到,它们都组合了@Conditional注解,通过不同的Condition实现类实现不同的功能,而且用到的实现类大部分都继承自实现了Condition接口的SpringBootCondition抽象类,其继承关系如下:

    9060ff8b1dfa4c1ba6b6ccf0260c45c5.png 

    ConditionalOnClass为例,看下具体代码:

    1. @Conditional(OnClassCondition.class)
    2. public @interface ConditionalOnClass {
    3. ...
    4. }
    5. @Order(Ordered.HIGHEST_PRECEDENCE)
    6. class OnClassCondition extends FilteringSpringBootCondition {
    7. ...
    8. }
    9. public abstract class SpringBootCondition implements Condition {
    10. @Override
    11. public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    12. String classOrMethodName = getClassOrMethodName(metadata);
    13. try {
    14. // 调用新定义的抽象方法 getMatchOutcome 并交由子类来实现,
    15. // 在 matches 方法中根据子类 getMatchOutcome 返回的结果判断是否匹配
    16. ConditionOutcome outcome = getMatchOutcome(context, metadata);
    17. logOutcome(classOrMethodName, outcome);
    18. recordEvaluation(context, classOrMethodName, outcome);
    19. return outcome.isMatch();
    20. }
    21. catch (NoClassDefFoundError ex) {}
    22. }
    23. // 在抽象类 SpringBootCondition 中实现了 matches 方法,是通过调用新定义的抽象方法 getMatchOutcome 并交由子类来实现
    24. // 在 matches 方法中根据子类返回的结果判断是否匹配
    25. public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);
    26. }

    5、SpringBoot starter 实践

    SpringBoot 使用上述自动配置原理实现了许多的starter,可以在项目中快速集成。日常工作中我们可以借鉴 SpringBoot 的 starter 的创建机制来创建自己的starter,这一节我们自己实现一个简单的starter项目,可以快速集成到其他 SpringBoot 项目中。
    我们设计一个能判断当前环境的starter项目:引入该项目后,会根据环境在 Spring 上下文中自动注入一个对应当前环境的实现IProfileService接口的类Bean,该BeangetProfile方法能够返回当前的环境名,我们需要根据不同环境注入不同的Bean

    1. // 不同环境有不同的实现类:DevService、ProdService
    2. public interface IProfileService {
    3. String getProfile();
    4. }

    首先创建一个Maven项目,命名为spring-boot-starter-profilespring-boot-starter项目依赖spring-boot-autoconfigure,引入其Maven坐标:

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-autoconfigure</artifactId>
    4. <version>2.0.1.RELEASE</version>
    5. </dependency>

    创建com.spring.learn.IProfileService接口和对应devprod环境的两个实现类:

    1. public interface IProfileService {
    2. String getProfile();
    3. }
    4. // dev 环境下的实现类
    5. public class DevService implements IProfileService{
    6. @Override
    7. public String getProfile() {
    8. return "dev";
    9. }
    10. }
    11. // prod 环境下的实现类
    12. public class ProdService implements IProfileService {
    13. @Override
    14. public String getProfile() {
    15. return "prod";
    16. }
    17. }

    接下来,创建自动配置类 ProfileAutoConfiguration,如何判断环境?我们用上文提到的@ConditionalOnProperty 代替,通过在 properties(或 yml)配置文件中配置profile.service.enabled配置项来模拟不同环境:

    1. @Configuration
    2. @ConditionalOnMissingBean(IProfileService.class) // 容器中不存在 IProfileService 类型的 Bean 时才生效
    3. public class ProfileAutoConfiguration {
    4. @Bean
    5. // profile.service.enabled=dev 时,注入 DevService
    6. @ConditionalOnProperty(prefix = "profile.service", value = "enabled", havingValue = "dev")
    7. public DevService devService() {
    8. return new DevService();
    9. }
    10. @Bean
    11. // profile.service.enabled=prod 时,注入 ProdService
    12. @ConditionalOnProperty(prefix = "profile.service", value = "enabled", havingValue = "prod")
    13. public ProdService prodService() {
    14. return new ProdService();
    15. }
    16. }

    resources目录下创建META-INF/spring.factories文件,并填入以下内容:

    1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    2. com.spring.learn.ProfileAutoConfiguration

    最后,通过mvn clean install将该包安装到本地仓库,方便其他项目引用。
    任意打开一个 SpringBoot 项目,引入我们的自定义starter坐标:

    1. <dependency>
    2. <groupId>org.spring.learn</groupId>
    3. <artifactId>spring-boot-starter-profile</artifactId>
    4. <version>1.0-SNAPSHOT</version>
    5. </dependency>

    application.properties配置文件中指定下环境配置,模拟不同环境:

    profile.service.enabled=prod

    通过SpringApplicationContext获取IProfileService类型Bean,打印getProfile方法获取的环境名称:

    1. public static void main(String[] args) {
    2. ConfigurableApplicationContext context = SpringApplication.run(LearnSpringBootApplication.class, args);
    3. IProfileService bean = context.getBean(IProfileService.class);
    4. System.out.println("get profile from IProfileService: " + bean.getProfile());
    5. }

    打印结果如下:

    abd911943e7645f6abbef6c460647623.png 

    6、小结

    本文围绕 SpringBoot 的核心功能展开,从总体上了解 SpringBoot 自动配置的原理以及自动配置核心组件的运作过程,重点学习自动配置原理、@EnableAutoConfiguration、 @lmport、 ImportSelector、@Conditional 以及 starter 示例解析等内容。掌握了这些基础的组建内容及其功能,在集成其他三方类库的自动配置时,才能够更加清晰地了解它们都运用了自动配置的哪些功能。

  • 相关阅读:
    xxl-job架构原理
    【场景化解决方案】旺店通与钉钉打通,实现多包裹数据同步
    企业如何通过CRM系统赢得客户?
    图扑软件助力企业数字化转型
    [MQ] 死信队列介绍与场景描述
    当线性规划与算法相遇:揭秘单纯形法(Simplex)的独特魅力
    【Flink状态管理(六)】Checkpoint的触发方式(1)通过CheckpointCoordinator触发算子的Checkpoint操作
    9-4 查找星期 (15分)
    C++11
    辣条也玩高端局?单品年销10亿,麻辣王子如何通过极致产品力突围?
  • 原文地址:https://blog.csdn.net/m0_72088858/article/details/126826120