SpringBoot外部化配置相關(guān)源碼API剖析和擴(kuò)展

1. 什么是外部化配置导犹?

個人理解是spring提供的屬性配置和環(huán)境切換功能贯要。核心Api為Environment抽象睛藻,而springboot的配置文件(proepreties/yaml)的加載和其密不可分怀樟,springboot會從默認(rèn)的location位置加載數(shù)據(jù)源并設(shè)置到Environment中薛训。根據(jù)配置環(huán)境來進(jìn)行屬性源的優(yōu)先級調(diào)整

Environment相關(guān)類圖

Environment類圖
2. 加載springboot外部化配置文件在2.4.0和之前版本有較大改動,下面分析會根據(jù)不同版本進(jìn)行不同分析
2.1 springboot2.3以及之前版本
  • 在SpringApplication啟動的時候在prepareEnvironment階段會發(fā)送ApplicationEnvironmentPreparedEvent事件

  • EnvironmentPostProcessorApplicationListener接受到事件會將spring.factories文件中所有的EnvironmentPostProcessor加載并回調(diào)其postProcessEnvironment()方法辈毯,此時很重要的ConfigFileApplicationListener#postProcessEnvironment()會被回調(diào)

    • 添加Random PropertySource
      protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        //1. 添加Random PropertySource到Environment中
          RandomValuePropertySource.addToEnvironment(environment);
        //2. 創(chuàng)建Loader內(nèi)置類坝疼,傳入Environment執(zhí)行l(wèi)oad方法
          new Loader(environment, resourceLoader).load();
      }
    
    • 創(chuàng)建Load對象
          Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
               //1. 傳入外部化配置環(huán)境對象
          this.environment = environment;
          //2. 實例化占位符解析器
              this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
          //3. 創(chuàng)建資源加載對象
              this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
          //4. 以及最重要的加載spring.factories文件中所有的PropertySourceLoader(內(nèi)置兩種:properteies/yaml)
              this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                      getClass().getClassLoader());
          }
    
    • 正式進(jìn)行l(wèi)oad外部化配置資源

              void load() {
            //調(diào)用靜態(tài)方法進(jìn)行加載
            //1. 環(huán)境對象
            //2. defaultProperties PropertySource 加載的profiles位置 spring.profiles.active / include
            //3. 處理加載邏輯
                  FilteredPropertySource.apply(this.environment, DefaultPropertiesPropertySource.NAME, LOAD_FILTERED_PROPERTY,
                          this::loadWithFilteredProperties);
              }
      
      • 替換如果defaultProperties存在的話,這個屬性是SpringApplication構(gòu)造的時候傳入的屬性源

          static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
                  Consumer<PropertySource<?>> operation) {
              MutablePropertySources propertySources = environment.getPropertySources();
              PropertySource<?> original = propertySources.get(propertySourceName);
            //1. 查看defaultProperties是否存在
              if (original == null) {
                  operation.accept(null);
                  return;
              }
            //構(gòu)造成FilteredPropertySource谆沃,然后加載并替換
              propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
              try {
                  operation.accept(original);
              }
              finally {
                  propertySources.replace(propertySourceName, original);
              }
          }
        
      • loadWithFilteredProperties(PropertySource<?> defaultProperties)加載

      private void loadWithFilteredProperties(PropertySource<?> defaultProperties) {
                  this.profiles = new LinkedList<>();
                  this.processedProfiles = new LinkedList<>();
                  this.activatedProfiles = false;
                  this.loaded = new LinkedHashMap<>();
                  initializeProfiles(); //1. 初始化profiles相關(guān)參數(shù)
                  while (!this.profiles.isEmpty()) { //2. 將獲取到的profile參數(shù)依次出棧钝凶,進(jìn)行加載
                      Profile profile = this.profiles.poll();
                      if (isDefaultProfile(profile)) { //3. 這里判斷是否是默認(rèn)的profile,其實這里方法名有奇異唁影,其實應(yīng)該是不是默認(rèn)的profile會被添加到Envrionment中
                          addProfileToEnvironment(profile.getName());
                      }
              //4. 加載符合當(dāng)前profile的配置文件耕陷,并添加到Environment最后面
                      load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
                      this.processedProfiles.add(profile);
                  }
                  //5. 加載 spring.config.name.fileExtension中剩余未加載的(翻閱多種情況,發(fā)現(xiàn)只可能在執(zhí)行該方法的時候外部修改了envrionment的activeProfiles方法才可能進(jìn)入)
                  load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
           //6. 將load到的PropertySource應(yīng)用到Environment對象中
                  addLoadedPropertySources();
           //7. 應(yīng)用profile到Environment中
              applyActiveProfiles(defaultProperties);
              }
      
      • 這里面核心只需要觀察load(profile, this::getPositiveProfileFilter,addToLoaded(MutablePropertySources::addLast, false));
      
       private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
                  //1. 依次獲取 spring.cofnig.addtional-location / location / default localtioni 的路徑進(jìn)行迭代
                  getSearchLocations().forEach((location) -> {
              //2. 封裝成ConfigDataLocation
                      String nonOptionalLocation = ConfigDataLocation.of(location).getValue(); 
                      boolean isDirectory = location.endsWith("/");
              //3. 如果是目錄則獲取spring.config.name作為文件名稱進(jìn)行加載据沈,不是則傳遞null
                      Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES; 
              //4. load資源
                      names.forEach((name) -> load(nonOptionalLocation, name, profile, filterFactory, consumer));
                  });
              }
      
      // --------------
      
              private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
                                DocumentConsumer consumer) {
              //....省略部分非核心方法
                  Set<String> processed = new HashSet<>();
            //1. 會迭代我們從spring.factories中獲取到的PropertySourceLoader(porperties/yaml)
                  for (PropertySourceLoader loader : this.propertySourceLoaders) {
                      for (String fileExtension : loader.getFileExtensions()) {
                          if (processed.add(fileExtension)) {
                  //2. 進(jìn)行加載哟沫,并傳入profile,拼接的路徑名稱..
                              loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                                      consumer);
                          }
                      }
                  }
              }
      // --------------
      
              //加載核心流程 profile ->  null -> (若沒有profile)default -> include -> active
              private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
                                                Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
                  DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null); // positive: if document.profiles.isEmpty() return true
                  DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile); // positive: if document.profiles contains profile return true | profile = true and document.profiles.isEmptry()
                  if (profile != null) {
                      // Try profile-specific file & profile section in profile file (gh-340)
                      String profileSpecificFile = prefix + "-" + profile + fileExtension;
      //1. 優(yōu)先加載 spring.config.name-{profile}.fileExtension 并通過defaultFilter過濾的(沒有spring.profiles的)
                      load(loader, profileSpecificFile, profile, defaultFilter, consumer); 
      //2. 然后加載 匹配spring.profiles和當(dāng)前 profile匹配的特有屬性
                      load(loader, profileSpecificFile, profile, profileFilter, consumer); 
                      // Try profile specific sections in files we've already processed
                      for (Profile processedProfile : this.processedProfiles) {
                          if (processedProfile != null) {
                              String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
                  //3. 加載之前加載過的profile中包含 spring.profiles的特有屬性
                              load(loader, previouslyLoaded, profile, profileFilter, consumer); 
                          }
                      }
                  }
                  // Also try the profile-specific section (if any) of the normal file
             //4. 最后加載spring.config.name.fileExtension 中spring.profiles不為空的
                  load(loader, prefix + fileExtension, profile, profileFilter, consumer);
              }
      

      這步進(jìn)行完畢之后會將所有的外部配置問價加載到org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loaded屬性中

              private Map<Profile, MutablePropertySources> loaded; //保存所有已經(jīng)加載的PropertySource
      
      • 最后應(yīng)用propertySource和Profile就大功告成了
      //1. 應(yīng)用PropertySource
      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<>();
                  for (MutablePropertySources sources : loaded) {
                      for (PropertySource<?> source : sources) {
                          if (added.add(source.getName())) {
                  //依次添加到Environment中
                              addLoadedPropertySource(destination, lastAdded, source);
                              lastAdded = source.getName();
                          }
                      }
                  }
              }
      //2. 應(yīng)用profile
              private void applyActiveProfiles(PropertySource<?> defaultProperties) {
                  List<String> activeProfiles = new ArrayList<>();
                  if (defaultProperties != null) {
                      Binder binder = new Binder(ConfigurationPropertySources.from(defaultProperties),
                              new PropertySourcesPlaceholdersResolver(this.environment));
                      activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.include"));
                      if (!this.activatedProfiles) {
                          activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.active"));
                      }
                  }
                  this.processedProfiles.stream().filter(this::isDefaultProfile).map(Profile::getName)
                          .forEach(activeProfiles::add);
                  this.environment.setActiveProfiles(activeProfiles.toArray(new String[0]));
              }
      
  • 最后上一下自己的實驗配置屬性圖锌介,和最后加載的Environment對象結(jié)果

