SpringBoot ApplicationContextInitializer系统初始化器原理解析

前言

Spring提供了ApplicationContextInitializer接口作为系统初始化器的实现,其本质就是Spring容器刷新之前执行的一个回调函数,可以让我们非常方便在Spring容器刷新前,往ApplicationContext添加一些属性或者添加要激活的配置文件等。本文主要通过学习SpringBoot源码的形式,来解析一下在SpringBoot是如何创建系统初始化器并应用的。

准备

事先准备两个自定义ApplicationContextInitializer接口实现,代码如下

MyApplicationContextInitializer

less 复制代码
@Slf4j
@Order(2)
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 声明要添加的属性
        Map<String, Object> mysqlMap = new HashMap<>();
        mysqlMap.put("mysql-host", "127.0.0.1");
        // 将属性添加到Application Context中
        applicationContext.getEnvironment().getPropertySources()
                .addLast(new MapPropertySource("mysqlMap", mysqlMap));

        // 添加要激活的配置文件
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        environment.addActiveProfile("extend");
        ConfigDataEnvironmentPostProcessor.applyTo(environment);
        log.info("MyApplicationContextInitializer initialize");
    }

}

OtherApplicationContextInitializer

less 复制代码
@Slf4j
@Order(1) // 数值越小,优先级越高
public class OtherApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
       log.info("OtherApplicationContextInitializer initialize...");
    }

}

Tips:这两个类之前有讲过,传送门: 教你如何扩展ApplicationContext

SpringApplicaiton启动类

less 复制代码
@Slf4j
@SpringBootApplication
public class Application {

