本文从SpringBoot源码分析 配置文件的加载原理和配置文件的优先级
 
  跟入源码之前,先提一个问题
  SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数(VM Options)传入配置项,为什么通过启动参数传入的配置项会“顶掉”配置文件中的配置?
 
示例
 
application.yml 
server.port: 8888
spring.profiles.active: dev
 
application-dev.yml 
spring.think: hello

 

在IDEA中使用命令行配置项 
VM Options 
-Dserver.port=5555

如下图:

 
启动结果:
Tomcat started on port(s): 5555 (http) with context path ''

同时在application.yml 和 启动参数(VM options)中设置 server.port, 最终采用了 启动参数 中的值。

 
  下面开始从main函数启动处,跟入SpringBoot源码,看看SpringBoot是如何处理的。
 
 
系统说明
JDK:1.8
SpringBoot 版本: 2.0.2.RELEASE
IDE: IntelliJ IDEA 2017
 
 
跟入源码正文 
#ApplicationConfigLoadFlow.java
public static void main(String[] args) {
SpringApplication.run(ApplicationConfigLoadFlow.class, args);
}

 

   从SpringApplication.run 函数开始,一个方法一个方法的跟入源码。需要跟入的方法给与注释或高亮。
 
IDEA 快捷键:
进入方法:  Ctrl + 鼠标左键
光标前进/后退: Ctrl + Shirt + 右方向键/左方向键
 
  依次跟入源码:
#SpringApplication.java
return run(new Class<?>[] { primarySource }, args)
#SpringApplication.java
return new SpringApplication(primarySources).run(args);
 #SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
//跟入
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
configureIgnoreBeanInfo(environment);

进入public ConfigurableApplicationContext run(String... args) 方法后,我们重点看 prepareEnvironment这个方法。

  这个方法之前的源码的从类名和源码注释上知道stopWatch用于计时,上下文context还未初始化,listeners监听器存储了EventPushlingRunListener。
 
  通过IDEA 一行行debug可以看到是在 prepareEnvironment方法执行后,server.port 配置项才被加载入 environment 环境配置中。
  如下图所示。注意:配置文件中的配置还未载入,请先接着往后看。
 
  因此,我们重新打断点跟入prepareEnvironment方法。
 
#SpringApplication.java
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
//跟入
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());

同样的套路,通过debug发现实在getOrCreateEnvironment方法执行后得到server.port的值 

 
#SpringApplication.java
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
if (this.webApplicationType == WebApplicationType.SERVLET) {
//跟入
return new StandardServletEnvironment();
}

虚拟机启动参数的加载 是在StandardServletEnvironment 的实例化过程中完成的。

  跟入StandardServletEnvironment的实例化过程之前,大家需要先了解 Java模板模式 。
  看一下StandardServletEnvironment的类继承关系图(通过IDEA 右键 类名 --> Diagrams --> Show Diagrams Popup 即可显示下图)

 
抽象父类AbstractEnvironment的实例化方法中,调用了可由子类继承的customizePropertySources方法。
 
#AbstractEnvironment.java
public AbstractEnvironment() {
//跟入
customizePropertySources(this.propertySources);
if (logger.isDebugEnabled()) {
logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
}
}

实体化的过程中回过头来调用了子类StandardServletEnvironment的customizePropertySources方法

 
#StandardServletEnvironment.java
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
//跟入
super.customizePropertySources(propertySources);
}

