SpringCloud应用启动流程分析(一)

  SpringCloud想必大家都不陌生了,它为开发人员提供了在分布式系统中快速构建一些常见模式的工具,比如配置管理、服务发现、断路器和负载均衡等等。所谓工欲善其事必先利其器,今天我们就来拆解一下SpringCloud的工作流程吧(基于spring-cloud-Greenwich.SR2)。

  需要指出的是,SpringCloud是基于SpringBoot构建的,阅读本文需要小伙伴们对SpringBoot的工作流程有所了解,不熟悉的小伙伴可以先看看下面两篇文章获得一些前置知识。

  1. SpringBoot 应用启动流程分析
  2. SpringBoot 自动装配机制分析

从 bootstrap.yml 说起

  除了application.ymlSpringCloud应用还可以额外提供一个配置文件,称作bootstrap.yml。大家一般会将服务名、注册中心以及配置中心的地址配置在这里,那么它是由谁加载的呢?

1
2
3
4
# 截取自 spring-cloud-commons reference doc, chapter 1.1
TThe bootstrap context uses a different convention for locating external configuration than the main
application context. Instead of application.yml (or .properties), you can use bootstrap.yml, keeping
the external configuration for bootstrap and main context nicely separate.

SpringCloud的文档提到,bootstrap.yml是由bootstrap context加载的,使用bootstrap.yml可以很好地将用于引导的外部配置和main context分离。这里的main context自然是SpringBoot应用启动时创建的应用上下文了,bootstrap context又是什么呢?

1
2
3
4
# 截取自 spring-cloud-commons reference doc, chapter 1.1
A Spring Cloud application operates by creating a “bootstrap” context, which is a parent context for
the main application. This context is responsible for loading configuration properties from the external
sources and for decrypting properties in the local external configuration files.

文档里同样有提到,SpringCloud应用会在启动时创建bootstrap context,并将它作为main context的父容器。bootstrap context的作用主要有两个:

  1. 加载来自外部的配置项
  2. 解密本地外部配置文件中的属性

这第二点可能不那么多见,第一点换个说法大家会更清楚——从配置中心拉数据。到这里想必是什么大家已经有概念了,那为什么呢?也就是具体是怎么实现的呢?别急,往下看哈。

bootstrap context

spring-cloud-bootstrap-pkg

  上图就是我们的起点啦——BootstrapApplicationListener,看一下它的定义:

1
2
3
4
5
// 不要问我怎么定位到这里的
public class BootstrapApplicationListener
implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
// omitted...
}

首先,它是一个ApplicationListener,其次它监听的事件是ApplicationEnvironmentPreparedEvent。之前分析SpringBoot应用启动流程的时候,我们说过发出该事件的时机是Environment已经初始化但main context还未创建。好了,既然是监听器,看看它对事件的处理呗。

onApplicationEvent(…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
// SpringBoot 应用启动时创建的 Environment
ConfigurableEnvironment environment = event.getEnvironment();
// 首先检查此特性是否开启
if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class, true)) {
return;
}

// IMPORTANT: 防重入
// Spring 自动装配机制并不支持将 ApplicationListener 指定给某个单独的 ApplicationContext
// 因此创建 bootstrap context 和 main context 时都会回调这个 ApplicationListener
// 显然,创建 main context 时才是我们感兴趣的时间节点
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return;
}

ConfigurableApplicationContext context = null;
// bootstrap 只是 bootstrap context 使用的配置文件的默认名称
// 可以在 System Properties 中进行指定,比如 -Dspring.cloud.bootstrap.name=some-file-name
String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");

// 看看开发者是否有打算给 main context 设置父容器,一般是没有的
for (ApplicationContextInitializer<?> initializer : event.getSpringApplication().getInitializers()) {
// ParentContextApplicationContextInitializer 是专门负责设置父容器的
if (initializer instanceof ParentContextApplicationContextInitializer) {
// 有的话返回父容器,如果它的 id 是 bootstrap
// 否则返回父容器的父容器,也就是 mainContext.getParent().getParent()
context = findBootstrapContext(
(ParentContextApplicationContextInitializer) initializer, configName);
}
}

// 开发者没有自行配置 bootstrap context...
if (context == null) {
// 那就创建它,将它设置为 main context 的父容器
context = bootstrapServiceContext(environment, event.getSpringApplication(), configName);
// 给 main context 注册监听器,在其启动失败时关闭 bootstrap context
event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));
}
}

分析onApplicationEvent(...)可以知道,bootstrap-context所使用的配置文件名称是可以任意指定的,默认是bootstrap。注意,这个名称并不能在application.yml中指定,这是因为application.yml是由main context加载的(对这个过程感兴趣的话可以看看ConfigFileApplicationListener)。一般情况下我们也不会配置ParentContextApplicationContextInitializer来给main context设置父容器,重头戏自然落在bootstrapServiceContext(...)上了。

