最简单说清楚Springboot中的bean加载顺序

前言

前天一个单身同事说他脱单了,我问他女方是谁,哪里认识的,他不说,装高人,许久才告诉我说,他的对象是注入的,我悟了许久,原来是公司里面的女同事呀,我又问他我认不认识,他又不说,装大佬,许久才让我想想Springboot 里面的bean的加载顺序以及我啥时候入职的,在那一刻,我突然就紧张了。

我紧张,不是因为我猜到了他对象是谁,而是我好像不是特别理得清Springboot 里面的bean 的加载顺序,为了缓解我的紧张感,我赶紧去做了一下功课,才发现原来之前有写过一篇文章就能解释清楚这个事情,所以我拜读了自己曾经的作品后,结合了一个很哇塞的示例,想在这里再单独对Springboot 里面的bean的加载顺序,进行一个梳理。

Springboot 版本:2.7.6

正文

一. Bean的前世今生

通常说到Spring 里面的bean,大家都有自己的认知,按照大家的认知,可以把大家分类如下。

  1. 一年大头兵。知道bean 就是Spring为我们创建的一个对象;
  2. 二年大头兵。不但知道一年大头兵知道的一切,还知道bean 在被创建前,还经历过BeanDefinition
  3. 三年大头兵。不但知道二年大头兵知道的一切,还知道bean 在成为BeanDefinition 前,还经历过ConfigurationClass

其实吧,通常Spring 中的bean ,在真正成为一个bean 前,首先是会被解析为ConfigurationClass ,然后再基于ConfigurationClass 创建出BeanDefinition ,最后在容器初始化的时候,再由BeanDefinition 创建出我们的bean

可能帅气的大聪明就有疑问了,我们关注bean 的今生就好了,为什么要去了解bean 的前世呢,确实,如果不想去理清bean 的加载顺序,那么是不需要去关注bean 的前世的,但是如果想理清bean 的加载顺序,那么bean 的前世就尤为重要了,特别是ConfigurationClass ,搞明白ConfigurationClass 怎么来的,再搞明白如何基于ConfigurationClass 得到BeanDefinition ,那bean 的加载顺序就十分的清楚了,这在我之前的那篇文章中是解释得很清楚的,但是那篇文章写得太长了,我估摸很多帅比是耐不住性子看完的,所以我就将ConfigurationClassBeanDefinition的加载用流程图的形式画了出来,我觉得帅比们肯定喜欢看图吧。

先看一下ConfigurationClass的加载流程图,如下所示。

可以发现把候选者创建为ConfigurationClass 然后添加到configurationClasses 中的过程是一个递归的过程,首先递归到的是初始配置对象 ,这个初始配置对象Springboot 中就是Springboot 的启动类,然后由初始配置对象 ,开枝散叶,把各种其它的候选者的ConfigurationClass 在递归的过程中都添加到configurationClasses 中,同时结合上图可以发现,不同类型的候选者的ConfigurationClass 添加到configurationClasses中是有先后顺序的,结合整个递归的流程,添加的先后顺序是如下的。

  1. 首先,由@Controller ,@Service ,@Repository ,@Component 和@Configuration 注解修饰的对象,和@Import注解间接或直接导入的对象;
  2. 其次,由自动装配机制加载的配置对象。

因为configurationClasses 是一个LinkedHashMap ,所以后续在遍历configurationClasses 时,先添加的会被先遍历到,这一点很重要。

现在拿到configurationClasses 了,再往后就是遍历configurationClasses ,然后解析每个ConfigurationClass 并得到相关的BeanDefinition,流程图如下所示。

上述流程就是按照ConfigurationClass 添加到configurationClasses 的先后顺序,依次遍历到每一个ConfigurationClass ,然后会判断这个ConfigurationClass 是否需要被跳过,这里判断的依据一般就是我们常使用的@ConditionalOnBean 和@ConditionalOnMissingBeanCondition 相关注解,再然后就是把ConfigurationClass 自己注册为容器中的BeanDefinition 和把ConfigurationClass 对应的BeanMethod (由@Bean 注解修饰的方法)注解为容器中的BeanDefinition ,最后执行ConfigurationClass 对应的ImportBeanDefinitionRegistrar 的逻辑来向容器中注册BeanDefinition

