SpringBoot核心特性——教你如何扩展ApplicationContext

482 阅读4分钟

前言

这次稍微讲讲如何在SpringBoot中进行应用上下文扩展,它可以让我们在SpringBoot启动时进行一些属性注册、激活相应配置文件等操作。

ApplicationContextInitializer接口

要想进行ApplicationContext扩展,那么就需要实现ApplicationContextInitializer接口,新建一个自定义ApplicationContextInitializer实现,代码如下:

package geek.springboot.application.contextInitializer;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.context.ApplicationContextInitializer;  
import org.springframework.context.ConfigurableApplicationContext;  
  
/**  
* 自定义ApplicationContext初始化实现  
*  
* @author Bruse  
*/  
@Slf4j  
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {  

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

通过spring.factories注册

在resources目录下新建META-INF/spring.factories,进行MyApplicationContextInitializer注册,代码如下:

org.springframework.context.ApplicationContextInitializer=\geek.springboot.application.contextInitializer.MyApplicationContextInitializer

启动SpringApplication,控制台输出如下,可以看到MyApplicationContextInitializer的加载优先级非常高

image.png

通过SpringApplication注册

除了使用spring.factories注册,也可以通过SpringApplication进行注册,代码如下:

@Slf4j  
@SpringBootApplication
public class Application {  
  
    public static void main(String[] args) {  
        SpringApplication application = new SpringApplication(Application.class);  
        // 注册ApplicationContextInitializer实现
        application.addInitializers(new MyApplicationContextInitializer());  
        application.run(args);  
    }  
  
}

或者换个SpringApplicationBuilder的写法,效果是一样的

@Slf4j  
@SpringBootApplication
public class Application {  
  
    public static void main(String[] args) {  
        SpringApplication application = new SpringApplicationBuilder(Application.class)  
        .initializers(new MyApplicationContextInitializer())  
        .build();  
        application.run(args);  
    }  
  
}

添加Properties

这里演示一下如何在SpringApplication启动时,往应用上下文ApplicationContext中添加属性。

MyApplicationContextInitializer稍作改动:

@Slf4j  
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));  
        log.info("MyApplicationContextInitializer initialize");  
    }  
  
}

为了印证mysql-host这个属性的确加载到ApplicationContext中,这里再注册一个事件监听,在SpringBoot启动完成后尝试获取mysql-host这个属性

@Slf4j  
@SpringBootApplication
public class Application {  
  
    public static void main(String[] args) {  

        SpringApplication application = new SpringApplication(Application.class);  
        // 注册ApplicationContextInitializer
        application.addInitializers(new MyApplicationContextInitializer());  
        // 注册事件监听
        application.addListeners((ApplicationListener<ApplicationReadyEvent>) event -> {  
            // 从当前ApplicationContext中获取mysql-host属性,并打印输出  
            String property = event.getApplicationContext().getEnvironment().getProperty("mysql-host");  
            log.info("mysql-host is {}", property);  
        });  
        application.run(args);  

    }  
  
}

启动SpringApplication,输出如下,证明在SpringApplicaiton启动时动态地往ApplicationContext添加属性成功

image.png

添加配置文件

接下来操作一下如何在SpringApplication启动时,动态添加需要激活的配置文件。

新建application-extend.yml

mysql:  
  user: root  
  password: 123

新建ExtendConfig做为配置映射Java Bean

package geek.springboot.application.configuration;  
  
import lombok.Data;  
import org.springframework.boot.context.properties.ConfigurationProperties;  
import org.springframework.stereotype.Component;  
  
@Data  
@ConfigurationProperties(prefix = "mysql")  
@Component  
public class ExtendConfig {  

    private String user;  

    private String password;  
  
    @Override  
    public String toString() {  
        return "ExtendConfig{" +  
            "user='" + user + '\'' +  
            ", password='" + password + '\'' +  
            '}';  
    }  
  
}

在之前SpringApplicaiton注册的Listener中添加以下代码,在SpringApplicaiton启动准备完成时,从Spring IOC中获取ExtendConfig Bean,并打印输出,以验证application-extend.yml是否真的被加载

application.addListeners((ApplicationListener<ApplicationReadyEvent>) event -> {  
    // 从当前ApplicationContext中获取mysql-host属性,并打印输出  
    String property = event.getApplicationContext().getEnvironment().getProperty("mysql-host");  
    log.info("mysql-host is {}", property);  

    // 从当前ApplicationContext获取ExtendConfig,并打印输出  
    ExtendConfig extendConfig = (ExtendConfig) event.getApplicationContext().getBean("extendConfig");  
    log.info(extendConfig.toString());  
});