bootstrapServiceContext(…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application, String configName) {
// 创建一个新的 Environment,默认会注册 systemEnvironment 和 systemProperties 这两个 PropertySource
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
// 清空默认注册的 PropertySource ,得到一个完全空白的 Environment
MutablePropertySources bootstrapProperties = bootstrapEnvironment.getPropertySources();
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}

// 组装 bootstrap property source
// 这一步是为了交给 ConfigFileApplicationListener 来读取
// bootstrap.yml(properties) 中的内容
Map<String, Object> bootstrapMap = new HashMap<>();
// ${spring.cloud.bootstrap.name:bootstrap}
bootstrapMap.put("spring.config.name", configName);
// bootstrap context 使用最基础的类型就好
bootstrapMap.put("spring.main.web-application-type", "none");

// bootstrap.yml 的路径也是可配的,默认为空,即 resources 根目录了
String configLocation = environment.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
// 添加 bootstrap property source,前边 onApplicationEvent(...) 就用到它来防重入了
bootstrapProperties.addFirst(new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));

// 将原 Environment 中的数据同步到这个新创建的 Environment
for (PropertySource<?> source : environment.getPropertySources()) {
// StubPropertySource 是用来占位的,不包含真实数据
// 这一节可以看看 WebApplicationContext 的启动过程
// 其中 ServletContextPropertySource 就是先进行占位再替换的
if (source instanceof PropertySource.StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}

// bootstrap context 算是一个后台的上下文
// 有些配置项是要调整的,比如不用打印 Banner
// 也不需要使用能启动 WebServer 的 ApplicationContext
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles())
.bannerMode(Banner.Mode.OFF)
.environment(bootstrapEnvironment)
// Don't use the default properties in this builder
.registerShutdownHook(false)
.logStartupInfo(false)
.web(WebApplicationType.NONE);
final SpringApplication builderApplication = builder.application();
// 使用 war 包部署的情况,无法推断 mainApplicationClass
if (builderApplication.getMainApplicationClass() == null) {
builder.main(application.getMainApplicationClass());
}

// 如果正在进行 context refresh
// 这里所说的 context refresh 和 RefreshScope 有关
// 并不是 ApplicationContext#refresh()
// 关于这部分的内容,我们下篇文章再说
if (environment.getPropertySources().contains("refreshArgs")) {
// 关于日志的监听器,影响的是全局的状态
// 但是一般做 context refresh 时只是希望刷新 Environment
// 所以这里要把这类监听器过滤掉
builderApplication.setListeners(filterListeners(builderApplication.getListeners()));
}

// 允许使用 SPI 机制单独给 bootstrap context 指定配置类
builder.sources(BootstrapImportSelectorConfiguration.class);
// 创建 bootstrap context,走一遍自动装配流程
// 走到 ConfigFileApplicationListener,读取 bootstrap.yml 的内容...
// 走到 PropertySourceLocator,加载外部配置项...
final ConfigurableApplicationContext context = builder.run();
// 指定其 id 为 bootstrap
context.setId("bootstrap");
// 监听 main context 的创建,将 bootstrap context 设置为其父容器
addAncestorInitializer(application, context);
// bootstrap context 创建完成以后,这些配置项就没用了
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
// 因为 bootstrap context 很可能读取了新的配置项了
// 将这些新数据同步给 main context 的 Environment
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}

不要慌,方法虽长,所做的事情却不算多:

  1. 创建bootstrap context,设置其为main context的父容器
  2. 支持单独为bootstrap context指定配置类
  3. bootstrap environmentmain environment中的数据进行同步

  首先添加的名为BOOTSTRAP_PROPERTY_SOURCE_NAMEPropertySource是为了帮助ConfigFileApplicationListener定位并读取bootstrap.yml(properties)中的配置项。由于这部分内容不是本篇的重点,就劳烦大家自己去读一读它的源码了。

  其次是创建bootstrap context,注意这一步复用了SpringBoot应用的启动流程,诸如自动配置什么的也统统会给bootstrap context安排上。由于自动装配机制并不能精确到可以指定某个自动配置类仅用于某个ApplicationContext,因此要特别注意ApplicationListenerApplicationContextInitializer这类组件的重入问题。提前剧透一下,加载外部配置项正是作为ApplicationContextInitializer来实现的,这个话题我们下节再谈,现在只需要知道执行完builder.run()创建好bootstrap context的同时,也会通过PropertySourceLocator去加载外部配置项。

  如果想单独给bootstrap context引入一些配置类怎么办?这个问题可以通过Spring SPI机制来解决,相信大家看一眼BootstrapImportSelectorConfiguration的源码就明白了,不多说了。

  最后是将bootstrap environmentmain environment中的数据进行同步。因为bootstrap context在创建的过程中可能已经加载了很多的外部配置项,而应用程序代码并不能直接注入bootstrap environment,想要使用这部分配置项的话就只能将它合并进main environment了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void mergeDefaultProperties(MutablePropertySources environment,
MutablePropertySources bootstrap) {
// 同步 DEFAULT_PROPERTIES,篇幅所限,这里删掉了

mergeAdditionalPropertySources(environment, bootstrap);
}