實驗結(jié)果圖
//classpath:/applicaiton.properties
name=default
spring.profiles.active=dev,prod
spring.profiles.include=config,test

#---
spring.profiles=test
name=default#test
#---
spring.profiles=negative
name=default#negative

Environment#propertySource

PropertySource結(jié)果圖
2.2 springboot 2.4版本配置加載

這個版本springboot重構(gòu)了之前的外部化文件加載方式嗜诀,并且添加了對各大元計算平臺的支持,如Kubernetes的ConfigMap等. 重構(gòu)了之前使用PropertySourceLoader進(jìn)行外部化配置地址 -> propertySource的轉(zhuǎn)變孔祸,其中核心Api類圖如下

SpringBoot2.4核心類圖

核心步驟

  • 通過SpringApplication的啟動生命周期回調(diào)到ConfigDataEnvironmentPostProcessor的回調(diào)
  • spring.factories中獲取ConfigDataLoader,ConfigDataLocationResolver 加載解析核心組件,并構(gòu)造成ConfigDataEnvironment對象
  • 通過ConfigDataEnvironment#processAndApply()開始加載配置文件邏輯
  • 核心加載架構(gòu)個人總結(jié)為三大步和三個階段
    • 三大步
      • 通過ConfigDataLocationResolver將相關(guān)spring.config.import,spring.config.addtional-location,spring.config.location等資源定位路徑下的spring.config.name-{profile}.fileExtension資源解析成ConfigDataResource
      • 通過ConfigDataLoaderConfigDataLocationResolver解析好的資源進(jìn)行加載筑公,將ConfigDataResource -> ConfigData 尊浪, 其中ConfigData是一組ProeprtySource
      • 將加載好的ConfigData添加到Environment中
    • 三大階段匣屡,核心對象為ConfigDataEnvironmentContributors捣作,其中分了三個大階段對外部化資源進(jìn)行加載
      • 無profile無CloudPlatform階段 , 這個階段會使用三大步中前兩步構(gòu)造出ConfigData
      • 根據(jù)環(huán)境參數(shù)spring.main.cloud-platform或者環(huán)境變量參數(shù)來自動探測云計算廠商環(huán)境鹅士,從而進(jìn)行二階段加載
      • 設(shè)置profiles,使用Binder Api從綁定的ConfigurationPropertySource中獲取spring.profiles / spring.config等資源,進(jìn)行第三階段的加載

