springboot啟動流程(四)application配置文件加載過程


所有文章

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當中。

至於具體的細節如:加載文件的時候編碼相關、多個文件相同配置是否覆蓋、加載器如何解析各種配置文件的內容有時間也可以仔細閱讀。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM