Springboot中PropertySource的結(jié)構(gòu)與加載過程逐步分析講解
記得之前寫過一篇文章分析spring BeanFactory的時(shí)候說過的spring當(dāng)中設(shè)計(jì)很經(jīng)典的一個(gè)點(diǎn)就是 “讀寫分離” 模式。使用這個(gè)模式可以很好的區(qū)分開框架與業(yè)務(wù)的使用上的側(cè)重點(diǎn)。業(yè)務(wù)層不應(yīng)該具有修改框架的特性。
所以講Propertysource我們從Environment開始講。我們知道我們平時(shí)在項(xiàng)目中拿到的Environment對象是只讀,但是它可以被轉(zhuǎn)換成可寫的對象。
在springboot中當(dāng)我們啟動(dòng)一個(gè)servlet應(yīng)用的時(shí)候在prepareEnvironment 階段實(shí)際上是new了一個(gè)StandardServletEnvironment
此時(shí)調(diào)用構(gòu)造函數(shù)放了四個(gè)propertysource進(jìn)去
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())); }
對應(yīng)的名稱分別為
- servletContextInitParams
- servletConfigInitParams
- jndiProperties 可選
- systemEnvironment
- systemProperties
對早期項(xiàng)目熟悉的同學(xué)可能,通過這幾個(gè)參數(shù)能立馬知道他們是如何演變過來的。
早期的servlet項(xiàng)目中有個(gè)web.xml配置(那么springboot是如何讓它消失的呢?思考下)。這個(gè)配置中有這樣的標(biāo)簽
<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容器所解析的,同時(shí)也對spring進(jìn)行了映射,包括jndi配置,即你在容器層面做的配置最終也會(huì)被映射到environment中。此處不是我們當(dāng)前的重點(diǎn)不展開。
現(xiàn)在我們先來看看PropertySource這個(gè)類
PropertySource是個(gè)抽象類代表name/value鍵值對的一個(gè)資源,使用了泛型可以代表任意對象類型,例如可以是java.util.Properties,也可以是java.util.Map等
PropertySource對象通常不單獨(dú)使用,而是通過對象聚合資源屬性,結(jié)合PropertyResolver實(shí)現(xiàn)來解析資源對象,并根據(jù)優(yōu)先級(jí)進(jìn)行搜索。
可以使用@PropertySource 注解將對應(yīng)的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使用了一些占位符號(hào),不能早于application context 加載,此時(shí)需要進(jìn)行存根。 //等到對應(yīng)的資源加載之后再加載當(dāng)前的propertysource。占位符會(huì)在容器的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)用其它方法會(huì)報(bào)異常,這里是個(gè)適配器模式。 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); } } }
我們可以看到它預(yù)留了一個(gè)抽象方法getProperty 給子類實(shí)現(xiàn),而此方法就是如何獲取每個(gè)propertysource中的屬性的value,此時(shí)就可以有各種各樣的實(shí)現(xiàn)方式
- 例如:在SystemEnvironmentPropertySource 中 調(diào)用父類MapPropertySource 的getProperty方法實(shí)際是調(diào)用map.get方法獲取對應(yīng)的屬性值
- 例如:CommandLinePropertySource中實(shí)際是調(diào)用CommandLineArgs的getNonOptionArgs()與getOptionValues(name)方法獲取對應(yīng)的屬性值
- 例如:apollo實(shí)現(xiàn)的ConfigPropertySource實(shí)際上是調(diào)用System.getProperty(key);
以及Properties對象的get方法獲取的屬性值。
了解完這個(gè)結(jié)構(gòu)之后我們后面再去看配置中心的實(shí)現(xiàn),看起來就容易理解多了,此處按下不表。
其它的不多介紹,具體的類層次結(jié)構(gòu)大家自行觀察。大體上最后的數(shù)據(jù)結(jié)構(gòu)基本上都是從hash表中獲取對應(yīng)的鍵值對。
了解完propertysource的數(shù)據(jù)結(jié)構(gòu)之后,那么問題來了springboot什么時(shí)候加載了配置文件呢?又是如何解析成對應(yīng)的propertysource呢?帶著這個(gè)問題我們將整個(gè)流程貫穿起來看看就知道了。
所以我們先來看看ConfigFileApplicationListener這個(gè)類,如果你問我為什么看這個(gè)類,我會(huì)告訴你你可以全局內(nèi)容搜索application.properties,當(dāng)然最好是你有初略過了一遍springboot源碼在來看會(huì)比較好。
ConfigFileApplicationListener實(shí)現(xiàn)了EnvironmentPostProcessor以及SmartApplicationListener這兩個(gè)接口。我們知道實(shí)現(xiàn)了ApplicationListener接口的類會(huì)在spring啟動(dòng)階段接收到各個(gè)環(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)做了兩個(gè)環(huán)節(jié)的處理,一個(gè)是環(huán)境裝備完成的時(shí)候處理了一次,一個(gè)是容器準(zhǔn)備完成時(shí)處理了一次,這兩次的事件的執(zhí)行時(shí)機(jī)分別如下
ApplicationEnvironmentPreparedEvent:prepareEnvironment
onApplicationPreparedEvent:prepareContext
在啟動(dòng)過程中prepareEnvironment 先執(zhí)行所以這個(gè)事件的執(zhí)行順序?yàn)榇a的邏輯順序,先進(jìn)第一個(gè)if條件再進(jìn)第二個(gè)條件。
具體來看這兩個(gè)方法
先看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()); } }
當(dāng)前類的postPorcessEnvironment方法添加了個(gè)RandomValuePropertySource,并并且new Loader 調(diào)用load方法在load方法中加載了application.properties文件,其它的邏輯就是如何找到這個(gè)文件以及如何加載這個(gè)文件具體細(xì)節(jié)自行研究,不多解釋,加載的時(shí)候用到了PropertySourceLoader,對應(yīng)的PropertySourceLoader有不同的實(shí)現(xiàn),擴(kuò)展名properties,xml使用PropertiesPropertySourceLoader 解析,而
“yml”, "yaml"使用YamlPropertySourceLoader加載,加載完成后就包裝成MapPropertySource子類。并且將其設(shè)置給Environment。
public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); //初始化默認(rèn)的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(); }
接著我們來看第二個(gè)事件的方法
第二個(gè)事件的方法添加了一個(gè)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); } }
這個(gè)方法做了一件神奇的事情,因?yàn)槟J(rèn)配置是最先被放到環(huán)境容器中的,所以它在最前面,所以后續(xù)往里又添加了很多其它的propertysource之后,需要將它移動(dòng)到最后,做一個(gè)兜底策略,最終就是取不到配置了再去取默認(rèn)配置。
在結(jié)合開始的時(shí)候的數(shù)據(jù)結(jié)構(gòu),大概我們就可以總結(jié)出如下過程
1、環(huán)境準(zhǔn)備階段,廣播了環(huán)境準(zhǔn)備完成事件
2、調(diào)用listener方法onApplicationEvent去初始化了application.properties文件
3、使用PropertySourceLoader解析對應(yīng)的文件并包裝成propertysource
4、將propertysource設(shè)置給environment
5、容器準(zhǔn)備階段,廣播了容器準(zhǔn)備完成事件
6、調(diào)用listener方法onApplicationEvent去設(shè)置了一個(gè)BeanfactoryPostProcessor
7、在refresh階段調(diào)用了這個(gè)postProcessor,調(diào)整了下默認(rèn)配置文件的順序。
具體的文件解析和占位符替換等等這些動(dòng)作這里先不介紹了。
到此這篇關(guān)于Springboot中PropertySource的結(jié)構(gòu)與加載過程逐步分析講解的文章就介紹到這了,更多相關(guān)Springboot PropertySource內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)與算法之樹(動(dòng)力節(jié)點(diǎn)java學(xué)院整理)
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)與算法之樹的相關(guān)知識(shí),最主要的是二叉樹中的二叉搜索樹,需要的朋友可以參考下2017-04-04淺談java反射和自定義注解的綜合應(yīng)用實(shí)例
本篇文章主要介紹了java反射和自定義注解的綜合應(yīng)用,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09springboot整合vue2-uploader實(shí)現(xiàn)文件分片上傳、秒傳、斷點(diǎn)續(xù)傳功能
對于大文件的處理,無論是用戶端還是服務(wù)端,如果一次性進(jìn)行讀取發(fā)送、接收都是不可取,很容易導(dǎo)致內(nèi)存問題,下面這篇文章主要給大家介紹了關(guān)于springboot整合vue2-uploader實(shí)現(xiàn)文件分片上傳、秒傳、斷點(diǎn)續(xù)傳功能的相關(guān)資料,需要的朋友可以參考下2023-06-06SpringBoot實(shí)現(xiàn)服務(wù)接入nacos注冊中心流程詳解
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)服務(wù)接入nacos注冊中心流程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01SpringBoot集成Memcached的項(xiàng)目實(shí)踐
Memcached是一個(gè)高性能的分布式內(nèi)存對象緩存系統(tǒng),用于動(dòng)態(tài)Web應(yīng)用以減輕數(shù)據(jù)庫負(fù)載,本文主要介紹了SpringBoot集成Memcached的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01解決 Spring RestTemplate post傳遞參數(shù)時(shí)報(bào)錯(cuò)問題
本文詳解說明了RestTemplate post傳遞參數(shù)時(shí)報(bào)錯(cuò)的問題及其原由,需要的朋友可以參考下2020-02-02Maven統(tǒng)一版本管理的實(shí)現(xiàn)
在使用Maven多模塊結(jié)構(gòu)工程時(shí),配置版本是一個(gè)比較頭疼的事,本文主要介紹了Maven統(tǒng)一版本管理的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03