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...条件装配

相关推荐
中國移动丶移不动2 分钟前
深度解析:Maven 和 Gradle 的使用比较及常见仓库推荐
java·maven
weixin_4383354012 分钟前
springboot 中添加TCP连接服务端
spring boot·后端·tcp/ip
得寸进尺的鸡丝面18 分钟前
tomcat官网下载历史版本
java·tomcat
罗政20 分钟前
PDF书籍《手写调用链监控APM系统-Java版》第7章 插件与链路的结合:Tomcat插件实现
java·pdf·tomcat
程序猿毕设源码分享网28 分钟前
基于springboot校园招聘系统源码和论文
java·spring boot·后端
m0_7482526033 分钟前
HarmonyOS Next 实现登录注册页面(ARKTS) 并使用Springboot作为后端提供接口
spring boot·华为·harmonyos
山山而川粤44 分钟前
美食推荐系统|Java|SSM|JSP|
java·开发语言·后端·学习·mysql
小小药1 小时前
012-spring的注解开发
java·后端·spring
找了一圈尾巴1 小时前
Wend看源码-Java-集合学习(Queue)
java·学习
#sakura1 小时前
javaEE
java·开发语言·java-ee