private void mergeAdditionalPropertySources(MutablePropertySources environment,
MutablePropertySources bootstrap) {
// 将 main environment 中的 DEFAULT_PROPERTIES 取出来
PropertySource<?> defaultProperties = environment.get(DEFAULT_PROPERTIES);
// 重新包装成 ExtendedDefaultPropertySource
ExtendedDefaultPropertySource result = defaultProperties instanceof ExtendedDefaultPropertySource
? (ExtendedDefaultPropertySource) defaultProperties
: new ExtendedDefaultPropertySource(DEFAULT_PROPERTIES,
defaultProperties);
// 遍历 bootstrap environment 中的数据
for (PropertySource<?> source : bootstrap) {
// 如果是 main environment 没有
if (!environment.contains(source.getName())) {
// 就添加,如果就可以通过 main environment 来访问这些外部配置了
result.add(source);
}
}
// 以下是保证两个 environment 中的数据结构一致
for (String name : result.getPropertySourceNames()) {
bootstrap.remove(name);
}
addOrReplace(environment, result);
addOrReplace(bootstrap, result);
}

如此这般之后,就可以通过main environment来访问由bootstrap context加载的外部配置项了,最后看一眼ExtendedDefaultPropertySource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static class ExtendedDefaultPropertySource extends SystemEnvironmentPropertySource {

// rest are omitted...

@Override
public Object getProperty(String name) {
// 先读取外部配置项
if (this.sources.containsProperty(name)) {
return this.sources.getProperty(name);
}
// 再读取由 main context 加载的
return super.getProperty(name);
}

@Override
public boolean containsProperty(String name) {
// 同样先判断是否存在于外部配置项
if (this.sources.containsProperty(name)) {
return true;
}
// 再考虑自身
return super.containsProperty(name);
}
}

可以看到这里定义了一个明确的访问顺序:外部配置项的优先级高于本地配置项。

PropertySourceLocator

1
2
3
4
5
6
7
8
/**
* Strategy for locating (possibly remote) property sources for the Environment.
* Implementations should not fail unless they intend to prevent the application from
* starting.
*/
public interface PropertySourceLocator {
PropertySource<?> locate(Environment environment);
}

显然这是一个扩展接口,交给开发者来实现的,SpringCloud本身只负责调用。很容易就能得知这个调用逻辑是在PropertySourceBootstrapConfiguration中实现的,ok let's go

1
2
3
4
5
6
7
8
9
// 翻看 spring.factories 可以知道,这个配置类是专门指定给 bootstrap context 加载的
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();

// rest are omitted...
}

它是一个ApplicationContextInitializer,嗯,可以直接去看对应的接口实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 包装类,用来容纳所有由 PropertySourceLocator 加载的配置
CompositePropertySource composite = new CompositePropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME);
// 排个序先
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
// 此 environment 为 bootstrap environment
ConfigurableEnvironment environment = applicationContext.getEnvironment();
// 遍历 PropertySourceLocator
for (PropertySourceLocator locator : this.propertySourceLocators) {
PropertySource<?> source = null;
// 逐个获取外部配置
source = locator.locate(environment);
if (source == null) {
continue;
}
// 有的话就合并到一起
logger.info("Located property source: " + source);
composite.addPropertySource(source);
empty = false;
}
// 如果加载到了外部配置
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
// 移除旧的
if (propertySources.contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
}
// 添加新的,这里可以根据配置调整加入的顺序,从而影响优先级
insertPropertySources(propertySources, composite);
// 重新加载日志组件,如果日志配置文件有变化的话
reinitializeLoggingSystem(environment, logConfig, logFile);
// 根据配置项 logging.level 重新设置对应包的日志级别
setLogLevels(applicationContext, environment);
// 根据配置项 spring.profiles.include 重新加载额外的配置文件
handleIncludedProfiles(environment);
}
}

主体逻辑还是比较易懂的,只不过要考虑到很多细节,比如日志配置文件变动了,需要重新初始化日志组件;再比如spring.profiles.include配置项变动了,需要加载这些额外的配置文件等等,这些细节就留给大家自己去看吧。最后,大家如果有使用ZookeeperNacos作为配置中心的话,可以看看它们对PropertySourceLocator的实现。

End

  今天和大家分享了bootstrap context的启动过程,相信大家看完一定有所收获。下次我们聊聊RefreshScope,这可是个非常好玩的特性,完。