SpringBoot源碼分析之加載配置文件

上一篇從使用角度介紹瞭如何在 springboot 啓動時對 yml 文件進行配置,這一篇嘗試從源碼角度去探討其加載配置文件的機制。spring

咱們回到 springboot 初始啓動時的 run 方法,爲了突出主幹邏輯,對代碼作了一些精簡:sql

public ConfigurableApplicationContext run(String... args) {
  //省略其餘代碼
  ...

   Collection<SpringApplicationRunListener> runListeners = getRunListeners(args);
   for (SpringApplicationRunListener runListener : runListeners) {
      runListener.started();
   }

   try {
      // Create and configure the environment
      ConfigurableEnvironment environment = getOrCreateEnvironment();
      configureEnvironment(environment, args);
      //廣播 environment 已準備事件到各個監聽器
      for (SpringApplicationRunListener runListener : runListeners) {
         runListener.environmentPrepared(environment);
      }

      // Create, load, refresh and run the ApplicationContext
      context = createApplicationContext();
      if (this.registerShutdownHook) {
         try {
            context.registerShutdownHook();
         }
         catch (AccessControlException ex) {
            // Not allowed in some environments.
         }
      }
      context.setEnvironment(environment);
      postProcessApplicationContext(context);
      applyInitializers(context);
      for (SpringApplicationRunListener runListener : runListeners) {
         runListener.contextPrepared(context);
      }

      // Load the sources
      Set<Object> sources = getSources();
      Assert.notEmpty(sources, "Sources must not be empty");
      load(context, sources.toArray(new Object[sources.size()]));
      for (SpringApplicationRunListener runListener : runListeners) {
         runListener.contextLoaded(context);
      }

      // Refresh the context
      refresh(context);
      afterRefresh(context, args);
      for (SpringApplicationRunListener runListener : runListeners) {
         runListener.finished(context, null);
      }
      return context;
   }
   catch (Throwable ex) {
      //省略
      ....
   }
複製代碼

對於 spring run listener 如何經過事件類型找到監聽器,前文已有敘述。此處從一個監聽器開始提及。springboot

ConfigFileApplicationListener

正是這個監聽器監聽到 environmentPrepared event後,開始加載配置文件邏輯。bash

@Override
public void onApplicationEvent(ApplicationEvent event) {
	//判斷類型,執行邏輯
   if (event instanceof ApplicationEnvironmentPreparedEvent) {
      onApplicationEnvironmentPreparedEvent(
            (ApplicationEnvironmentPreparedEvent) event);
   }
   if (event instanceof ApplicationPreparedEvent) {
      onApplicationPreparedEvent((ApplicationPreparedEvent) event);
   }
}
複製代碼

跟着代碼走,進入下面的方法:服務器

private void onApplicationEnvironmentPreparedEvent(
      ConfigurableEnvironment environment, SpringApplication application) {
   //加載全部的配置屬性到 environment
   addPropertySources(environment, application.getResourceLoader());
   //將 environment 綁定到 springApplication
   bindToSpringApplication(environment, application);
}
複製代碼

加載屬性

protected void addPropertySources(ConfigurableEnvironment environment,
      ResourceLoader resourceLoader) {
   RandomValuePropertySource.addToEnvironment(environment);
   try {
   	  //核心方法,使用 resourceLoader 從 environment 中加載屬性
      new Loader(environment, resourceLoader).load();
   } catch (IOException ex) {
      throw new IllegalStateException("Unable to load configuration files", ex);
   }
}
複製代碼

load 方法代碼以下:app

public void load() throws IOException {
	//初始化屬性加載器,用於後面的屬性加載工做
   this.propertiesLoader = new PropertySourcesLoader();
   this.activatedProfiles = false;
   //初始化 profiles 集合,包裝成爲一個 LIFO(後進先出)隊列,爲了實現後面覆蓋前面的特性
   this.profiles = Collections.asLifoQueue(new LinkedList<String>());
   //初始化 active profile 集合
   Set<String> initialActiveProfiles = initializeActiveProfiles();
   this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
   this.profiles.add(null);

	//取出來全部的profile,依次load,若是後次加載的屬性會覆蓋上次加載的屬性值
   while (!this.profiles.isEmpty()) {
      String profile = this.profiles.poll();
      for (String location : getSearchLocations()) {
         if (!location.endsWith("/")) {
            // location is a filename already, so don't search for more // filenames load(location, null, profile); } else { for (String name : getSearchNames()) { load(location, name, profile); } } } } //綁定屬性到 environment 中 addConfigurationProperties(this.propertiesLoader.getPropertySources()); } 複製代碼

profile

profile 用於區別不一樣環境下的配置,好比如今有兩套環境:生產環境和測試環境,若是有兩個配置文件對於同一個屬性,進行了不一樣的配置(好比服務器port),那我只須要針對兩套環境寫兩個配置文件,只須要更改 profile 的值便可讓應用自動選擇加載哪一個配置文件。dom

這裏經過使用一個 *Collections.asLifoQueue(new LinkedList());*方法構建了一個 LIFO(後進先出)的隊列,相似棧的性質。目的就是爲了後面的 profile 具備更高的優先級。好比指定了ide

spring.profiles.active=dev,hsqldb工具

則對於一樣一個屬性的配置,hsqldb 的屬性值會覆蓋 dev 的屬性值。post

對於 profile 的配置分三步進行:

Set<String> initialActiveProfiles = initializeActiveProfiles();
   this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
   this.profiles.add(null)
  
複製代碼

取到全部 active 的 profile

第一步:初始化 active profile 的集合

private Set<String> initializeActiveProfiles() {
	//ACTIVE_PROFILES_PROPERTY = "spring.profiles.active"
	//若是環境中沒有該屬性,則返回一個空集
   if (!this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
      return Collections.emptySet();
   }
   //Property source(好比系統屬性)設置的 profile 優先級高於配置文件中的配置
   Set<String> activeProfiles = getProfilesForValue(
         this.environment.getProperty(ACTIVE_PROFILES_PROPERTY));
   maybeActivateProfiles(activeProfiles);
   return activeProfiles;
}
複製代碼

咱們進去 getProfilesForValue 往方法裏面一直走:

List<String> list = Arrays
      .asList(StringUtils.commaDelimitedListToStringArray(value != null
            ? this.environment.resolvePlaceholders(value) : fallback));
Collections.reverse(list);
return new LinkedHashSet<String>(list);
複製代碼

此段代碼總共作了三件事:

  1. 將環境中的 profile 配置屬性使用符號進行分割;
  2. 使用集合工具類將之反轉(爲了使後面的 profile 優先級高於前面的
  3. 加入一個 hashset(爲了去重) 中返回,其中 LinkedHashSet 底層使用一個 LinkedHashMap來保存數據,故能使得插入的數據保持順序

獲取到 profile集合以後,按順序入隊。

private void addProfiles(Set<String> profiles) {
   for (String profile : profiles) {
      this.profiles.add(profile);
      if (!this.environment.acceptsProfiles(profile)) {
         // If it's already accepted we assume the order was set // intentionally prependProfile(this.environment, profile); } } } 複製代碼

第二步:拿到命令行配置的 profile

在初始化 profile 完畢以後,又經過下面的一行代碼對一些 profile 進行了合併。

this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
複製代碼

咱們能夠繼續看下併入的profile都是什麼?

private List<String> getUnprocessedActiveProfiles(
      Set<String> initialActiveProfiles) {
   List<String> unprocessedActiveProfiles = new ArrayList<String>();
   for (String profile : this.environment.getActiveProfiles()) {
      if (!initialActiveProfiles.contains(profile)) {
         unprocessedActiveProfiles.add(profile);
      }
   }
   // 繼續反轉,邏輯同上
   Collections.reverse(unprocessedActiveProfiles);
   return unprocessedActiveProfiles;
}
複製代碼

這裏處理了一些經過其餘方式設置的 profile,因爲前面經過ACTIVE_PROFILES_PROPERTY屬性設置的profile擁有更高的優先級,因此此處繼續從後面入隊。

第三步:添加默認的profile

this.profiles.add(null);
複製代碼

默認的 profile 爲null,優先級最低,只要在 queue 前面有元素便可被覆蓋,因此此時加入 null,是爲了後續處理統一。

到此,通過三步操做,咱們已經按照優先級順序拿到了全部的 active 的 profile。

接下來就是出隊,按照優先級順序依次加載各個 profile 相應的屬性,若是屬性相同,則後面出隊的覆蓋前面的,與優先級特性相對應。

依次加載 profile 屬性

while (!this.profiles.isEmpty()) {
   String profile = this.profiles.poll();
   for (String location : getSearchLocations()) {
      if (!location.endsWith("/")) {
         // location is a filename already, so don't search for more // filenames load(location, null, profile); } else { for (String name : getSearchNames()) { load(location, name, profile); } } } } 複製代碼

上面就是依次加載的主要邏輯。

從上面的代碼中能夠看到,依次出隊取到 profile 屬性後,經過一個 getSearchLocations 方法拿到全部應該加載 property 的位置,依次進行加載。

private Set<String> getSearchLocations() {
   Set<String> locations = new LinkedHashSet<String>();
   // CONFIG_LOCATION_PROPERTY = "spring.config.location"
   // 用戶配置優先,因此先處理配置
   if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
      for (String path : asResolvedSet(
            this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
         if (!path.contains("$")) {
            path = StringUtils.cleanPath(path);
            if (!ResourceUtils.isUrl(path)) {
               path = ResourceUtils.FILE_URL_PREFIX + path;
            }
         }
         locations.add(path);
      }
   }
   // 加載默認配置
   // DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"
   locations.addAll(
         asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
               DEFAULT_SEARCH_LOCATIONS));
   return locations;
}
複製代碼

從上面代碼中能夠看到,若是用戶經過 spring.config.location 參數對加載文件的路徑進行了配置,則會首先將該路徑加入到 set 集合中(一樣也能夠保證順序)。以後纔會加載默認的路徑,而默認路徑即如前文所介紹的。

在拿到路徑後,先判斷是不是目錄,若是 location 不以 「/」結尾,則認爲已是一個文件名,直接加載;反之,則要在該路徑下搜索到全部知足條件的配置文件。

// Search for a file with the given name
for (String ext : this.propertiesLoader.getAllFileExtensions()) {
   if (profile != null) {
      // Try the profile specific file
      loadIntoGroup(group, location + name + "-" + profile + "." + ext,
            null);
      loadIntoGroup(group, location + name + "-" + profile + "." + ext,
            profile);
   }
   loadIntoGroup(group, location + name + "." + ext, profile);
}
複製代碼

上面代碼對符合目錄下全部的合法的配置文件進行了搜索。

合法文件名的樣式以下:

location + name + "-" + profile + "." + ext

好比 application-dev.yml

for循環裏面對於全部支持的文件擴展名進行了遍歷,咱們能夠看看支持的文件擴展名有哪些:

public String[] getFileExtensions() {
   return new String[] { "yml", "yaml" };
}
複製代碼

這是負責加載 yml 文件的 YamlPropertySourceLoader 返回的符合條件的文件擴展名。

public String[] getFileExtensions() {
   return new String[] { "properties", "xml" };
}
複製代碼

這是負責加載 property 文件的 PropertiesPropertySourceLoader 返回的符合條件的文件擴展名。

這兩個加載器都是 springboot 自帶的加載器,固然也能夠自定義加載屬性類,便可加載指定擴展名的文件。

接下來重點看下實際進行加載的 loadIntoGroup 方法:

這裏 group 的名字爲「profile=dev 」樣式。

private PropertySource<?> loadIntoGroup(String identifier, String location,
      String profile) throws IOException {
   Resource resource = this.resourceLoader.getResource(location);
   PropertySource<?> propertySource = null;
   if (resource != null) {
      String name = "applicationConfig: [" + location + "]";
      String group = "applicationConfig: [" + identifier + "]";
      propertySource = this.propertiesLoader.load(resource, group, name,
            profile);
      if (propertySource != null) {
         handleProfileProperties(propertySource);
      }
   }
   //省略日誌打印
   ....
   return propertySource;
}
複製代碼

由 yml 加載器或者 property 加載器對文件加載後獲得 propertySource(裏面存放各類key-value變量),具體第三方加載器的加載細節此處暫時不討論。能夠看下 handleProfileProperties 方法的處理邏輯:

private void handleProfileProperties(PropertySource<?> propertySource) {
   Set<String> activeProfiles = getProfilesForValue(
         propertySource.getProperty(ACTIVE_PROFILES_PROPERTY));
   maybeActivateProfiles(activeProfiles);
   // INCLUDE_PROFILES_PROPERTY = "spring.profiles.include"
   Set<String> includeProfiles = getProfilesForValue(
         propertySource.getProperty(INCLUDE_PROFILES_PROPERTY));
   addProfiles(includeProfiles);
}
複製代碼

這裏又對 spring.profiles.include 指定的 profile 參數進行了處理。

綁定屬性到 environment 中

bindToSpringApplication(environment, application);
複製代碼

對該方法進一步跟進,能夠發現最終進入下面方法:

private void doBindPropertiesToTarget() throws BindException {
   RelaxedDataBinder dataBinder = (this.targetName != null
         ? new RelaxedDataBinder(this.target, this.targetName)
         : new RelaxedDataBinder(this.target));
   //驗證
   if (this.validator != null) {
      dataBinder.setValidator(this.validator);
   }
   if (this.conversionService != null) {
      dataBinder.setConversionService(this.conversionService);
   }
   //設置各類屬性
   dataBinder.setIgnoreNestedProperties(this.ignoreNestedProperties);
   dataBinder.setIgnoreInvalidFields(this.ignoreInvalidFields);
   dataBinder.setIgnoreUnknownFields(this.ignoreUnknownFields);
   customizeBinder(dataBinder);
   Set<String> names = getNames();
   PropertyValues propertyValues = getPropertyValues(names);
   //執行真正的綁定
   dataBinder.bind(propertyValues);
   if (this.validator != null) {
      validate(dataBinder);
   }
}
複製代碼

dataBinder.bind(propertyValues) 方法執行真正的數據綁定工做。

protected void doBind(MutablePropertyValues mpvs) {
   checkAllowedFields(mpvs);
   checkRequiredFields(mpvs);
   applyPropertyValues(mpvs);
}
複製代碼
protected void applyPropertyValues(MutablePropertyValues mpvs) {
   try {
      // 執行綁定
      getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
   }
   //異常處理
   ...
}
複製代碼

總結

此文對 springboot 加載配置文件的流程進行了一個粗線條的分析,不少細節尚未分析到位,可是主幹邏輯已然分析清楚了,不足之處,留待後面完善了。

相關文章
相關標籤/搜索