Springcloud环境中bootstrap.yml加载原理
如果是Springcloud 项目,一般会将配置中心、注册中心等地址存入bootstrap.yml 中,同时将其他的配置存入配置中心。
也就是说bootstrap.yml 的读取会比较靠前。 下面研究其机制。
1. Springcloud 启动
Springcloud 环境启动过个中有一个重要的listener。BootstrapApplicationListener. 这个listener 在Spring-cloud-context-xxx.jar 中,且在spring.factories 文件自动配置。
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.cloud.bootstrap.BootstrapApplicationListener,\
- 查看org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent 调用时机
将断点下到org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent 方法,查看其调用时机如下:
核心过程如下:
(1).org.springframework.boot.SpringApplication#prepareEnvironment boot启动过程中准备环境
(2).org.springframework.boot.context.event.EventPublishingRunListener#environmentPrepared 广播环境准备事件
(3).org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType) 获取到所有的ApplicationListener, 开始广播
(4) 调用到org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent
- org.springframework.cloud.bootstrap.BootstrapApplicationListener#onApplicationEvent 调用
准备一系列的Sprinigcloud 环境,注册一些PropertySource, 处理SpringApplication、environment 对象等。
核心代码如下:org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
// 重置environment
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
String configLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
Map<String, Object> bootstrapMap = new HashMap<>();
bootstrapMap.put("spring.config.name", configName);
// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap
// will fail
// force the environment to use none, because if though it is set below in the
// builder
// the environment overrides it
bootstrapMap.put("spring.main.web-application-type", "none");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
// 添加一个boostrap的MapPropertySource,后面解析bootstrap.yml 会用到
bootstrapProperties.addFirst(
new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
// 重建一个builder
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(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();
if (builderApplication.getMainApplicationClass() == null) {
// gh_425:
// SpringApplication cannot deduce the MainApplicationClass here
// if it is booted from SpringBootServletInitializer due to the
// absense of the "main" method in stackTraces.
// But luckily this method's second parameter "application" here
// carries the real MainApplicationClass which has been explicitly
// set by SpringBootServletInitializer itself already.
builder.main(application.getMainApplicationClass());
}
if (environment.getPropertySources().contains("refreshArgs")) {
// If we are doing a context refresh, really we only want to refresh the
// Environment, and there are some toxic listeners (like the
// LoggingApplicationListener) that affect global static state, so we need a
// way to switch those off.
builderApplication
.setListeners(filterListeners(builderApplication.getListeners()));
}
builder.sources(BootstrapImportSelectorConfiguration.class);
// builder.run() 相当于会重亲启动ApplicationContext (这一步会启动Springboot环境,加载配置也是在这一步骤进行加载的)
final ConfigurableApplicationContext context = builder.run();
// gh-214 using spring.application.name=bootstrap to set the context id via
// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
// spring.application.name
// during the bootstrap phase.
context.setId("bootstrap");
// Make the bootstrap context a parent of the app context
addAncestorInitializer(application, context);
// It only has properties in it now that we don't want in the parent so remove
// it (and it will be added back later)
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
// 合并propertySource
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
// 返回Context,相当于替换Context
return context;
}
简单理解过程如下:springboot启动-》准备环境-》springcloudlistener操作(1. 设置springcloud环境-》2.builder.run启动springcloud环境(大体类似boot启动)-》3.清理环境中springcloud相关信息-》合并environment,重置context)-》boot其他listener-》boot其他启动逻辑
这里return 之前的environment 如下,如图:
2. bootstrap.yml加载过程
断点查看Springcloud 环境完全启动的environment的相关source :
过程解释:
上面说了org.springframework.cloud.bootstrap.BootstrapApplicationListener#bootstrapServiceContext 方法builder.run(); 会使用上面springcloud 初始化之后的一些环境进行启动,相当于原来springboot环境的启动。 启动完成之后替换Springboot的context和environment 对象。
- builder.run(); 重新启动
-
调用到:org.springframework.boot.SpringApplication#run(java.lang.String...)
-
然后继续调用到:org.springframework.boot.SpringApplication#prepareEnvironment 方法
-
org.springframework.boot.context.config.ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = this.loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
Iterator var3 = postProcessors.iterator();
while(var3.hasNext()) {
EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var3.next();
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
获取所有的环境后置处理器,进行后置处理。这里包括:
- 继续调用到org.springframework.boot.context.config.ConfigFileApplicationListener
这个里面加载重要的配置信息并且进行加载配置。
private static final String DEFAULT_PROPERTIES = "defaultProperties";
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
private static final String DEFAULT_NAMES = "application";
private static final Set<String> NO_SEARCH_NAMES = Collections.singleton((Object)null);
private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);
private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
private static final Set<String> LOAD_FILTERED_PROPERTY;
public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
public static final int DEFAULT_ORDER = -2147483638;
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
this.getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = isFolder ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMES;
names.forEach((name) -> {
this.load(location, name, profile, filterFactory, consumer);
});
});
}
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
Iterator var13 = this.propertySourceLoaders.iterator();
PropertySourceLoader loader;
do {
if (!var13.hasNext()) {
throw new IllegalStateException("File extension of config file location '" + location + "' is not known to any PropertySourceLoader. If the location is meant to reference a directory, it must end in '/'");
}
loader = (PropertySourceLoader)var13.next();
} while(!this.canLoadFileExtension(loader, location));
this.load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
} else {
Set<String> processed = new HashSet();
Iterator var7 = this.propertySourceLoaders.iterator();
while(var7.hasNext()) {
PropertySourceLoader loaderx = (PropertySourceLoader)var7.next();
String[] var9 = loaderx.getFileExtensions();
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String fileExtension = var9[var11];
if (processed.add(fileExtension)) {
this.loadForFileExtension(loaderx, location + name, "." + fileExtension, profile, filterFactory, consumer);
}
}
}
}
}
private Set<String> getSearchNames() {
if (this.environment.containsProperty("spring.config.name")) {
String property = this.environment.getProperty("spring.config.name");
return this.asResolvedSet(property, (String)null);
} else {
return this.asResolvedSet(ConfigFileApplicationListener.this.names, "application");
}
}
org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#load传入的参数如下:name = bootstrap, location=classpath:/
这里的核心逻辑是
(1). 获取搜索目录searchLocations,成员变量设置的默认值;
(2). 获取搜索的name; 如果包含spring.config.name ( 就获取cloud启动设置的bootstrap);如果不包含就获取application.xxx (这里也就是为什么bootstrap.yml 优先获取)。
当springcloud 环境启动完成builder.run()执行完之后, 有一个remove bootstrap 的操作。 所以springcloud 环境启动完成之后相当于删除掉标记,然后合并context和environment 之后继续springboot 的启动。
(3). 然后遍历当前的loaders(也就是property和yml加载器),然后获取当前loader 支持的后缀(property),最后拼接传递参数让loader 去依次到指定位置获取。支持的后缀如下:(简单理解就是调用方给一个文件的basename和location,比如bootstrap,这里会分别调用两个loader 两次,然后传递不同的后缀去指定路径找文件加载。这也是为什么properties\yml\yaml都支持的原因。)
org.springframework.boot.env.PropertiesPropertySourceLoader#getFileExtensions
public String[] getFileExtensions() {
return new String[]{"properties", "xml"};
}
org.springframework.boot.env.YamlPropertySourceLoader#getFileExtensions
public String[] getFileExtensions() {
return new String[]{"yml", "yaml"};
}
-
继续调用到:org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loadDocuments
-
继续调用到: org.springframework.boot.env.YamlPropertySourceLoader#load 解析classpath:./bootstrap.yml
-
创建一个OriginTrackedMapPropertySource然后加入到
public OriginTrackedMapPropertySource(String name, Map source, boolean immutable) { super(name, source); this.immutable = immutable; }
这里传入的参数如下:
这里就完成了加载bootstrap.yaml。
可能不通的Springboot 加载的类以及后置处理器不一样,但大体思路都是这样。
springoot 启动-》environment准备-》广播事件-》获取EnvironmentPostProcessor 环境后置处理器-》执行org.springframework.boot.env.EnvironmentPostProcessor#postProcessEnvironment 方法进行初始化配置加载(可能不同的版本采用不通的后置处理器处理)。