要特别关注,@ConditionalOnBean 和@ConditionalOnMissingBeanCondition 相关注解生效的时间,就是在将ConfigurationClass 解析为BeanDefinition 的一开始,那么ConfigurationClass 被遍历到的顺序就尤为重要的,我们可以仔细想想,假设A 添加了@ConditionalOnBean(B.class) ,再假设AConfigurationClass 又先于BConfigurationClass 被添加到configurationClasses 中,那么AConfigurationClass 就会被先遍历到,此时AConfigurationClass 解析为BeanDefinition 的一开始,就会去判断BBeanDefinition 是否存在于容器中,在我们的假设场景下此时B 是不存在的,所以AConfigurationClass 不会被加载为容器中的BeanDefinition ,如果想让ACondition 被满足,就需要让BConfigurationClass 先于A的被加载。

到此,bean的前世今生,就介绍完毕了,稍微总结一下。

  1. bean 的前世其实是BeanDefinition
  2. BeanDefinition 的前世其实是ConfigurationClass
  3. 每个ConfigurationClass 会按照加载的先后顺序依次解析为BeanDefinition
  4. 每个ConfigurationClass 在解析为BeanDefinition 时会先进行各种Condition判断;
  5. 所以ConfigurationClass 的加载顺序其实就影响bean的加载顺序。
  6. 由@Controller ,@Service ,@Repository ,@Component 和@Configuration 注解修饰的对象,和@Import 注解间接或直接导入的对象,其ConfigurationClass先加载;
  7. 自动装配机制加载的配置对象,其ConfigurationClass后加载;
  8. 每个ConfigurationClass 包含着自己的BeanMethod (由@Bean 注解修饰的方法)和ImportBeanDefinitionRegistrar (通过@Import注解导入的);
  9. ConfigurationClass 在解析为BeanDefinition 时,会遵循ConfigurationClass 自己本身,BeanMethodImportBeanDefinitionRegistrar这样的顺序来解析。

二. Bean的加载示例

上面已经有理论知识了,下面来结合一个示例,来做一个验证。

示例工程结构如下所示。

further包下面的代码如下所示。

java 复制代码
@ConditionalOnBean(MyController.class)
public class MyFurtherConfig {

    public MyFurtherConfig() {
        System.out.println();
    }

    @Bean
    public MyFurtherService myFurtherService() {
        return new MyFurtherService();
    }

}

public class MyFurtherService {

    public MyFurtherService() {
        System.out.println();
    }

}

origin包下面的代码,如下所示。

java 复制代码
@Configuration
@Import({MyImportBeanDefinitionRegistrar.class,
        MyImportSelector.class,
        MySupport.class})
public class MyConfig {

    public MyConfig() {
        System.out.println();
    }

    @Bean
    public MyDao myDao() {
        return new MyDao();
    }

}

public class MyController {

    public MyController() {
        System.out.println();
    }

}

public class MyDao {

    public MyDao() {
        System.out.println();
    }

}

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition = new RootBeanDefinition(MyController.class);
        registry.registerBeanDefinition("myController", beanDefinition);
    }

}

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {
                "com.lee.ioc.order.further.MyFurtherConfig"
        };
    }

}

@Service
public class MyService {

    public MyService() {
        System.out.println();
    }

}

public class MySupport {

    public MySupport() {
        System.out.println();
    }

}

pom文件如下所示。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <groupId>com.lee.ioc.order</groupId>
    <artifactId>ioc-order</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.lee.ioc.order.starter</groupId>
            <artifactId>ioc-order-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

示例工程引入了一个提前写好的starter ,这个starter结构如下所示。

TestBeanMyAutoConfig代码如下。

java 复制代码
public class TestBean {

}

@Configuration
public class MyAutoConfig {

    public MyAutoConfig() {
        System.out.println();
    }

    @Bean
    public TestBean testBean() {
        return new TestBean();
    }

}

spring.factories文件内容如下。

yaml 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.lee.ioc.order.starter.config.MyAutoConfig

pom文件如下所示。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <groupId>com.lee.ioc.order.starter</groupId>
    <artifactId>ioc-order-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

