微信公衆號:吉姆餐廳ak 學習更多源碼知識,歡迎關注。 web
![]()
SpringBoot2 | SpringBoot啓動流程源碼分析(一)spring
SpringBoot2 | SpringBoot啓動流程源碼分析(二)bootstrap
SpringBoot2 | @SpringBootApplication註解 自動化配置流程源碼分析(三)springboot
SpringBoot2 | SpringBoot Environment源碼分析(四)bash
SpringBoot2 | SpringBoot自定義AutoConfiguration | SpringBoot自定義starter(五)微信
SpringBoot2 | SpringBoot監聽器源碼分析 | 自定義ApplicationListener(六)app
SpringBoot2 | 條件註解@ConditionalOnBean原理源碼深度解析(七)ide
SpringBoot2 | Spring AOP 原理源碼深度剖析(八)工具
SpringBoot2 | SpingBoot FilterRegistrationBean 註冊組件 | FilterChain 責任鏈源碼分析(九)源碼分析
SpringBoot2 | BeanDefinition 註冊核心類 ImportBeanDefinitionRegistrar (十)
SpringBoot2 | Spring 核心擴展接口 | 核心擴展方法總結(十一)
本篇文章主要介紹
springBoot2.x
配置文件解析流程,另外會涉及SpringBoot2.x
在environment
處理邏輯上相對於SpringBoot1.x
的變更。 springCloud的配置文件解析,則是在此基礎上作了擴展。在springBoot解析邏輯以前,添加了bootstrap
配置,經過監聽器BootstrapApplicationListener
實現。後續有詳細介紹。
Environment
是 spring 爲運行環境提供的高度抽象接口,項目運行中的全部相關配置都基於此接口。 springBoot
對此接口作了擴展。 先來看一個簡單的SpringBoot
應用。
@org.springframework.boot.autoconfigure.SpringBootApplication
@RestController
public class SpringBootApplication {
@Autowired
Environment environment;
@RequestMapping(value = "/environment", method = RequestMethod.GET)
public String environment() {
//輸出environment 類型
System.out.println(environment.getClass());
return JSON.toJSONString(environment);
}
}
複製代碼
上述代碼注入的environment
具體對象是什麼呢? 跟着源碼,搜尋答案。
前面springBoot
啓動流程中,咱們提到了有個prepareEnvironment
方法:
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
複製代碼
private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
//一、初始化environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
//二、加載默認配置
configureEnvironment(environment, applicationArguments.getSourceArgs());
//三、通知環境監聽器,加載項目中的配置文件
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (this.webApplicationType == WebApplicationType.NONE) {
environment = new EnvironmentConverter(getClassLoader())
.convertToStandardEnvironmentIfNecessary(environment);
}
ConfigurationPropertySources.attach(environment);
return environment;
}
複製代碼
接下來對上面三步進行詳細分析:
一、初始化environment
:
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
//springboot應用返回的environment
if (this.webApplicationType == WebApplicationType.SERVLET) {
return new StandardServletEnvironment();
}
return new StandardEnvironment();
}
複製代碼
能夠看到根據類型進行匹配 environment
,獲取到StandardServletEnvironment
,該實例直接注入到spring容器,因此上面示例代碼的輸出的類型就是StandardServletEnvironment
。
StandardServletEnvironment
是整個springboot
應用運行環境的實現類,後面全部關於配置和環境的操做都基於此類。看一下該類的結構:
StandardServletEnvironment
的初始化一定會致使父類方法的初始化:
AbstractEnvironment
:
public AbstractEnvironment() {
//從名字能夠看出加載咱們的自定義配置文件
customizePropertySources(this.propertySources);
if (logger.isDebugEnabled()) {
logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
}
}
複製代碼
在構造方法中調用自定義配置文件,spring的一向作法,模板模式,調用的是實例對象的自定義邏輯:
@Override
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);
}
複製代碼
由於該配置類是基於web環境,因此先加載和 servlet有關的參數,addLast
放在最後:
/** System environment property source name: {@value} */
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/** JVM system properties property source name: {@value} */
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
複製代碼
該類又是對StandardEnvironment
的擴展,這裏會調用super.customizePropertySources(propertySources);
:
@Override
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()));
}
複製代碼
/** System environment property source name: {@value} */
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/** JVM system properties property source name: {@value} */
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
複製代碼
能夠看到放入順序是永遠放在最後面,也就是先加入的在前面。systemEnvironment
是在systemProperties
前面,這點很重要。由於前面的配置會覆蓋後面的配置,也就是說系統變量中的配置比系統環境變量中的配置優先級更高。以下:
2)加載默認配置:
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
//加載啓動命令行配置屬性
configurePropertySources(environment, args);
//設置active屬性
configureProfiles(environment, args);
}
複製代碼
這裏接收的參數是ConfigurableEnvironment
,也就是StandardServletEnvironment
的父類。 繼續跟進configurePropertySources
方法:
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
//獲取配置存儲集合
MutablePropertySources sources = environment.getPropertySources();
//判斷是否有默認配置,默認爲空
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties));
}
//加載命令行配置
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
"springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
複製代碼
上述代碼主要作兩件事: 一是判斷SpringBootApplication
是否指定了默認配置, 二是加載默認的命令行配置。
上面有個核心關鍵類出現了,MutablePropertySources
,mutable中文是可變的意思,該類封裝了屬性資源集合:
public class MutablePropertySources implements PropertySources {
private final Log logger;
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
}
複製代碼
該類又是如何使用的呢?
MutablePropertySources
傳遞到文件解析器propertyResolver
中,同時AbstractEnvironment
又實現了文件解析接口ConfigurablePropertyResolver
,因此AbstractEnvironment
就有了文件解析的功能。因此StandardServletEnvironment
文件解析功能實際委託給了PropertySourcesPropertyResolver
來實現。
繼續看一下configureProfiles(environment, args);
方法:
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
environment.getActiveProfiles(); // ensure they are initialized
// But these ones should go first (last wins in a property key clash)
Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}
複製代碼
該方法主要將SpringBootApplication
中指定的additionalProfiles
文件加載到environment
中,通常默認爲空。 該變量的用法,在項目啓動類中,須要顯示建立SpringApplication
實例,以下:
SpringApplication springApplication = new SpringApplication(MyApplication.class);
//設置profile變量
springApplication.setAdditionalProfiles("prd");
springApplication.run(MyApplication.class,args);
複製代碼
三、通知環境監聽器,加載項目中的配置文件
觸發監聽器:
listeners.environmentPrepared(environment);
複製代碼
在SpringBoot2 | SpringBoot啓動流程源碼分析(一)中提到了該方法通知的監聽器,和配置文件有關的監聽器類型爲ConfigFileApplicationListener
,監聽到事件時執行的方法:
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
//加載項目中的配置文件
addPropertySources(environment, application.getResourceLoader());
configureIgnoreBeanInfo(environment);
bindToSpringApplication(environment, application);
}
複製代碼
繼續跟進去,會發現一個核心內部類 Loader ,配置文件加載也就委託給該內部類來處理:
private class Loader {
private final Log logger = ConfigFileApplicationListener.this.logger;
//當前環境
private final ConfigurableEnvironment environment;
//類加載器,能夠在項目啓動時經過 SpringApplication 構造方法指定,默認採用 Launcher.AppClassLoader加載器
private final ResourceLoader resourceLoader;
//資源加載工具類
private final List<PropertySourceLoader> propertySourceLoaders;
//LIFO隊列
private Queue<String> profiles;
//已處理過的文件
private List<String> processedProfiles;
private boolean activatedProfiles;
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
//獲取類加載器
this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
: resourceLoader;
//獲取propertySourceLoaders
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
PropertySourceLoader.class, getClass().getClassLoader());
}
//......
}
複製代碼
上面propertySourceLoaders
經過 SpringFactoriesLoader 獲取當前項目中類型爲 PropertySourceLoader 的全部實現類,默認有兩個實現類,以下圖:
繼續來看主要解析方法:load()
:
public void load() {
this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
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();
}
複製代碼
跟進去上面初始化方法:
private void initializeProfiles() {
Set<Profile> initialActiveProfiles = initializeActiveProfiles();
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
//若是爲空,添加默認的profile
if (this.profiles.isEmpty()) {
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
if (!this.profiles.contains(defaultProfile)) {
this.profiles.add(defaultProfile);
}
}
}
// The default profile for these purposes is represented as null. We add it
// last so that it is first out of the queue (active profiles will then
// override any settings in the defaults when the list is reversed later).
//這裏添加一個爲null的profile,主要是加載默認的配置文件
this.profiles.add(null);
}
複製代碼
上面主要作了兩件事情:
1)判斷是否指定了profile,若是沒有,添加默認環境:default。後面的解析流程會解析default
文件,好比:application-default.yml、application-default.properties
。
注意:在第2步中咱們提到了
additionalProfiles
屬性,若是咱們經過該屬性指定了profile,這裏就不會加載默認的配置文件,根據咱們指定的profile進行匹配。
2)添加一個null的profile,主要用來加載沒有指定profile的配置文件,好比:application.properties 由於 profiles 採用了 LIFO 隊列,後進先出。因此會先加載profile爲null的配置文件
,也就是匹配application.properties、application.yml
。
繼續跟進解析方法load
:
private void load(Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
//獲取默認的配置文件路徑
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
//循環加載
names.forEach(
(name) -> load(location, name, profile, filterFactory, consumer));
});
}
複製代碼
能夠看到springBoot2.0底層的新的改動都是基於
lambda
表達式實現。
繼續跟進getSearchLocations()
方法:
獲取路徑以後,會拼接配置文件名稱,選擇合適的yml
或者properties
解析器進行解析: (name) -> load(location, name, profile, filterFactory, consumer)
具體的解析邏輯比較簡單,咱們來梳理一下:
1)獲取默認的配置文件路徑,有4種。 2)遍歷全部的路徑,拼裝配置文件名稱。 3)再遍歷解析器,選擇yml或者properties解析,將解析結果添加到集合MutablePropertySources當中。
最後解析的結果以下:
至此,springBoot中的資源文件加載完畢,解析順序從上到下,因此前面的配置文件會覆蓋後面的配置文件。能夠看到application.properties
的優先級最低,系統變量和環境變量的優先級相對較高。
主要有兩處大的變化:
1、在SpringBoot1.x版本中,若是咱們定義了application-default.properties
文件,優先級順序:
application-default.properties > application-dev.properties > application.properties
複製代碼
在SpringBoot2.x版本中,若是咱們定義了application-default.properties
文件,會有兩種狀況:
1)沒有配置application-dev.properties
文件,優先級順序:
application-default.properties > application.properties
複製代碼
2)同時配置了application-dev.properties
文件,優先級順序:
application-dev.properties > application.properties
複製代碼
這是由於在2.x版本中,若是定義了application-dev.properties
文件,application-default.properties
文件將會刪除。
代碼中,在解析application.properties
時會有以下判斷,判斷是否有 active 文件,若是有,則會刪除默認的profile
:
private void maybeActivateProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
if (this.activatedProfiles) {
this.logger.debug("Profiles already activated, '" + profiles
+ "' will not be applied");
return;
}
addProfiles(profiles);
this.logger.debug("Activated profiles "
+ StringUtils.collectionToCommaDelimitedString(profiles));
this.activatedProfiles = true;
//刪除默認的配置文件,即application-default.* 文件
removeUnprocessedDefaultProfiles();
}
private void removeUnprocessedDefaultProfiles() {
this.profiles.removeIf(Profile::isDefaultProfile);
}
複製代碼
2、SpringBoot 1.x版本中,項目的配置文件統一被封裝在內部類ConfigFileApplicationListener$ConfigurationPropertySources
對象中,存入environment
。而在2.x版本中,配置文件則是分別被包裝成OriginTrackedMapPropertySource
存入environment
中
在springboot1.x
升級到springboot2.x
的時候,還請注意以上兩點。
SpringBoot2 | SpringBoot啓動流程源碼分析(一)
SpringBoot2 | SpringBoot啓動流程源碼分析(二)
SpringBoot2 | @SpringBootApplication註解 自動化配置流程源碼分析(三)
SpringBoot2 | SpringBoot Environment源碼分析(四)
SpringBoot2 | SpringBoot自定義AutoConfiguration | SpringBoot自定義starter(五)