詳細(xì)步驟如下

  • ConfigDataEnvironmentPostProcessor回調(diào)
void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
      Collection<String> additionalProfiles) {
   try {
      this.logger.trace("Post-processing environment to add config data");
      resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
      //執(zhí)行ConfigData加載和應(yīng)用
      getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
   }
   catch (UseLegacyConfigProcessingException ex) {
      this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]",
            ex.getConfigurationProperty()));
        //這里兼容了springboot2.4之前版本的實現(xiàn),可以通過spring.config.use-legacy-processing=true來調(diào)整為之前的實現(xiàn)
      //若拋出UseLegacyConfigProcessingException異常則使用老的方式(ConfigFileApplicationListener)進(jìn)行外部化文件配置加載
      postProcessUsingLegacyApplicationListener(environment, resourceLoader);
   }
}
  • ConfigDataEnvironment對象的構(gòu)造
ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
      ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
   Binder binder = Binder.get(environment); //1. 綁定當(dāng)前Environment對象
   UseLegacyConfigProcessingException.throwIfRequested(binder);
   this.logFactory = logFactory;
   this.logger = logFactory.getLog(getClass());
   //2. 從屬性spring.config.on-not-found中獲取文件找不到的執(zhí)行邏輯
   this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
         .orElse(ConfigDataNotFoundAction.FAIL);
   this.bootstrapContext = bootstrapContext;
   this.environment = environment;
   //3. 從spring.factories中獲取ConfigDataLocationResolver實現(xiàn)以舒。(可以自己實現(xiàn)蔓钟,擴(kuò)展點之一)
   //4. 同時這里面會傳入boostrapper/resourceLoader/Binder等參數(shù)用于構(gòu)造參數(shù)反射
   this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
   this.additionalProfiles = additionalProfiles;
   //5. 從spring.factories中獲取所有的ConfigDataLoader并用反射進(jìn)行實例化
   this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext);
   //6. 創(chuàng)建ConfigDataEnvironmentContributors對象滥沫,里面會根據(jù)spring.config.import / location等默認(rèn)定位參數(shù)初始化Contributor
   this.contributors = createContributors(binder);
}
  • 解析并加載 processAndApply(),整個外部化配置解析的核心框架兰绣,這里能明顯看到我上面說明的三大階段