又调用了父类StandardEnvironment的customizePropertySources方法

 
#StandardEnvironment.java
protected void customizePropertySources(MutablePropertySources propertySources) {
//跟入
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

 
  通过IDEA 的变量监听功能,可以看到正是StandardEnvironment类的getSystemProperties()方法获取到了之前设置的虚拟机启动参数server.port的值。
 
  继续跟进去
#AbstractEnvironment.java
public Map<String, Object> getSystemProperties() {
try {
//跟入
return (Map) System.getProperties();
#System.java
public static Properties getProperties() {
SecurityManager sm = getSecurityManager();
if (sm != null) {
sm.checkPropertiesAccess();
} return props;

我们搜索一下有没有什么地方初始化 props


#System.java
private static Properties props;
private static native Properties initProperties(Properties props);

发现了静态方法 initProperties,从方法名上即可知道在类被加载的时候 就初始化了 props, 这是个本地方法,继续跟的话需要看对应的C++代码。

 
  回到StandardEnvironment类的customizePropertySources方法
 
#StandardEnvironment.java
protected void customizePropertySources(MutablePropertySources propertySources) {
//SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: systemProperties
//跟入
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
#MutablePropertySources.java
/**
* Add the given property source object with lowest precedence.
* 添加属性源,并使其优先级最低
*/
public void addLast(PropertySource<?> propertySource) {
 再看一下MutablePropertySources的类注释
 
 * <p>Where <em>precedence</em> is mentioned in methods such as {@link #addFirst}
* and {@link #addLast}, this is with regard to the order in which property sources
* will be searched when resolving a given property with a {@link PropertyResolver}.
*
* addFist 和 add Last 会设置属性源的优先级,
* PropertyResolver解析配置时会根据优先级使用配置源
*
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.1
* @see PropertySourcesPropertyResolver
*/
public class MutablePropertySources implements PropertySources {
 
问题2:
 
  此时我们已经看到虚拟机的启动参数先添加到系统当中,那么后面添加进来的Property Source属性源的优先级是否比 SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME(systemProperties) 属性源的优先级高呢?
 
  回到SpringApplication的prepareEnvironment方法
  同样的debug套路发现listeners.environmentPrepared执行后,application.yml 和 application-dev.yml 两个配置文件的配置项都被加载完成,所以我们继续跟入environmentPrepared方法
 
  在跟入environmentPrepared方法之前,需要了解 Java事件监听机制
 
  跟入environmentPrepared中的源码
 
#SpringApplicationRunListeners.java
public void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
//跟入
listener.environmentPrepared(environment);
}
}
#EventPublishingRunListener.java
public void environmentPrepared(ConfigurableEnvironment environment) {
//广播ApplicationEnvrionmentPreparedEvnet事件
//跟入
this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
this.application, this.args, environment));
}
#SimpleApplicationEventMulticaster.java
public void multicastEvent(ApplicationEvent event) {
//跟入
multicastEvent(event, resolveDefaultEventType(event));
} @Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//注意此时 getApplicationListeners(event, type) 返回结果
//包含 监听器 *ConfigFileApplicationListener*
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
//跟入
invokeListener(listener, event);
}
}
}
#SimpleApplicationEventMulticaster.java
/**
* Invoke the given listener with the given event.
* 调用对应事件的监听者
* @param listener the ApplicationListener to invoke
* @param event the current event to propagate
* @since 4.1
*/
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
//跟入
doInvokeListener(listener, event);
}
} private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
//跟入
listener.onApplicationEvent(event);
}
#ApplicationListener.java
//实现接口的监听器当中,有并跟入ConfigFileApplicationListener的实现
void onApplicationEvent(E event);

#ConfigFileApplicationListener.java
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
//跟入
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
} private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
//跟入:当postProcessor 为 ConfigFileApplicationListener
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
#ConfigFileApplicationListener.java
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
//跟入
addPropertySources(environment, application.getResourceLoader());
} protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
//environment的属性源中包含 systemProperties 属性源 即包含 server.port启动参数
RandomValuePropertySource.addToEnvironment(environment);
//跟入 load()方法
new Loader(environment, resourceLoader).load();
}