这里稍微提一下,application.yml代码如下

spring:  
  profiles:  
    active: default  # 没有激活extend配置文件

接下来就是最关键的MyApplicationContextInitializer改动

@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();
    // 添加application-extend.yml
    environment.addActiveProfile("extend");  
    // 应用新添加的配置文件
    ConfigDataEnvironmentPostProcessor.applyTo(environment);  
    // 打印
    log.info("MyApplicationContextInitializer initialize");  
}

启动SpringApplication,控制台输出如下,证明通过ApplicationContextInitializer动态地添加配置文件操作是成功的

image.png

插曲

如何使用ApplicationContextInitializer动态添加配置文件这一项操作SpringBoot官方文档上并没有仔细说明,百度搜索到的那些无非也只是些简简单单控制台输出的小Demo,那么我是如何知道这样的方式的呢?这里分享一下我是如何思考并找到答案的。

首先官方没仔细提及,Google、百度搜索不到想要的结果,那么就换个思路,ApplicationContextInitializer是个接口,接口必然会有一个或多个实现类,那么就看看别的实现类是否有可供参考的地方。

在IDEA中查看ApplicationContextInitializer相关实现类,看到有个叫ConfigDataApplicationContextInitializer的实现 image.png

同样是配置一些东西,感觉它应该能给我一些参考,那么深入进去查看它源码

image.png

可以看出它的整体逻辑是

  1. 获取environment
  2. 对environment做一些操作
  3. 应用environment的变更,也就是applyTo(xxx)

那么是否这样照着操作就可以了?这里我又想到了SpringBoot提供的在application.yml中根据spring.profiles.active指定要激活的配置的功能。这两者背后的逻辑应该都是类似的。

那么再稍看一下spring.profiles.active的实现

image.png

原来spring.profiles.active对应着的就是org/springframework/boot/context/config/Profiles类的activeProfiles属性。接下来看看都有哪些地方引用了getActive()。

image.png

顺藤摸瓜找到org/springframework/boot/context/config/ConfigDataEnvironment``的applyToEnvironment(xxx)方法。

image.png

再往上找到了applyToEnvironment()的调用方processAndApply()

image.png

再往上找到了org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorpostProcessEnvironment(),这里基本可以确定,处理要激活配置文件是交由ConfigDataEnvironmentPostProcessor进行处理的。

image.png

答案到这里就清楚了,才就有MyApplicationContextInitializer的这段添加配置文件的代码:

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

@Order排序

多个ApplicationContextInitializer实现,可以通过@Order调整它们的优先级

新建OtherApplicationContextInitializer,代码如下:

package geek.springboot.application.contextInitializer;  
  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;  
import org.springframework.context.ApplicationContextInitializer;  
import org.springframework.context.ConfigurableApplicationContext;  
import org.springframework.core.annotation.Order;  
import org.springframework.core.env.ConfigurableEnvironment;  
import org.springframework.core.env.MapPropertySource;  
  
import java.util.HashMap;  
import java.util.Map;  
  
@Slf4j  
@Order(1) // 数值越小,优先级越高  
public class OtherApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {  
  
    @Override  
    public void initialize(ConfigurableApplicationContext applicationContext) {  
        log.info("OtherApplicationContextInitializer initialize...");  
    }  
  
}

MyApplicationContextInitializer添加上@Order注解

@Slf4j  
@Order(2)  // 数值越小,优先级越高
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
 ...
}

SpringApplication注册Initializers

SpringApplication application = new SpringApplication(Application.class);  
application.addInitializers(new MyApplicationContextInitializer());  // MyContextInitializer在先
application.addInitializers(new OtherApplicationContextInitializer()); // OtherContextInitializer在后

启动SpringApplicaiton,输出如下,可以看到,不管添加顺序如何,最先初始化并调用initialize()方法的是OtherApplicationContextInitializer

image.png

源码分析

简单看看SpringApplicaiton.run()中是如何处理ApplicationContextInitializer的

来到run()方法实现,大致扫一下整体流程,看到有一个prepareContext()方法,根据语义来看大概里面封装了一些ApplicationContext初始化前的准备逻辑

image.png

点进去看看,看到了一个applyInitializers(),这个Initializers感觉会不会就是我们的ApplicationContextInitializer

image.png

再点进去看看,Bingo!就是ApplicationContextInitializer,可以看到在prepareContext阶段,SpringApplicaiton就获取了当前所有ApplicationContextInitializer实现,并逐个调用其initialize()方法

image.png

结尾

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

上一篇文章:《SpringBoot核心特性——手写一个自己的starter

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