void processAndApply() {
   //1. 封裝ConfigDataImporter對象缀辩,里面有解析ConfigDataLocation -> ConfigDataResource 和load ConfigDataResource -> ConfigData之類的操作
   ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
         this.loaders);
   this.bootstrapContext.register(Binder.class, InstanceSupplier
         .from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)));
   //1. 加載和解析ConfigDataLocation -> ConfigDataResource -> ConfigData ,此時還沒有導(dǎo)入到Environment中雌澄,執(zhí)行完畢之后應(yīng)該都是BOUND_IMPORT,且此時綁定了spring.config / spring.profiles相關(guān)的配置屬性信息
   ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
   //2. 獲取包含Root Contributor中 所有ConfigurationPropertySource的Binder
   Binder initialBinder = contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
   //3. 重新注冊Binder到Bootstrapper中
   this.bootstrapContext.register(Binder.class, InstanceSupplier.of(initialBinder));
   ConfigDataActivationContext activationContext = createActivationContext(initialBinder); //構(gòu)建激活的上下文對象,此時對元計算平臺進(jìn)行設(shè)置
   //4. 帶云計算平臺參數(shù)上下文進(jìn)行二次迭代
   contributors = processWithoutProfiles(contributors, importer, activationContext);
   //5. 構(gòu)建profile
   activationContext = withProfiles(contributors, activationContext);
   //6. 帶profile參數(shù)進(jìn)行第三次迭代
   contributors = processWithProfiles(contributors, importer, activationContext);
   //7. 應(yīng)用到Environment對象中
   applyToEnvironment(contributors, activationContext);
}

內(nèi)容比較復(fù)雜,核心為ConfigDataEnvironmrntContributor的幾個階段的處理旗唁,可以看其中的內(nèi)部類Kind

enum Kind {
    //包含了所有的Contributors
   ROOT,
    //上面我們剛創(chuàng)建就屬于這個狀態(tài)
   INITIAL_IMPORT,
    //已經(jīng)將內(nèi)部PropertySource應(yīng)用到Environment中的Contributors
   EXISTING,
    //剛解析構(gòu)造好ConfigData,還沒有綁定spring.config / spring.profiles等環(huán)境參數(shù)
   UNBOUND_IMPORT,
        //已經(jīng)綁定好環(huán)境參數(shù)階段
   BOUND_IMPORT;
}