    public static void main(String[] args) {

        // 初始化SpringApplication
        SpringApplication application = new SpringApplication(Application.class);
        
        // 添加初始化器
        application.addInitializers(new MyApplicationContextInitializer());
        application.addInitializers(new OtherApplicationContextInitializer());

        // 启动SpringApplication
        application.run(args);
    }

原理探索

调用链路

initializers列表

在调用SpringApplicaiton.ddInitializers()注册系统初始化器时,可以看到实际就是往SpringApplication的initializers列表添加

applyInitializers()

接着查看initializers引用情况,可以看到在SpringApplication的applyInitializers()方法中,会获取当前初始化器列表,然后逐个遍历,并调用其initialize()方法,实现在Spring容器刷新前回调。

prepareContext()

再往上查找方法调用,可以看出是在SpringApplication的prepareContext()方法中进行初始化器回调操作的。

而prepareContext()又是在SpringApplication的run()方法进行调用的

由此我们已经摸清了ApplicationContextInitializer的调用链路,在SpringApplication启动后不久就开始逐个遍历处理系统初始化器

排序

顺带一提,如果有多个ApplicationContextInitializer实现,它们之间可以通过@Order注解定义优先级,而排序则是在获取初始化器列表的时候,也就是SpringApplication的asUnmodifiableOrderedSet()方法。

初始化链路

new SpringApplication()

搞清楚调用链路后,还需要知道this.initializers在什么时候?通过什么方式初始化的?接下来再通过属性引用找到为initializers赋值的地方

最终找到是在SpringApplication的构造函数中为其赋值的,也就是说在SpringApplication初始化的时候,就已经把ApplicationContextInitializer列表给初始化好了。

SpringApplication.getSpringFactoriesInstances()

继续往下找,最终找到是在getSpringFactoriesInstances()方法中找到了初始化逻辑

先是获取所有ApplicationContextInitializer实现的全限定类名

然后创建具体的实例

具体创建逻辑则在SpringApplication的createSpringFactoriesInstances()方法中,其实就是通过反射的机制创建具体实例

Tips:这个时候ApplicationContextInitializer列表并不包含自定义实现的 MyApplicationContextInitializer和OtherApplicationContextInitializer,因为这两个是在SpringApplication初始化完成后通过addInitializers()方法添加的,而这个时候还没走到addInitializers()这一步。

并且可以看到在SpringApplication的getSpringFactoriesInstances()方法中,其实已经有做过排序AnnotationAwareOrderComparator.sort(instances);

但是在获取this.initializers时,也还是要再做一次排序list.sort(AnnotationAwareOrderComparator.INSTANCE);

因为在开发者调用SpringApplication的addInitializers()方法添加自定义ApplicationContextInitializer后,原先已经排好序的initializers有可能需要重新调整,所以即便在初始化initializers时排了一次序,在获取initializers时也再做了一次排序,也就是二次排序。

SpringFactoriesLoader

在获取所有ApplicationContextInitializer的全限定类名时,可以看到是调用了SpringFactoriesLoaderloadFactoryNames()方法

可以从注释看到SpringFactoriesLoader是框架内部使用的通用工厂加载机制它会读取类路径下所有JAR文件中的 META-INF/spring.factories文件加载并实例化给定类型的工厂

还简单给出了示例,spring.factories文件必须是Key=Value形式,其中Key是抽象类或者接口的全限定类名,Value是逗号分隔的实现类的全限定类名列表。

ini 复制代码
org.springframework.context.ApplicationContextInitializer=\geek.springboot.application.contextInitializer.MyApplicationContextInitializer

到这里我们就知道了,为什么在实现自动装配或者注册一些监听器时,只能按照规则在resources文件夹下创建 META-INF/spring.factories,因为SpringFactoriesLoader的实现就已经限定了它只会读取 META-INF/spring.factories。并且也明白了原来所有的spring.factories都由 SpringFactoriesLoader读取,然后加载并实例化给定类型的实现。

loadFactoryNames()

回到loadFactoryNames()的实现,可以看到它最终是调用了loadSpringFactories()方法获取全部已加载的Spring工厂实现,最后获取关于ApplicationContextInitializer接口的工厂实现。

loadSpringFactories()

走进loadSpringFactories()方法中时,可以看到SpringFactoriesLoader使用了缓存机制,根据classLoader获取相关的工厂实现,若不为空则直接返回。说明了classLoader读取META-INF/spring.factories只会读取一次,并且是用了ConcurrentReferenceHashMap保存工厂实现。

loadSpringFactories()具体实现逻辑,根据classLoader查询缓存中是否存在相应的工厂实现,有的话直接返回,否则进行META-INF/spring.factories读取

BootstrapRegistryInitializer初始化时

那么其实可以推断出在调用SpringApplicaiton的setInitializers()方法之前,META-INF/spring.factories已经被读取过了,回过头来看SpringApplication初始化逻辑,可以看到在setInitializers()之前,还做过初始化this.bootstrapRegistryInitializers操作,而就是在那个时候SpringFactoriesLoader读取了META-INF/spring.factories

触类旁通

查看SpringApplication的getSpringFactoriesInstances()方法调用,可以看到

ApplicationListener

SpringApplicationRunListener

SpringBootExceptionReporter

一些之前已经提到过的实现,跟初始化ApplicationContextInitializer的本质其实是一样的,最终都是依靠SpringApplication的getSpringFactoriesInstances()方法,而该方法实现又和SpringFactoriesLoader离不开关系。

总结

从初始化到调用这完整的链路,其实在写本文之前我已经通过debug理顺了一遍。但是在写本文的时候,我并没有直接给标准答案似的,一上来就说通过SpringFactoriesLoader加载META-INF/spring.factories,然后怎么怎么样,最后走到SpringApplicaitonprepareContext()方法进行回调。

其实我是故意的,因为若是第一次通过源码看一个功能的实现原理,其实并不会那么顺畅的,都是要通过反复断点,反复查看方法调用链路,最终才能理顺整个实现逻辑。所以本文其实是还原了当时查看源码分析原理时的视角,强调的是怎么一步步找到答案的过程。

结尾

本文章源自《Learn SpringBoot》专栏,感兴趣的话还请关注点赞收藏.

上一篇文章:《SpringBoot核心特性------教你如何自定义@Conditional...条件装配

相关推荐
IT毕设梦工厂11 分钟前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
Ylucius1 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习
七夜zippoe1 小时前
分布式系统实战经验
java·分布式
是梦终空1 小时前
JAVA毕业设计176—基于Java+Springboot+vue3的交通旅游订票管理系统(源代码+数据库)
java·spring boot·vue·毕业设计·课程设计·源代码·交通订票
落落落sss1 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
码爸2 小时前
flink doris批量sink
java·前端·flink
工业互联网专业2 小时前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
Monodye2 小时前
【Java】网络编程:TCP_IP协议详解(IP协议数据报文及如何解决IPv4不够的状况)
java·网络·数据结构·算法·系统架构
一丝晨光2 小时前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
无名指的等待7123 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端