跟入load之前,需要了解  java lambda表达式

 
#ConfigFileApplicationListener.java
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
//跟入
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
#ConfigFileApplicationListener.java
private void load(Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
//getSearchLocations()默认返回:
//[./config/, file:./, classpath:/config/, classpath:/]
//即搜索这些路径下的文件
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
//getSearchNames()返回:application
Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
//跟入load(.....)
names.forEach(
(name) -> load(location, name, profile, filterFactory, consumer));
});
}
#ConfigFileApplicationListener.java
private void load(String location, String name, Profile profile,
DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
//name默认为:application,所以这个if分支略过
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile,
filterFactory.getDocumentFilter(profile), consumer);
}
}
}
//this.propertySourceLoaders: PropertiesPropertySourceLoader,YamlPropertySourceLoader
for (PropertySourceLoader loader : this.propertySourceLoaders) {
//PropertiesPropertySourceLoader.getFileExtensions(): properties, xml
//YamlPropertySourceLoader.getFileExtensions(): yml, yaml
for (String fileExtension : loader.getFileExtensions()) {
//location: [./config/, file:./, classpath:/config/, classpath:/]
//name: application
String prefix = location + name;
fileExtension = "." + fileExtension;
//profile: null, dev
//相当于对(location, fileExtension, profile)做笛卡尔积,
//遍历每一种可能,然后加载
//加载文件的细节在loadForFileExtension中完成
loadForFileExtension(loader, prefix, fileExtension, profile,
filterFactory, consumer);
}
}
}

继续跟入 loadForFileExtension 方法,可以了解载入一个配置文件的更多细节。

 
  回到之前的load()方法
 
#ConfigFileApplicationListener.java
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
//跟入
addLoadedPropertySources();
#ConfigFileApplicationListener.java
private void addLoadedPropertySources() {
//destination: 进入ConfigFileApplicationListener监听器前已有的配置
//即destination中包含 systemProperties 配置源
MutablePropertySources destination = this.environment.getPropertySources();
String lastAdded = null;
//loaded: 此次监听通过扫描文件加载进来的配置源
//loaded: application.yml, appcalition-dev.yml
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
//倒序后 loaded: application-dev.yml, application.yml
Collections.reverse(loaded);
//先处理 application-dev.yml
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
//第一次进入: lastAdded:null
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
}
else {
//第一次进入: 把application-dev.yml至于最低优先级
destination.addLast(source);
}
}
else {
//第二次进入:
//让 application.yml 优先级比 application-dev.yml 低
destination.addAfter(lastAdded, source);
}
//第一次遍历结束: lastAdded: application-dev
lastAdded = source.getName();
}
}
}

执行后得到各自的优先级,如下图:

 
   systemProperties优先级高,解析器会优先使用 systemProperties中的 server.port 配置项即 5555 所以最终Tomcat 启动端口是 5555
 
  从中也可以看出,如果application.yml 和 application-dev.yml中有相同的配置项,会优先采用application-dev.yml中的配置项。
  
参考:

从SpringBoot源码分析 配置文件的加载原理和优先级的更多相关文章

  1. SpringBoot源码分析(二)启动原理

    Springboot的jar启动方式,是通过IOC容器启动 带动了Web容器的启动 而Springboot的war启动方式,是通过Web容器(如Tomcat)的启动 带动了IOC容器相关的启动 一.不 ...

  2. 【MyBatis源码分析】Configuration加载(下篇)

    元素设置 继续MyBatis的Configuration加载源码分析: private void parseConfiguration(XNode root) { try { Properties s ...

  3. 【Spring源码分析】Bean加载流程概览

    代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. 很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事 ...

  4. 【Spring源码分析】Bean加载流程概览(转)

    转载自:https://www.cnblogs.com/xrq730/p/6285358.html 代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. ...

  5. Dubbo源码分析之ExtensionLoader加载过程解析

    ExtensionLoader加载机制阅读: Dubbo的类加载机制是模仿jdk的spi加载机制:  Jdk的SPI扩展加载机制:约定是当服务的提供者每增加一个接口的实现类时,需要在jar包的META ...

  6. Android 7.0 Gallery图库源码分析3 - 数据加载及显示流程

    前面分析Gallery启动流程时,说了传给DataManager的data的key是AlbumSetPage.KEY_MEDIA_PATH,value值,是”/combo/{/local/all,/p ...

  7. [ipsec][strongswan] strongswan源码分析--(四)plugin加载优先级原理

    前言 如前所述, 我们知道,strongswan以插件功能来提供各种各样的功能.插件之间彼此相互提供功能,同时也有可能提供重复的功能. 这个时候,便需要一个优先级关系,来保证先后加载顺序. 方法 在配 ...

  8. Spring源码分析:Bean加载流程概览及配置文件读取

    很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事的都是Java Web的工作,对于程序员来说,一个Web项目用到Spring,只是配置一下配置文件而已 ...

  9. 【MyBatis源码分析】Configuration加载(上篇)

    config.xml解析为org.w3c.dom.Document 本文首先来简单看一下MyBatis中将config.xml解析为org.w3c.dom.Document的流程,代码为上文的这部分: ...

随机推荐

  1. BUUCTF-Misc-No.3

    比赛信息 比赛地址:Buuctf靶场 内心os(蛮重要的) 我只想出手把手教程,希望大家能学会然后自己也成为ctf大佬,再来带带我QWQ 文件中的秘密 | SOLVED | 打开文件,winhex照妖 ...

  2. 【DevCloud · 敏捷智库】两种你必须了解的常见敏捷估算方法

    背景 在某开发团队辅导的回顾会议上,团队成员对于优化估计具体方法上达成了一致意见.询问是否有什么具体的估计方法来做估算. 问题分析 回顾意见上大家对本次Sprint的效果做回顾,其中80%的成员对于本 ...

  3. 数据可视化实例(三): 散点图(pandas,matplotlib,numpy)

    关联 (Correlation) 关联图表用于可视化2个或更多变量之间的关系. 也就是说,一个变量如何相对于另一个变化. 散点图(Scatter plot) 散点图是用于研究两个变量之间关系的经典的和 ...

  4. python之class面向对象(进阶篇)

    上一篇<Python 面向对象(初级篇)>文章介绍了面向对象基本知识: 面向对象是一种编程方式,此编程方式的实现是基于对 类 和 对象 的使用 类 是一个模板,模板中包装了多个“函数”供使 ...

  5. Django框架09 /ajax、crsf、settings导入

    Django框架09 /ajax.crsf.settings导入 目录 Django框架09 /ajax.crsf.settings导入 1. ajax概述 2. ajax应用 3. ajax上传文件 ...

  6. 老司机带你玩转面试(2):Redis 过期策略以及缓存雪崩、击穿、穿透

    前文回顾 建议前一篇文章没看过的同学先看下前面的文章: 「老司机带你玩转面试(1):缓存中间件 Redis 基础知识以及数据持久化」 过期策略 Redis 的过期策略都有哪些? 在聊这个问题之前,一定 ...

  7. MnasNet:经典轻量级神经网络搜索方法 | CVPR 2019

    论文提出了移动端的神经网络架构搜索方法,该方法主要有两个思路,首先使用多目标优化方法将模型在实际设备上的耗时融入搜索中,然后使用分解的层次搜索空间,来让网络保持层多样性的同时,搜索空间依然很简洁,能够 ...

  8. three.js 绘制3d地图

    通过地图数据配合three可以做出非常酷炫的地图,在大数据展示中十分常见. 这篇郭先生就来说说使用three.js几何体制作3D地图.在线案例点击原文地址. 地图的数据是各个地图块的点数组,通过THR ...

  9. python numpy库np.percentile用法说明

    在python中计算一个多维数组的任意百分比分位数,此处的百分位是从小到大排列,只需用np.percentile即可…… a = range(1,101) #求取a数列第90%分位的数值 np.per ...

  10. eclipse GIT本地库分支操作

    git分支是一个重要的知识点,平时我们开发主要结合eclipse,idea来操作,今天这贴主要以eclipse来操作git本地库分支,主要内容包括新建分支,切换分支,合并分支,冲突解决,重命名分支,删 ...