接下來繼續(xù)跟processAndApply()方法

  • processInitial : 處理 Kind為INITIAL_IMPORT類型的Contributros 检疫,這里面也是主要的解析配置的地方
    ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
            ConfigDataActivationContext activationContext) {
        //1. 獲取Import階段屎媳,分導(dǎo)入前導(dǎo)入后
        ImportPhase importPhase = ImportPhase.get(activationContext);
        this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
                (activationContext != null) ? activationContext : "no activation context"));
        ConfigDataEnvironmentContributors result = this;
        int processed = 0;
        while (true) {
            //1階段. 初始化為null
            //2階段. 設(shè)置好ActivationContext(相關(guān)云計算平臺參數(shù)進(jìn)行第二輪的迭代),進(jìn)行相關(guān)云平臺過濾
            //3階段. 進(jìn)行profile文件的解析和加載
            ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
            if (contributor == null) {
                this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
                return result;
            }
            if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
                //從UNBOUND_IMPORT Contributor中獲取配置屬性源
                Iterable<ConfigurationPropertySource> sources = Collections
                        .singleton(contributor.getConfigurationPropertySource());
                // 進(jìn)行占位符解析
                PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
                        result, activationContext, true);
                Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
                ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
                // 綁定ConfigDataProperties 并進(jìn)行替換
                result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                        result.getRoot().withReplacement(contributor, bound));
                continue;
            }
            //2.封裝Resolver,Loader等相關(guān)操作上下文對象
            ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
                    result, contributor, activationContext);
            ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
            //3. 從ConfigDataLocationContributor(ConfigDataProperties)中獲取ConfigDataLocation(資源路徑對象)
            List<ConfigDataLocation> imports = contributor.getImports();
            this.logger.trace(LogMessage.format("Processing imports %s", imports));
            //4. 解析到Map<ConfigDataResource, ConfigData>
            Map<ConfigDataResource, ConfigData> imported = importer.resolveAndLoad(activationContext,
                    locationResolverContext, loaderContext, imports);
            this.logger.trace(LogMessage.of(() -> imported.isEmpty() ? "Nothing imported" : "Imported "
                    + imported.size() + " resource " + ((imported.size() != 1) ? "s" : "") + imported.keySet()));
            ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
                    asContributors(imported));
            result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext, //返回設(shè)置好Child Contributor的結(jié)果集,然后繼續(xù)下一次迭代
                    result.getRoot().withReplacement(contributor, contributorAndChildren));
            processed++;
        }
    }
  • getNextToProcess() : 用來獲取ConfigDataEnvironmentContributes中下次個滿足解析的Contributor

      private ConfigDataEnvironmentContributor getNextToProcess(ConfigDataEnvironmentContributors contributors,
              ConfigDataActivationContext activationContext, ImportPhase importPhase) {
          for (ConfigDataEnvironmentContributor contributor : contributors.getRoot()) {
              /**
               * 1. 剛進(jìn)來是INITIAL_IMPORT
               * 2. activationContext = null
               * 3. importPhase = BEFORE_PROFILE_ACTIVATION
               */
              if (contributor.getKind() == Kind.UNBOUND_IMPORT
                      || isActiveWithUnprocessedImports(activationContext, importPhase, contributor)) {
                  return contributor;
              }
          }
          return null;
      }
    
      private boolean isActiveWithUnprocessedImports(ConfigDataActivationContext activationContext,
              ImportPhase importPhase, ConfigDataEnvironmentContributor contributor) {
          //ConfigDataProperties -> ConfigDataActivationContext (前兩者為null為true) ( onCloudPlatform -> Profiles) (為null為true/匹配當(dāng)前環(huán)境)
          return contributor.isActive(activationContext) && contributor.hasUnprocessedImports(importPhase);
      }
    //下面是一些列的判斷方法,依次遞進(jìn)。返回true表示當(dāng)前為激活環(huán)境双泪,現(xiàn)在階段Kind為INITIAL_IMPORT焙矛,且activation為null
    //所以會返回true
      boolean isActive(ConfigDataActivationContext activationContext) {
          return this.properties == null || this.properties.isActive(activationContext);
      }
      boolean isActive(ConfigDataActivationContext activationContext) {
          return this.activate == null || this.activate.isActive(activationContext);
      }
    boolean isActive(ConfigDataActivationContext activationContext) {
      if (activationContext == null) {
        return false;
      }
      boolean activate = true;
      activate = activate && isActive(activationContext.getCloudPlatform());
      activate = activate && isActive(activationContext.getProfiles());
      return activate;
    }
    
    • 構(gòu)造ConfigDataLocationResolver薄扁,ConfigDataLoader等上下文對象
              //2.封裝Resolver,Loader等相關(guān)操作上下文對象
              ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
                      result, contributor, activationContext);
              ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
    
    • 獲取默認(rèn)的定位資源并進(jìn)行解析加載
    //3. 從ConfigDataLocationContributor(ConfigDataProperties)中獲取ConfigDataLocation(資源路徑對象)
    List<ConfigDataLocation> imports = contributor.getImports();
    this.logger.trace(LogMessage.format("Processing imports %s", imports));
    //4. 解析到Map<ConfigDataResource, ConfigData>
    Map<ConfigDataResource, ConfigData> imported = importer.resolveAndLoad(activationContext,
          locationResolverContext, loaderContext, imports);
    
    • resolveAndLoad : 解析configDataLocationConfigDataResource,隨后ConfingDataLoader#loadConfigData脱盲,并返回Map<ConfigDataResource, ConfigData>映射關(guān)系钱反,具體解析流程匣距,我直接截取了最核心的解析和加載代碼毅待,如下
      Map<ConfigDataResource, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,
              ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,
              List<ConfigDataLocation> locations) {
          try {
              //1. 初始化import階段profile為空 , 這個第三階段會派上用場
              Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
              //2. 使用ConfigDateLocationResolver進(jìn)行加載和解析
          // ConfigDataResolutionResult 包含了ConfigDataLocation 和ConfigDataResource(解析結(jié)果)
              List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
              //3. 使用ConfigDataLoader將ConfigDataResource -> ConfigData -> (PropertySource)
              return load(loaderContext, resolved);
          }
          catch (IOException ex) {
              throw new IllegalStateException("IO error on loading imports from " + locations, ex);
          }
      }
    
    // resolver() 核心 ,根這上面resolve方法一直跟就找到了,ConfigDataLocationResolvers#resolve()
      private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver,
              ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
         //進(jìn)行解析
          List<ConfigDataResolutionResult> resolved = resolve(location, () -> resolver.resolve(context, location));
          if (profiles == null) {
              return resolved;
          }
          //下面是第三階段用來進(jìn)行profile環(huán)境加載
          List<ConfigDataResolutionResult> profileSpecific = resolve(location,
                  () -> resolver.resolveProfileSpecific(context, location, profiles));
          return merge(resolved, profileSpecific);
      }
    
    //ConfigDataImport#load() , 核心使用剛才加載到的ConfigDataResource列表進(jìn)行ConfigDataLoader#load加載
    //我們可以通過在META-INF/spring.factories中配置我們自己實現(xiàn)的ConfigDataLoader進(jìn)行擴(kuò)展加載其他格式的外部化環(huán)境吱涉,
    // 比如最后我會演示擴(kuò)展實現(xiàn)一個加載json文件的Loader
      private Map<ConfigDataResource, ConfigData> load(ConfigDataLoaderContext loaderContext,
              List<ConfigDataResolutionResult> candidates) throws IOException {
          Map<ConfigDataResource, ConfigData> result = new LinkedHashMap<>();
          //1. 從后向前迭代ConfigDataResolutionResult(包含ConfigDataLocation,ConfigDataResource)
        //2. 這里有個細(xì)節(jié)怎爵,為什么是從后往前遍歷鳖链?因為之前解析profile的時候是從優(yōu)先級低 -> 高
          for (int i = candidates.size() - 1; i >= 0; i--) {
              ConfigDataResolutionResult candidate = candidates.get(i);
              ConfigDataLocation location = candidate.getLocation();
              ConfigDataResource resource = candidate.getResource();
              if (this.loaded.add(resource)) { //set緩存并去重
                  try {
                      //2. ConfigDataLoader加載將ConfigDataResource -> ConfigData (PropetySource)又是一個擴(kuò)展點
                      ConfigData loaded = this.loaders.load(loaderContext, resource);
                      if (loaded != null) {
                          result.put(resource, loaded);
                      }
                  }
                  catch (ConfigDataNotFoundException ex) {
                      handle(ex, location);
                  }
              }
          }
          return Collections.unmodifiableMap(result);
      }
    

    這邊核心的三步我們就完成了兩步芙委,解析和加載题山,隨后就是一些重復(fù)邏輯顶瞳,加載另外兩階段的配置慨菱,這邊挑一些細(xì)節(jié)來展示,我們回到ConfigDataEnvironment#processAndApply()符喝,剛剛執(zhí)行完processInitia()方法邏輯协饲,解析和加載了第一階段,隨便進(jìn)行云計算廠商的配置整合描馅,核心在createActivationContext()

      void processAndApply() {
          //1. 封裝ConfigDataImporter對象铭污,里面有解析ConfigDataLocation -> ConfigDataResource 和load ConfigDataResource -> ConfigData之類的操作
          ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
                  this.loaders);
          this.bootstrapContext.register(Binder.class, InstanceSupplier
                  .from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)));
          //1. 加載和解析ConfigDataLocation -> ConfigDataResource -> ConfigData ,此時還沒有導(dǎo)入到Environment中嘹狞,執(zhí)行完畢之后應(yīng)該都是BOUND_IMPORT,且此時綁定了spring.config / spring.profiles相關(guān)的配置屬性信息
          ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
          //2. 獲取包含Root Contributor中 所有ConfigurationPropertySource的Binder
          Binder initialBinder = contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
          //3. 重新注冊Binder到Bootstrapper中
          this.bootstrapContext.register(Binder.class, InstanceSupplier.of(initialBinder));
          ConfigDataActivationContext activationContext = createActivationContext(initialBinder); //構(gòu)建激活的上下文對象,此時對元計算平臺進(jìn)行設(shè)置
          //4. 帶云計算平臺參數(shù)上下文進(jìn)行二次迭代
          contributors = processWithoutProfiles(contributors, importer, activationContext);
          //5. 構(gòu)建profile
          activationContext = withProfiles(contributors, activationContext);
          //6. 帶profile參數(shù)進(jìn)行第三次迭代
          contributors = processWithProfiles(contributors, importer, activationContext);
          //7. 應(yīng)用到Environment對象中
          applyToEnvironment(contributors, activationContext);
      }
    
    • 自動探測和整合第三場云廠商 知市,如k8s等.詳細(xì)可以參考CloudPlatform這個類,里面有自動探測和通過配置相關(guān)環(huán)境變量的方法來進(jìn)行設(shè)置
      private CloudPlatform deduceCloudPlatform(Environment environment, Binder binder) {
          for (CloudPlatform candidate : CloudPlatform.values()) {
    //嘗試從Environment上下文中獲取spring.main.cloud-platform,若有指定對應(yīng)的云計算廠商則直接返回對應(yīng)的CloudPlatform
              if (candidate.isEnforced(binder)) { 
                  return candidate;
              }
          }
    //從環(huán)境變量中尋找是否有對應(yīng)云平臺的環(huán)境變量參數(shù),比如k8s(svc相關(guān)環(huán)境參數(shù)): KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT
          return CloudPlatform.getActive(environment);
      }
    
    • 獲取Environment中的profile屬性
      private ConfigDataActivationContext withProfiles(ConfigDataEnvironmentContributors contributors,
              ConfigDataActivationContext activationContext) {
          this.logger.trace("Deducing profiles from current config data environment contributors");
          Binder binder = contributors.getBinder(activationContext, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
          try {
              //優(yōu)先設(shè)置構(gòu)造SpringApplication的addtionalProfile
              Set<String> additionalProfiles = new LinkedHashSet<>(this.additionalProfiles);
          //設(shè)置include profile
              additionalProfiles.addAll(getIncludedProfiles(contributors, activationContext));
          //設(shè)置active profile 规哲、 default profile
              Profiles profiles = new Profiles(this.environment, binder, additionalProfiles);
              return activationContext.withProfiles(profiles);
          }
          catch (BindException ex) {
              if (ex.getCause() instanceof InactiveConfigDataAccessException) {
                  throw (InactiveConfigDataAccessException) ex.getCause();
              }
              throw ex;
          }
      }
    
    • 隨后完成第三階段的解析和加載以及最后應(yīng)用到Environment中
          //6. 帶profile參數(shù)進(jìn)行第三次迭代
          contributors = processWithProfiles(contributors, importer, activationContext);
          //7. 應(yīng)用到Environment對象中
          applyToEnvironment(contributors, activationContext);
    
    2.3 兩種版本不同的加載優(yōu)先級如下
    
    //springboot2.4之前
    //location優(yōu)先級為: spring.config.addtional-location > spring.config.location or default
    //這里的default指springboot默認(rèn)加載位置 classpath:/ classpath:/config/ ...
    //profile優(yōu)先級:
    //spring.profiles.active > spring.profiles.include
    //且這里如果有spring.profiles指定的多環(huán)境格式,如下,此時加載test環(huán)境的時候袄简,spring.profiles=test也會隨后加載
    
    name=default
    spring.profiles.active=dev,prod
    spring.profiles.include=config,test
    #---
    spring.profiles=test
    name=default#test
    
    //springboot2.4之后
    //location優(yōu)先級為: spring.config.import > addtionial-location > location
    //profile優(yōu)先級
    //spring.profiles.include > active(之間還多了一個spring.config.group)
    
    總結(jié):

    springboot2.4和之前版本實現(xiàn)有較大差距绿语,前者擴(kuò)展了通過spring.config.import導(dǎo)入資源吕粹,并且資源加載來源更加寬廣了匹耕,springboot內(nèi)建的實現(xiàn)甚至可以從svn中加載配置。而下面也將進(jìn)行簡單的兩個版擴(kuò)展配置的方式

    spring boot 2.4之前, 只需要實現(xiàn)PropertySourceLoader接口然后添加到META-INF/spring.factories即可
    • 自定義CustomPropertySourceLoader
    //自定義json后綴資源加載器
    public class CustomPropertySourceLoader implements PropertySourceLoader {
    
        public static final String CUSTOM_PREFIX = "json";
    
        @Override
        public String[] getFileExtensions() {
            return new String[]{CUSTOM_PREFIX};
        }
    
        @Override
        public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
            Map<String, Object> result  =  new ObjectMapper().readValue(resource.getURL(),new TypeReference<Map<String,Object>>(){});
            return Collections.singletonList(new MapPropertySource("JSON_PROPERTY_SOURCE", result));
        }
    }
    
    • 配置文件
    #自定義ConfigDataLocationResolver -> ConfigDataLocation -> ConfigDataResource
    org.springframework.boot.context.config.ConfigDataLocationResolver=\
    boot.in.action.bootsourcelearning.configdata.CustomConfigDataLocationResolver
    
    spring boot2.4擴(kuò)展
    • 實現(xiàn)ConfigDataLocationResolver , 和ConfigDataResource , 這種自定義實現(xiàn)將可以解析custom:前綴的資源煤傍,實現(xiàn)參考了ConfigTreeDataLocationResolver
    public class CustomConfigDataLocationResolver implements ConfigDataLocationResolver<CustomConfigDataResource> {
    
        public static final String CUSTOM_CONFIG_PREFIX = "custom:";
    
        @Override
        public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
            return location.hasPrefix(CUSTOM_CONFIG_PREFIX) && location.getValue().endsWith(".properties");
        }
    
        @Override
        public List<CustomConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
            List<CustomConfigDataResource> result = new ArrayList<>();
            try {
                Resource[] resources = new PathMatchingResourcePatternResolver().getResources(location.getValue().substring(CUSTOM_CONFIG_PREFIX.length()));
                for (Resource resource : resources) {
                    result.add(new CustomConfigDataResource(PropertiesLoaderUtils.loadProperties(resource)));
                }
            } catch (IOException e) {
                if (location.isOptional()) {
                    log.warn("not found resource :{}", location.getValue());
                } else {
                    ReflectionUtils.rethrowRuntimeException(e);
                }
            }
            return result;
        }
    }
    
    • 實現(xiàn)ConfigDataLoader
    public class CustomConfigDataLoader implements ConfigDataLoader<CustomConfigDataResource> {
    
        @Override
        public ConfigData load(ConfigDataLoaderContext context, CustomConfigDataResource resource) throws ConfigDataResourceNotFoundException {
            Properties properties = resource.getProperties();
            return new ConfigData(Collections.singleton(new PropertiesPropertySource("FILE_PROPERTY_SOURCE", properties)));
        }
    }
    
    • 配置文件
    
    #自定義ConfigDataLocation ConfigDataResource->ConfigData
    org.springframework.boot.context.config.ConfigDataLoader=\
    boot.in.action.bootsourcelearning.configdata.CustomConfigDataLoader
    
    org.springframework.boot.env.PropertySourceLoader=\
    boot.in.action.bootsourcelearning.configdata.CustomPropertySourceLoader
    
    • 測試輸入程序如下
        public static void main(String[] args) {
            ConfigurableApplicationContext context = new SpringApplicationBuilder(BootSourceLearningApplication.class)
    //                .properties("spring.config.use-legacy-processing=true")
                    .properties("spring.config.additional-location=classpath:/custom/")
                    .properties("spring.config.import=classpath:/custom/custom.json,optional:custom:/custom/custom.properties")
                    .applicationStartup(new BufferingApplicationStartup(2048))
                    .web(WebApplicationType.SERVLET)
                    .run(args);
            context.getEnvironment().getPropertySources().forEach(System.out::println);
        }
    
    //輸出結(jié)果如下,成功加載custom前綴和 .json后綴的PropertySource
    MapPropertySource {name='server.ports'}
    ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
    StubPropertySource {name='servletConfigInitParams'}
    ServletContextPropertySource {name='servletContextInitParams'}
    PropertiesPropertySource {name='systemProperties'}
    OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
    RandomValuePropertySource {name='random'}
    PropertiesPropertySource {name='FILE_PROPERTY_SOURCE'} //custom前綴
    MapPropertySource {name='JSON_PROPERTY_SOURCE'} //.json后綴
    
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蒋失,一起剝皮案震驚了整個濱河市桐玻,隨后出現(xiàn)的幾起案子镊靴,更是在濱河造成了極大的恐慌偏竟,老刑警劉巖踊谋,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件殖蚕,死亡現(xiàn)場離奇詭異睦疫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)宛官,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門进宝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枷恕,“玉大人徐块,你說我怎么就攤上這事胡控≈缂ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長凡傅。 經(jīng)常有香客問我夏跷,道長槽华,這世上最難降的妖魔是什么猫态? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任懂鸵,我火速辦了婚禮匆光,結(jié)果婚禮上终息,老公的妹妹穿的比我還像新娘。我一直安慰自己柳譬,他們只是感情好美澳,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布制跟。 她就那樣靜靜地躺著雨膨,像睡著了一般聊记。 火紅的嫁衣襯著肌膚如雪排监。 梳的紋絲不亂的頭發(fā)上社露,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天峭弟,我揣著相機(jī)與錄音瞒瘸,去河邊找鬼熄浓。 笑死赌蔑,一個胖子當(dāng)著我的面吹牛娃惯,可吹牛的內(nèi)容都是我干的趾浅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼纽谒,長吁一口氣:“原來是場噩夢啊……” “哼鼓黔!你這毒婦竟也來了请祖?” 一聲冷哼從身側(cè)響起肆捕,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤慎陵,失蹤者是張志新(化名)和其女友劉穎席纽,沒想到半個月后润梯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甥厦,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡舶赔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年竟纳,在試婚紗的時候發(fā)現(xiàn)自己被綠了锥累。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揩悄。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡删性,死狀恐怖蹬挺,靈堂內(nèi)的尸體忽然破棺而出巴帮,到底是詐尸還是另有隱情榕茧,我是刑警寧澤用押,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布蜻拨,位于F島的核電站缎讼,受9級特大地震影響血崭,放射性物質(zhì)發(fā)生泄漏厘灼。R本人自食惡果不足惜手幢,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一跺涤、第九天 我趴在偏房一處隱蔽的房頂上張望桶错。 院中可真熱鬧院刁,春花似錦粪狼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艺蝴。三九已至鸟废,卻和暖如春侮攀,著一層夾襖步出監(jiān)牢的瞬間兰英,已是汗流浹背畦贸。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工趋厉, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留胶坠,地道東北人沈善。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓乡数,卻偏偏與公主長得像,于是被迫代替她去往敵國和親闻牡。 傳聞我的和親對象是個殘疾皇子净赴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容