通过在每个bean 的构造函数中打断点,可以知道这些bean 的加载顺序,以debug运行程序,可以得到如下的加载顺序(越靠上越先实例化)。

  1. MyConfig (由@Configuration注解修饰);
  2. MyService (由@Service注解修饰);
  3. MySupport (由@Import注解直接导入);
  4. MyDao (由@Bean注解修饰的方法导入);
  5. MyController (由ImportBeanDefinitionRegistrar导入)
  6. MyAutoConfig(由自动装配机制导入的配置对象);
  7. TestBean (由自动装配机制导入的配置对象里的@Bean注解修饰的方法导入)。

上述的结果是完全符合我们的理论推导的,但是好像漏了MyFurtherConfigMyFurtherService,搞明白为什么漏了,就算是明白本文的主旨了。

首先,MyFurtherConfigMyConfig 通过@Import 注解导入的ImportSelector 间接导入的配置对象,所以按照ConfigurationClass 的递归加载流程,我们可以确定,MyFurtherConfigConfigurationClass 要先于MyConfigConfigurationClass加载。

其次,MyControllerMyConfig 通过@Import 注解导入的ImportBeanDefinitionRegistrar 在执行逻辑时会导入的对象,而只有在将MyConfigConfigurationClass 解析为BeanDefinition 时,才会执行到ImportBeanDefinitionRegistrar的逻辑。

所以,在将MyFurtherConfigConfigurationClass 解析为BeanDefinition 时,MyConfigConfigurationClass 还没有被解析为BeanDefinition ,从而对应的ImportBeanDefinitionRegistrar 也没有被执行,因此MyController 也没有被导入,最终导致MyFurtherConfig 的@ConditionalOnBean (MyController.class )条件不满足,则MyFurtherConfigConfigurationClass 解析为BeanDefinition 的动作不会执行,进而也不会解析MyFurtherConfigBeanMethod ,现象就是MyFurtherConfigMyFurtherService都没有注册到容器中。

总结

Spring 或者说Springboot 中的bean 的加载顺序,是很容易被忽略的一个知识点,很多人也被那稍显复杂的源码劝退,从而只能去找相关的文章寻求答案,但很多讲bean 加载顺序的文章,给的结论太过草率,结论其实是有误的,而要真正理解bean的加载顺序,其实是需要理解两大块内容。

  1. ConfigurationClass 的加载顺序。因为ConfigurationClass 在解析为BeanDefinition 时,会遵循先加载先解析的规则,所以在不加其它Condition 条件时,先加载的ConfigurationClass ,就是会比后加载的ConfigurationClass 先一步执行到解析为BeanDefinition的逻辑;
  2. ConfigurationClass 解析为BeanDefinition 的步骤。ConfigurationClass 会先把自己解析为BeanDefinition ,然后如果有由@Bean 注解修饰的方式,则把由@Bean 注解对应的BeanDefinition 解析出来,最后如果还通过@Import 注解导入了ImportBeanDefinitionRegistrar ,则再执行ImportBeanDefinitionRegistrar 的逻辑来注册BeanDefinition

所以bean 的加载顺序是一个有迹可循但是容易让人晕乎的知识点,但如果能够理解上面的ConfigurationClass 加载流程图和BeanDefinition 加载流程图,这个知识点其实就搞定百分之九十了,剩下百分之十,无非就是各种Condition条件的使用和控制自动装配类加载顺序的注解的使用,不知道这些知识其实对整体的理解是不构成影响的。

相关推荐
找了一圈尾巴1 分钟前
Wend看源码-Java-集合学习(List)
java·学习
逊嘘21 分钟前
【Java数据结构】链表相关的算法
java·数据结构·链表
爱编程的小新☆22 分钟前
不良人系列-复兴数据结构(二叉树)
java·数据结构·学习·二叉树
m0_7482478026 分钟前
SpringBoot集成Flowable
java·spring boot·后端
小娄写码35 分钟前
线程池原理
java·开发语言·jvm
散一世繁华,颠半世琉璃36 分钟前
SpringBoot揭秘:URL与HTTP方法如何定位到Controller
spring boot·后端·http
陌上花开࿈5 小时前
调用第三方接口
java
Aileen_0v06 小时前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
桂月二二7 小时前
Java与容器化:如何使用Docker和Kubernetes优化Java应用的部署
java·docker·kubernetes
liuxin334455668 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全