Debug Spring web环境时了解到在启动期间创建了两个容器: Spring IOC容器和Web容器,其中Web容器是Spring IOC 的子容器。Debug SpringBoot源码时了解到 SpringBoot启动期间只创建了一个Web容器。今天在学习Feign源码时Debug到FeignContext时突然看到FeignContext的父容器是SpringBoot创建的Web容器,打开Web容器一看发现Web容器也有一个父容器: BootStrap Context。突然乱了阵脚,难道SpringBoot在启动期间创建了两个容器?带着这个问题又Debug了下SpringBoot的源码,便有了今天这篇文章,Mark一下。
带着上面遇到的疑惑,我分别在SpringCloud环境和SpringBoot环境下分别对SpringBoot启动流程Debug了一遍。功夫不负有心人,两个环境一对比变发现了其中的奥妙。在SpringBoot环境下Debug并没有发现什么异常(差点以为是之前研究SpringBoot时偷懒了呢),当在SpringCloud 环境下发现了与SpringBoot环境的差异。
SpringBoot通过Main方法进行启动:
该启动过程分为两步:第一步:包装SpringApplication; 第二步:使用包装好的SpringApplication调用run()进行启动。
public static void main(Stirng[] args) {
SpringApplication.run(TestApplication.class, args);
}
在包装SpringApplication这步中发现了一处差异:
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
SpringBoot环境 : 获取到12个ApplicationListener
SpringCloud环境: 获取到13个ApplicationListener
其中多出来的一个监听器为 : BootstrapApplicationListener,在IDEA全文中搜索该监听器发现它在 spring-cloud-context包中。既然找到了差异,那么接着往下看哪里使用了这边扫描出来的 ApplicationListener.
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = deduceWebApplicationType();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
//通过Spring SPI方式获取所有的 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
通过Debug 发现,在run方法中调用的 prepareEnvironment()方法的listeners.environmentPrepared(environment); 中获取到l BootStrapApplicationListener.
我们重点看看这个BootStrapApplicationListener到底做了什么:
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
//获取到当前的环境变量
ConfigurableEnvironment environment = event.getEnvironment();
//如果为开启SpringCloud直接返回
if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
true)) {
return;
}
//判断该监听器是否已经执行过,如果执行过会直接返回
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return;
}
ConfigurableApplicationContext context = null;
//设置读取bootstrap文件
String configName = environment
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
.getInitializers()) {
if (initializer instanceof ParentContextApplicationContextInitializer) {
context = findBootstrapContext(
(ParentContextApplicationContextInitializer) initializer,
configName);
}
}
//创建一个新的Spring容器
if (context == null) {
context = bootstrapServiceContext(environment, event.getSpringApplication(),
configName);
}
apply(context, event.getSpringApplication(), environment);
}
bootstrapServiceContext方法创建了一个新的Context:
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
//设置 bootstrap 文件路径
String configLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
Map<String, Object> bootstrapMap = new HashMap<>();
bootstrapMap.put("spring.config.name", configName);
bootstrapMap.put("spring.main.web-application-type", "none");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
//设置是否已经初始化bootstrap环境
bootstrapProperties.addFirst(
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
List<String> names = new ArrayList<>(SpringFactoriesLoader
.loadFactoryNames(BootstrapConfiguration.class, classLoader));
for (String name : StringUtils.commaDelimitedListToStringArray(
environment.getProperty("spring.cloud.bootstrap.sources", ""))) {
names.add(name);
}
//使用SpringApplicationBuilder手动创建一个新的容器
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
.environment(bootstrapEnvironment)
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
//省略部分代码。。。。
//手动调用 SpringApplication.run()方法实例化新创建的容器
final ConfigurableApplicationContext context = builder.run();
context.setId("bootstrap");
// 创建祖先容器
addAncestorInitializer(application, context);
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}
private void addAncestorInitializer(SpringApplication application,
ConfigurableApplicationContext context) {
boolean installed = false;
//遍历所有的initializer,判断是否已经存在 祖先initializer
for (ApplicationContextInitializer<?> initializer : application
.getInitializers()) {
if (initializer instanceof AncestorInitializer) {
installed = true;
//如果存在,则设置 bootStrapApplication
((AncestorInitializer) initializer).setParent(context);
}
}
//如果不存在,则创建。
if (!installed) {
application.addInitializers(new AncestorInitializer(context));
}
}
这里主要是创建AncestorInitializer对象。
当BootStrap环境初始化完毕后,再次回到SpringBoot初始化流程会触发所有的initializers,当执行AncestorInitializer时,将BootStrap ApplicationContext容器设为父容器:
private static class AncestorInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private ConfigurableApplicationContext parent;
public AncestorInitializer(ConfigurableApplicationContext parent) {
this.parent = parent;
}
@Override
public void initialize(ConfigurableApplicationContext context) {
//如果已经存在父容器,则直接取出
while (context.getParent() != null && context.getParent() != context) {
context = (ConfigurableApplicationContext) context.getParent();
}
reorderSources(context.getEnvironment());
//执行这一步:设置父容器
new ParentContextApplicationContextInitializer(this.parent)
.initialize(context);
}
}
上述方法将设置父容器的逻辑委托给ParentContextApplicationContextInitializer
类处理,来看下initialize
方法:
public class ParentContextApplicationContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private int order = Ordered.HIGHEST_PRECEDENCE;
private final ApplicationContext parent;
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (applicationContext != this.parent) {
//将当前手动创建的BootStrap容器设置为父容器
applicationContext.setParent(this.parent);
//创建监听器,主要用来发布项目中存在父子容器事件
applicationContext.addApplicationListener(EventPublisher.INSTANCE);
}
}
}
BootStrap Application 容器的作用: 提前加载SpringCloud 相关的配置类,比如BootStrap Application会提前加载配置中心相关配置类,优先加读取bootstrap
配置文件等逻辑。
首先,SpringBoot
项目是通过SpringApplication启动,在上述逻辑中又构建了一个
SpringApplicationBuilder`对象,再次执行run方法,所以启动流程会执行两遍,只是读取的配置文件和配置类不同。所以在SpringCloud环境下run()方法会执行两遍。
同样,当第二次创建SpringApplicationBuilder
并启动时,会不会再次出发监听器,然后接着创建SpringApplicationBuilder
呢? 肯定不会。否则就是死循环了。上面已经提到了,SpringCloud通过标识符BOOTSTRAP_PROPERTY_SOURCE_NAME
来判断。监听器执行之后,会设置该变量对应值,下次启动前如果有值,表明已经执行。
在SpringBoot环境只创建了一个WEB容器,在SpringCloud环境下创建了两个容器: Bootstrap父容器和Web子容器。所以在SpringCloud环境下容器结构是这样的:
需要注意的是SpringCloud环境下会执行两次 SpringApplication的run()方法。如果要对SpringBoot进行扩展的话需要保持自定义逻辑是支持幂等的。
流程如下:
1. Main方法启动SpringBoot
2. 调用SpringApplication.run()方法
3. 扫描到 BootStrapApplicationListener
4. 调用BootStrapApplicationListener#onApplicationEvent()
5. 判断enviroment中是否可以取到 “bootstrap” 属性,若有则直接return返回
6. 向enviroment塞入 “bootstrap” 标识已经执行过该监听器
7. 手动创建新的SpringApplication()并调用run方法
8. 扫描到 BootStrapApplicationListener
9. 调用BootStrapApplicationListener#onApplicationEvent()
10. enviroment可以取到 “bootstrap” 标识则判断已经执行过了该监听器,直接return
11. 执行第二次run()方法后续流程完成Bootstrap容器创建
12. 设置BootStrap容器为祖先容器
13. 回到第3步走第一次run()方法扫描到 BootStrapApplicationListener的地方执行后续流程
14. 完成WEB容器启动