Springboot中PropertySource的結構與加載過程逐步分析講解
記得之前寫過一篇文章分析spring BeanFactory的時候說過的spring當中設計很經(jīng)典的一個點就是 “讀寫分離” 模式。使用這個模式可以很好的區(qū)分開框架與業(yè)務的使用上的側(cè)重點。業(yè)務層不應該具有修改框架的特性。
所以講Propertysource我們從Environment開始講。我們知道我們平時在項目中拿到的Environment對象是只讀,但是它可以被轉(zhuǎn)換成可寫的對象。
在springboot中當我們啟動一個servlet應用的時候在prepareEnvironment 階段實際上是new了一個StandardServletEnvironment
此時調(diào)用構造函數(shù)放了四個propertysource進去
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);
}super.customizePropertySources(propertySources)
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()));
}
對應的名稱分別為
- servletContextInitParams
- servletConfigInitParams
- jndiProperties 可選
- systemEnvironment
- systemProperties
對早期項目熟悉的同學可能,通過這幾個參數(shù)能立馬知道他們是如何演變過來的。
早期的servlet項目中有個web.xml配置(那么springboot是如何讓它消失的呢?思考下)。這個配置中有這樣的標簽
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springMVC-servlet.xml</param-value>
</context-param>
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>home-page</param-name>
<param-value>home.jsp</param-value>
</init-param>
</servlet>
這些參數(shù)是被servlet容器所解析的,同時也對spring進行了映射,包括jndi配置,即你在容器層面做的配置最終也會被映射到environment中。此處不是我們當前的重點不展開。
現(xiàn)在我們先來看看PropertySource這個類
PropertySource是個抽象類代表name/value鍵值對的一個資源,使用了泛型可以代表任意對象類型,例如可以是java.util.Properties,也可以是java.util.Map等
PropertySource對象通常不單獨使用,而是通過對象聚合資源屬性,結合PropertyResolver實現(xiàn)來解析資源對象,并根據(jù)優(yōu)先級進行搜索。
可以使用@PropertySource 注解將對應的PropertySource 加入到Enviroment
public abstract class PropertySource<T> {
protected final Log logger = LogFactory.getLog(getClass());
protected final String name;
protected final T source;
public PropertySource(String name, T source) {
Assert.hasText(name, "Property source name must contain at least one character");
Assert.notNull(source, "Property source must not be null");
this.name = name;
this.source = source;
}
@SuppressWarnings("unchecked")
public PropertySource(String name) {
this(name, (T) new Object());
}
public String getName() {
return this.name;
}
public T getSource() {
return this.source;
}
public boolean containsProperty(String name) {
return (getProperty(name) != null);
}
@Nullable
public abstract Object getProperty(String name);
@Override
public boolean equals(Object other) {
return (this == other || (other instanceof PropertySource &&
ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) other).name)));
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(this.name);
}
@Override
public String toString() {
if (logger.isDebugEnabled()) {
return getClass().getSimpleName() + "@" + System.identityHashCode(this) +
" {name='" + this.name + "', properties=" + this.source + "}";
}
else {
return getClass().getSimpleName() + " {name='" + this.name + "'}";
}
}
public static PropertySource<?> named(String name) {
return new ComparisonPropertySource(name);
}
//StubPropertySource內(nèi)部類,存根PropertySouece 目的是為了,延遲加載。
//即有些propertysource使用了一些占位符號,不能早于application context 加載,此時需要進行存根。
//等到對應的資源加載之后再加載當前的propertysource。占位符會在容器的refresh階段被替換,
//具體解析可以查看AbstractApplicationContext#initPropertySources()
public static class StubPropertySource extends PropertySource<Object> {
public StubPropertySource(String name) {
super(name, new Object());
}
/**
* Always returns {@code null}.
*/
@Override
@Nullable
public String getProperty(String name) {
return null;
}
}
//靜態(tài)內(nèi)部類為了named方法使用,僅僅用戶比較,調(diào)用其它方法會報異常,這里是個適配器模式。
static class ComparisonPropertySource extends StubPropertySource {
private static final String USAGE_ERROR =
"ComparisonPropertySource instances are for use with collection comparison only";
public ComparisonPropertySource(String name) {
super(name);
}
@Override
public Object getSource() {
throw new UnsupportedOperationException(USAGE_ERROR);
}
@Override
public boolean containsProperty(String name) {
throw new UnsupportedOperationException(USAGE_ERROR);
}
@Override
@Nullable
public String getProperty(String name) {
throw new UnsupportedOperationException(USAGE_ERROR);
}
}
}我們可以看到它預留了一個抽象方法getProperty 給子類實現(xiàn),而此方法就是如何獲取每個propertysource中的屬性的value,此時就可以有各種各樣的實現(xiàn)方式
- 例如:在SystemEnvironmentPropertySource 中 調(diào)用父類MapPropertySource 的getProperty方法實際是調(diào)用map.get方法獲取對應的屬性值
- 例如:CommandLinePropertySource中實際是調(diào)用CommandLineArgs的getNonOptionArgs()與getOptionValues(name)方法獲取對應的屬性值
- 例如:apollo實現(xiàn)的ConfigPropertySource實際上是調(diào)用System.getProperty(key);
以及Properties對象的get方法獲取的屬性值。
了解完這個結構之后我們后面再去看配置中心的實現(xiàn),看起來就容易理解多了,此處按下不表。
其它的不多介紹,具體的類層次結構大家自行觀察。大體上最后的數(shù)據(jù)結構基本上都是從hash表中獲取對應的鍵值對。
了解完propertysource的數(shù)據(jù)結構之后,那么問題來了springboot什么時候加載了配置文件呢?又是如何解析成對應的propertysource呢?帶著這個問題我們將整個流程貫穿起來看看就知道了。
所以我們先來看看ConfigFileApplicationListener這個類,如果你問我為什么看這個類,我會告訴你你可以全局內(nèi)容搜索application.properties,當然最好是你有初略過了一遍springboot源碼在來看會比較好。
ConfigFileApplicationListener實現(xiàn)了EnvironmentPostProcessor以及SmartApplicationListener這兩個接口。我們知道實現(xiàn)了ApplicationListener接口的類會在spring啟動階段接收到各個環(huán)節(jié)的事件,所以我們直接查看onApplicationEvent方法
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
我們發(fā)現(xiàn)做了兩個環(huán)節(jié)的處理,一個是環(huán)境裝備完成的時候處理了一次,一個是容器準備完成時處理了一次,這兩次的事件的執(zhí)行時機分別如下
ApplicationEnvironmentPreparedEvent:prepareEnvironment
onApplicationPreparedEvent:prepareContext
在啟動過程中prepareEnvironment 先執(zhí)行所以這個事件的執(zhí)行順序為代碼的邏輯順序,先進第一個if條件再進第二個條件。
具體來看這兩個方法
先看onApplicationEnvironmentPreparedEvent,遍歷調(diào)用了一輪postProcessor.postProcessEnvironment
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
當前類的postPorcessEnvironment方法添加了個RandomValuePropertySource,并并且new Loader 調(diào)用load方法在load方法中加載了application.properties文件,其它的邏輯就是如何找到這個文件以及如何加載這個文件具體細節(jié)自行研究,不多解釋,加載的時候用到了PropertySourceLoader,對應的PropertySourceLoader有不同的實現(xiàn),擴展名properties,xml使用PropertiesPropertySourceLoader 解析,而
“yml”, "yaml"使用YamlPropertySourceLoader加載,加載完成后就包裝成MapPropertySource子類。并且將其設置給Environment。
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
//初始化默認的profile=default
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
resetEnvironmentProfiles(this.processedProfiles);
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
接著我們來看第二個事件的方法
第二個事件的方法添加了一個BeanFactoryPostProcessor 為PropertySourceOrderingPostProcessor,而BeanFactoryPostProcessor 是再refresh的InvokeBeanFactoryPostProcessor 階段執(zhí)行的。我們先看看它是如何執(zhí)行的,postProcessBeanFactory調(diào)用了如下方法
private void reorderSources(ConfigurableEnvironment environment) {
PropertySource<?> defaultProperties = environment.getPropertySources()
.remove(DEFAULT_PROPERTIES);
if (defaultProperties != null) {
environment.getPropertySources().addLast(defaultProperties);
}
}
這個方法做了一件神奇的事情,因為默認配置是最先被放到環(huán)境容器中的,所以它在最前面,所以后續(xù)往里又添加了很多其它的propertysource之后,需要將它移動到最后,做一個兜底策略,最終就是取不到配置了再去取默認配置。
在結合開始的時候的數(shù)據(jù)結構,大概我們就可以總結出如下過程
1、環(huán)境準備階段,廣播了環(huán)境準備完成事件
2、調(diào)用listener方法onApplicationEvent去初始化了application.properties文件
3、使用PropertySourceLoader解析對應的文件并包裝成propertysource
4、將propertysource設置給environment
5、容器準備階段,廣播了容器準備完成事件
6、調(diào)用listener方法onApplicationEvent去設置了一個BeanfactoryPostProcessor
7、在refresh階段調(diào)用了這個postProcessor,調(diào)整了下默認配置文件的順序。
具體的文件解析和占位符替換等等這些動作這里先不介紹了。
到此這篇關于Springboot中PropertySource的結構與加載過程逐步分析講解的文章就介紹到這了,更多相關Springboot PropertySource內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
springcloud引入spring-cloud-starter-openfeign失敗的解決
這篇文章主要介紹了springcloud?引入spring-cloud-starter-openfeign失敗的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringMvc @RequestParam 使用推薦使用包裝類型代替包裝類型
這篇文章主要介紹了SpringMvc @RequestParam 使用推薦使用包裝類型代替包裝類型,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02
Java?ArrayList集合之解鎖數(shù)據(jù)存儲新姿勢
這篇文章主要介紹了Java?ArrayList集合之解鎖數(shù)據(jù)存儲新姿勢,ArrayList是一個動態(tài)數(shù)組,可以自動調(diào)整大小,并提供了豐富的操作方法,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2025-03-03
idea?http?request無法識別環(huán)境變量的解決步驟
AlibabaCloudToolkit插件安裝后在?Editor->File?Types增加?AlibabaCloudROStemplates(JSON)項且會配置為解析*.json?文件,導致http?client無法正確解析http-client.env.json文件而無法讀取環(huán)境變量,本文介紹idea?http?request無法識別環(huán)境變量問題,需要的朋友可以參考下2023-08-08
SpringBoot結合Neo4j自定義cypherSql的方法
這篇文章主要介紹了SpringBoot結合Neo4j自定義cypherSql,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-11-11
java?LockSupport實現(xiàn)原理示例解析
這篇文章主要為大家介紹了java?LockSupport實現(xiàn)原理示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01

