所有文章
https://www.cnblogs.com/lay2017/p/11478237.html
觸發監聽器加載配置文件
在上一篇文章中,我們看到了Environment對象的創建方法。同時也稍微提及了一下ConfigFileApplicationListener這個監聽器,這個監聽器主要工作是為了加載application.properties/yml配置文件的。
回顧一下prepareEnvironment方法的代碼
private ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments ) { // 創建一個Environment對象 ConfigurableEnvironment environment = getOrCreateEnvironment(); // 配置Environment對象 configureEnvironment(environment, applicationArguments.getSourceArgs()); // 觸發監聽器(主要是觸發ConfigFileApplicationListener,這個監聽器將會加載如application.properties/yml這樣的配置文件) listeners.environmentPrepared(environment); // 省略 }
我們看到Environment對象在初始創建並配置之后會發布出一個事件給監聽器,注意!這里的監聽器並不是ConfigFileApplicationListener而是一個負責分發事件的監聽器EventPublishingRunListener。
我們跟進EventPublishingRunListener監聽器的environmentPrepared方法
private final SimpleApplicationEventMulticaster initialMulticaster; @Override public void environmentPrepared(ConfigurableEnvironment environment) { this.initialMulticaster .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment)); }
這里包裝了一個ApplicationEnvironmentPreparedEvent事件,並通過廣播的方式廣播給監聽該事件的監聽器,到這個時候才觸發了ConfigFileApplicationListener
我們跟進ConfigFileApplicationListener的onApplicationEvent方法
@Override public void onApplicationEvent(ApplicationEvent event) { // 只觸發Environment相關的事件 if (event instanceof ApplicationEnvironmentPreparedEvent) { onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event); } if (event instanceof ApplicationPreparedEvent) { onApplicationPreparedEvent(event); } }
Event將會觸發onApplicationEnvironmentPreparedEvent
繼續跟進
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()); } }
我們看到,首先加載了Environment的后置處理器,然后經過排序以后遍歷觸發每個處理器。這里注意,ConfigFileApplicationListener本身也實現了EnvironmentPostProcessor接口,所以這里將會觸發ConfigFileApplicationListener內部方法執行
我們跟進ConfigFileApplicationListener的postProcessEnvironment方法
@Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { addPropertySources(environment, application.getResourceLoader()); }
再跟進addPropertySources方法
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { RandomValuePropertySource.addToEnvironment(environment); new Loader(environment, resourceLoader).load(); }
我們看到,這里實例化了一個Loader用來加載application配置文件,而核心邏輯就在load方法當中。
加載器加載application配置文件
跟進Loader加載器的構造方法中
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { this.environment = environment; this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment); this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(); // 文件application配置文件的資源加載器,包括propertis/xml/yml/yaml擴展名 this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); }
我們注意到,再構造方法中將會從spring.factories中加載PropertySourceLoader接口的具體實現類,具體請參閱:輔助閱讀。
我們打開spring.factories可以看到
這里包括兩個實現
1)PropertiesPropertySourceLoader:用於加載property/xml格式的配置文件
2) YamlPropertySourceLoader:用於加載yml/yaml格式的配置文件
到這里,我們可以知道springboot支持的不同配置文件是通過選擇不同的加載器來實現
下面,我們回到Loader加載器的load方法中,跟進加載的主要邏輯
public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); // 初始化profiles initializeProfiles(); while (!this.profiles.isEmpty()) { // 消費一個profile Profile profile = this.profiles.poll(); // active的profile添加到Environment if (profile != null && !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } // 加載 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } // 重置Environment中的profiles resetEnvironmentProfiles(this.processedProfiles); // 加載 load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); // 添加所有properties到Environment中 addLoadedPropertySources(); }
代碼有點小長,我們根據如何加載默認的application.properties/yml配置文件的流程來了解一下
先跟進initializeProfiles方法看看如果初始化profiles
private void initializeProfiles() { // 第一個profile為null,這樣能保證首個加載application.properties/yml this.profiles.add(null); Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty(); this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty)); addActiveProfiles(activatedViaProperty); // 沒有額外配置profile的時候,將使用默認的 if (this.profiles.size() == 1) { for (String defaultProfileName : this.environment.getDefaultProfiles()) { Profile defaultProfile = new Profile(defaultProfileName, true); this.profiles.add(defaultProfile); } } }
這里注意兩點
1)將會首先添加一個null,保證第一次加載的是application配置
2) 其次,如果沒有配置profile,那么使用default。注意,我們的application配置文件還未加載,所以這里的"沒有配置"並不是指你的application配置文件中有沒有配置,而是如命令行、獲取main方法傳入等其它方法配置
我們並未配置任何active的profile,所以這里最終將產生一個這樣的數據
profiles=[null, "default"]
回到load方法中,我們繼續往下看
public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); // 初始化profiles initializeProfiles(); while (!this.profiles.isEmpty()) { // 消費一個profile Profile profile = this.profiles.poll(); // active的profile添加到Environment if (profile != null && !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } // 加載 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } // 重置Environment中的profiles resetEnvironmentProfiles(this.processedProfiles); // 加載 load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); // 添加所有properties到Environment中 addLoadedPropertySources(); }
while循環中,首先拿到的是profile=null,然后就直接進入第二個load加載方法加載配置文件
我們跟進第二個load加載方法(請注意區分load方法,后續還會出現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)); }); }
該方法中的邏輯主要是搜索每個位置下的每個指定的配置文件名,並加載
跟進getSearchLocations看看要搜索哪些位置
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"; private Set<String> getSearchLocations() { if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) { return getSearchLocations(CONFIG_LOCATION_PROPERTY); } Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY); locations.addAll( asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS)); return locations; }
很顯然,由於我們沒有自定義一些搜索位置,那么默認搜索classpath:/、classpath:/config/、file:./、file:./下
回到第二個load方法,我們再看看getSearchNames方法要加載哪些文件
private static final String DEFAULT_NAMES = "application"; private Set<String> getSearchNames() { if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); return asResolvedSet(property, null); } return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES); }
相似的邏輯,最終返回默認的配置文件名application,也就是我們最熟悉的名字
接下來,再回到第二個load方法,我們可以跟進第三個load方法了,看看如何根據locations和names來加載配置文件
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { // 省略 Set<String> processed = new HashSet<>(); // 遍歷加載器 for (PropertySourceLoader loader : this.propertySourceLoaders) { // 獲取擴展名 for (String fileExtension : loader.getFileExtensions()) { if (processed.add(fileExtension)) { // 加載對應擴展名的文件 loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory, consumer); } } } }
這個load方法主要邏輯表明將會加載每個加載器可以支持的配置文件,在Loader初始化的時候我們獲得了兩個加載器,同時每個加載器支持兩種格式。所以這里的嵌套遍歷中,我們將會嘗試加載4種配置文件,如
1)application.properties
2) application.xml
3) application.yml
4) application.yaml
再跟進loadForFileExtension方法,看看具體每種的加載
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null); DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile); // 當前沒有profile if (profile != null) { String profileSpecificFile = prefix + "-" + profile + fileExtension; load(loader, profileSpecificFile, profile, defaultFilter, consumer); load(loader, profileSpecificFile, profile, profileFilter, consumer); for (Profile processedProfile : this.processedProfiles) { if (processedProfile != null) { String previouslyLoaded = prefix + "-" + processedProfile + fileExtension; load(loader, previouslyLoaded, profile, profileFilter, consumer); } } } // 加載具體格式的文件 load(loader, prefix + fileExtension, profile, profileFilter, consumer); }
由於當前profile=null,所以我們直接進入第四個load方法
跟進第四個load方法,由於該方法有點長,我們省略次要的代碼
private void load( PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer ) { try { // 獲取資源 Resource resource = this.resourceLoader.getResource(location); // 加載為Document對象 List<Document> documents = loadDocuments(loader, name, resource); List<Document> loaded = new ArrayList<>(); // 遍歷Document集合 for (Document document : documents) { if (filter.match(document)) { // 添加profile addActiveProfiles(document.getActiveProfiles()); addIncludedProfiles(document.getIncludeProfiles()); loaded.add(document); } } Collections.reverse(loaded); if (!loaded.isEmpty()) { // 回調處理每個document loaded.forEach((document) -> consumer.accept(profile, document)); } } catch (Exception ex) {} }
首先配置文件會被加載為Document這樣的內存對象,並最終回調處理。
這里我們看到回調是調用consumer這樣一個接口,我們得回到第一個load方法,看看調用第二個load方法的時候傳入的consumer是啥
public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); // 初始化profiles initializeProfiles(); while (!this.profiles.isEmpty()) { // 消費一個profile Profile profile = this.profiles.poll(); // active的profile添加到Environment if (profile != null && !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } // 加載 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } // 重置Environment中的profiles resetEnvironmentProfiles(this.processedProfiles); // 加載 load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); // 添加所有properties到Environment中 addLoadedPropertySources(); }
我們看到,在調用第二個load方法的時候就通過addToLoaded這個方法的執行來獲取一個consumer,用來回調處理配置文件的Document對象。
注意!在調用addToLoaded的時候通過方法引用指定了一個method,這個method將在consumer回調的內部被使用。
method=MutablePropertySources:addLast
跟進addToLoaded方法
private Map<Profile, MutablePropertySources> loaded; private DocumentConsumer addToLoaded( BiConsumer<MutablePropertySources, PropertySource<?>> addMethod, boolean checkForExisting) { return (profile, document) -> { // 省略 MutablePropertySources merged = this.loaded.computeIfAbsent(profile, (k) -> new MutablePropertySources()); // 回調method addMethod.accept(merged, document.getPropertySource()); }; }
我們看到,loaded是一個profile和MutableProperySources的鍵值組合。方法邏輯中將會先獲取loaded里面的MutablePropertySources,然后調用addLast方法將Document中的PropertySource給添加到MutablePropertySources中。
到這里,一個application配置文件被加載到內存了。但是還沒完,前面的文章中我們說過Environment對象是應用程序環境的抽象,包含了properties。那么,我們還得將這些內存中的PropertySource給添加到Environment中。
回到第一個load方法中
public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); // 初始化profiles initializeProfiles(); while (!this.profiles.isEmpty()) { // 消費一個profile Profile profile = this.profiles.poll(); // active的profile添加到Environment if (profile != null && !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } // 加載 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } // 重置Environment中的profiles resetEnvironmentProfiles(this.processedProfiles); // 加載 load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); // 添加所有properties到Environment中 addLoadedPropertySources(); }
我們看到最后一行,addLoadedPropertySources方法的作用也就是將之前loaded里面的東西給添加到Environment中
跟進addLoadedPropertySources方法
private void addLoadedPropertySources() { MutablePropertySources destination = this.environment.getPropertySources(); List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values()); // 反向排序 Collections.reverse(loaded); String lastAdded = null; Set<String> added = new HashSet<>(); // 遍歷loaded for (MutablePropertySources sources : loaded) { // 遍歷配置 for (PropertySource<?> source : sources) { // 排重 if (added.add(source.getName())) { // 添加每個到Environment中 addLoadedPropertySource(destination, lastAdded, source); lastAdded = source.getName(); } } } }
這個方法很清晰地表明,將loaded中地PropertySource給追加到Environment中。
到這里,默認的application.properties/yml這樣地配置文件就被加載到了Environment當中了。不過還沒有結束,這里還有個比較重要的問題,多環境的時候application.properties/yml配置文件中指定了profile,就會加載application-{profile}.properties/yml是怎么實現的呢?
加載多環境的application-{profile}配置文件
回到我們之前的第四個load方法
private void load( PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer ) { try { // 獲取資源 Resource resource = this.resourceLoader.getResource(location); // 加載為Document對象 List<Document> documents = loadDocuments(loader, name, resource); List<Document> loaded = new ArrayList<>(); // 遍歷Document集合 for (Document document : documents) { if (filter.match(document)) { // 添加profile addActiveProfiles(document.getActiveProfiles()); addIncludedProfiles(document.getIncludeProfiles()); loaded.add(document); } } Collections.reverse(loaded); if (!loaded.isEmpty()) { // 回調處理每個document loaded.forEach((document) -> consumer.accept(profile, document)); } } catch (Exception ex) {} }
這里,已經把默認的application.properties/yml給加載成為了Document。然后在遍歷documents的時候,會把Document中的profiles做一次添加
我們跟進addActiveProfiles看看
private Deque<Profile> profiles; void addActiveProfiles(Set<Profile> profiles) { if (profiles.isEmpty()) { return; } // 省略 // 添加到隊列 this.profiles.addAll(profiles); // 省略 this.activatedProfiles = true; // 移除掉default removeUnprocessedDefaultProfiles(); }
我們看到,新的profiles首先會被添加到現有隊列中。最初的profiles=[null, "default"]。而后,我們消費了null,profiles=["default"]。現在,我們添加一個profile="test"。那么,profiles=["default", "test"]。
再看最后一行removeUnprocessedDefaultProfiles,將會移除default。所以,最終profiles=["test"]。
再回到第一個load方法中
public void load() { this.profiles = new LinkedList<>(); this.processedProfiles = new LinkedList<>(); this.activatedProfiles = false; this.loaded = new LinkedHashMap<>(); // 初始化profiles initializeProfiles(); while (!this.profiles.isEmpty()) { // 消費一個profile Profile profile = this.profiles.poll(); // active的profile添加到Environment if (profile != null && !profile.isDefaultProfile()) { addProfileToEnvironment(profile.getName()); } // 加載 load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } // 重置Environment中的profiles resetEnvironmentProfiles(this.processedProfiles); // 加載 load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); // 添加所有properties到Environment中 addLoadedPropertySources(); }
這時候while循環里面將會拿到profile="test",跟之前一樣一路下去,直到loadForFileExtension方法
跟進loadForFileExtension
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null); DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile); if (profile != null) { // 拼接如:application-test.properties/yml String profileSpecificFile = prefix + "-" + profile + fileExtension; // 加載文件 load(loader, profileSpecificFile, profile, defaultFilter, consumer); load(loader, profileSpecificFile, profile, profileFilter, consumer); for (Profile processedProfile : this.processedProfiles) { if (processedProfile != null) { String previouslyLoaded = prefix + "-" + processedProfile + fileExtension; load(loader, previouslyLoaded, profile, profileFilter, consumer); } } } // 加載具體格式的文件 load(loader, prefix + fileExtension, profile, profileFilter, consumer); }
我們看到,當有profile的時候文件名就不再是application.properties/yml了。它會把profile給拼接上去,所以就變成了application-test.properties/yml,並加載文件。后續也一樣得最終添加到Environment當中。
總結
application配置文件的加載過程邏輯並不復雜,只是具體細節比較多,所以代碼中包含了不少附加的邏輯。那么拋開細節,我們可以看到其實就是到相應的目錄下搜索相應的文件是否存在,加載到內存以后再添加到Environment當中。
至於具體的細節如:加載文件的時候編碼相關、多個文件相同配置是否覆蓋、加載器如何解析各種配置文件的內容有時間也可以仔細閱讀。