從SpringBoot原始碼分析 配置檔案的載入原理和優先順序
從SpringBoot原始碼分析 配置檔案的載入原理和優先順序
本文從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中的配置項。
參考:
1. SpringBoot原始碼分析之SpringBoot的啟動過程
3. springboot啟動時是如何載入配置檔案application.yml檔案
|
原文地址 :https://www.cnblogs.com/tanliwei/p/9304072.html