前言
这次稍微讲讲如何在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的加载优先级非常高
通过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添加属性成功
添加配置文件
接下来操作一下如何在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动态地添加配置文件操作是成功的
插曲
如何使用ApplicationContextInitializer动态添加配置文件这一项操作SpringBoot官方文档上并没有仔细说明,百度搜索到的那些无非也只是些简简单单控制台输出的小Demo,那么我是如何知道这样的方式的呢?这里分享一下我是如何思考并找到答案的。
首先官方没仔细提及,Google、百度搜索不到想要的结果,那么就换个思路,ApplicationContextInitializer是个接口,接口必然会有一个或多个实现类,那么就看看别的实现类是否有可供参考的地方。
在IDEA中查看ApplicationContextInitializer相关实现类,看到有个叫ConfigDataApplicationContextInitializer
的实现
同样是配置一些东西,感觉它应该能给我一些参考,那么深入进去查看它源码
可以看出它的整体逻辑是
- 获取environment
- 对environment做一些操作
- 应用environment的变更,也就是applyTo(xxx)
那么是否这样照着操作就可以了?这里我又想到了SpringBoot提供的在application.yml中根据spring.profiles.active指定要激活的配置的功能。这两者背后的逻辑应该都是类似的。
那么再稍看一下spring.profiles.active的实现
原来spring.profiles.active对应着的就是org/springframework/boot/context/config/Profiles类的activeProfiles属性。接下来看看都有哪些地方引用了getActive()。
顺藤摸瓜找到org/springframework/boot/context/config/ConfigDataEnvironment``的applyToEnvironment(xxx)
方法。
再往上找到了applyToEnvironment()的调用方processAndApply()
再往上找到了org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor
的postProcessEnvironment()
,这里基本可以确定,处理要激活配置文件是交由ConfigDataEnvironmentPostProcessor进行处理的。
答案到这里就清楚了,才就有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
源码分析
简单看看SpringApplicaiton.run()中是如何处理ApplicationContextInitializer的
来到run()方法实现,大致扫一下整体流程,看到有一个prepareContext()
方法,根据语义来看大概里面封装了一些ApplicationContext初始化前的准备逻辑
点进去看看,看到了一个applyInitializers()
,这个Initializers感觉会不会就是我们的ApplicationContextInitializer
再点进去看看,Bingo!就是ApplicationContextInitializer,可以看到在prepareContext阶段,SpringApplicaiton就获取了当前所有ApplicationContextInitializer实现,并逐个调用其initialize()方法
结尾
本文章源自《Learn SpringBoot》专栏,感兴趣的话还请关注点赞收藏.
上一篇文章:《SpringBoot核心特性——手写一个自己的starter》