SpringBoot外部化配置相关源码API剖析和扩展

1. 什么是外部化配置?

个人理解是spring提供的属性配置和环境切换功能。核心Api为Environment抽象,而springboot的配置文件(proepreties/yaml)的加载和其密不可分,springboot会从默认的location位置加载数据源并设置到Environment中。根据配置环境来进行属性源的优先级调整

Environment相关类图

Environment类图
2. 加载springboot外部化配置文件在2.4.0和之前版本有较大改动,下面分析会根据不同版本进行不同分析
2.1 springboot2.3以及之前版本
  • 在SpringApplication启动的时候在prepareEnvironment阶段会发送ApplicationEnvironmentPreparedEvent事件

  • EnvironmentPostProcessorApplicationListener接受到事件会将spring.factories文件中所有的EnvironmentPostProcessor加载并回调其postProcessEnvironment()方法,此时很重要的ConfigFileApplicationListener#postProcessEnvironment()会被回调

    • 添加Random PropertySource
      protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        //1. 添加Random PropertySource到Environment中
          RandomValuePropertySource.addToEnvironment(environment);
        //2. 创建Loader内置类,传入Environment执行load方法
          new Loader(environment, resourceLoader).load();
      }
    
    • 创建Load对象
          Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
               //1. 传入外部化配置环境对象
          this.environment = environment;
          //2. 实例化占位符解析器
              this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
          //3. 创建资源加载对象
              this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);
          //4. 以及最重要的加载spring.factories文件中所有的PropertySourceLoader(内置两种:properteies/yaml)
              this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                      getClass().getClassLoader());
          }
    
    • 正式进行load外部化配置资源

              void load() {
            //调用静态方法进行加载
            //1. 环境对象
            //2. defaultProperties PropertySource 加载的profiles位置 spring.profiles.active / include
            //3. 处理加载逻辑
                  FilteredPropertySource.apply(this.environment, DefaultPropertiesPropertySource.NAME, LOAD_FILTERED_PROPERTY,
                          this::loadWithFilteredProperties);
              }
      
      • 替换如果defaultProperties存在的话,这个属性是SpringApplication构造的时候传入的属性源

          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;
              }
            //构造成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相关参数
                  while (!this.profiles.isEmpty()) { //2. 将获取到的profile参数依次出栈,进行加载
                      Profile profile = this.profiles.poll();
                      if (isDefaultProfile(profile)) { //3. 这里判断是否是默认的profile,其实这里方法名有奇异,其实应该是不是默认的profile会被添加到Envrionment中
                          addProfileToEnvironment(profile.getName());
                      }
              //4. 加载符合当前profile的配置文件,并添加到Environment最后面
                      load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
                      this.processedProfiles.add(profile);
                  }
                  //5. 加载 spring.config.name.fileExtension中剩余未加载的(翻阅多种情况,发现只可能在执行该方法的时候外部修改了envrionment的activeProfiles方法才可能进入)
                  load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
           //6. 将load到的PropertySource应用到Environment对象中
                  addLoadedPropertySources();
           //7. 应用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 的路径进行迭代
                  getSearchLocations().forEach((location) -> {
              //2. 封装成ConfigDataLocation
                      String nonOptionalLocation = ConfigDataLocation.of(location).getValue(); 
                      boolean isDirectory = location.endsWith("/");
              //3. 如果是目录则获取spring.config.name作为文件名称进行加载,不是则传递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. 进行加载,并传入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. 优先加载 spring.config.name-{profile}.fileExtension 并通过defaultFilter过滤的(没有spring.profiles的)
                      load(loader, profileSpecificFile, profile, defaultFilter, consumer); 
      //2. 然后加载 匹配spring.profiles和当前 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);
              }
      

      这步进行完毕之后会将所有的外部配置问价加载到org.springframework.boot.context.config.ConfigFileApplicationListener.Loader#loaded属性中

              private Map<Profile, MutablePropertySources> loaded; //保存所有已经加载的PropertySource
      
      • 最后应用propertySource和Profile就大功告成了
      //1. 应用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. 应用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对象结果

实验结果图
//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结果图
2.2 springboot 2.4版本配置加载

这个版本springboot重构了之前的外部化文件加载方式,并且添加了对各大元计算平台的支持,如Kubernetes的ConfigMap等. 重构了之前使用PropertySourceLoader进行外部化配置地址 -> propertySource的转变,其中核心Api类图如下

SpringBoot2.4核心类图

核心步骤

  • 通过SpringApplication的启动生命周期回调到ConfigDataEnvironmentPostProcessor的回调
  • spring.factories中获取ConfigDataLoader,ConfigDataLocationResolver 加载解析核心组件,并构造成ConfigDataEnvironment对象
  • 通过ConfigDataEnvironment#processAndApply()开始加载配置文件逻辑
  • 核心加载架构个人总结为三大步和三个阶段
    • 三大步
      • 通过ConfigDataLocationResolver将相关spring.config.import,spring.config.addtional-location,spring.config.location等资源定位路径下的spring.config.name-{profile}.fileExtension资源解析成ConfigDataResource
      • 通过ConfigDataLoaderConfigDataLocationResolver解析好的资源进行加载,将ConfigDataResource -> ConfigData , 其中ConfigData是一组ProeprtySource
      • 将加载好的ConfigData添加到Environment中
    • 三大阶段,核心对象为ConfigDataEnvironmentContributors,其中分了三个大阶段对外部化资源进行加载
      • 无profile无CloudPlatform阶段 , 这个阶段会使用三大步中前两步构造出ConfigData
      • 根据环境参数spring.main.cloud-platform或者环境变量参数来自动探测云计算厂商环境,从而进行二阶段加载
      • 设置profiles,使用Binder Api从绑定的ConfigurationPropertySource中获取spring.profiles / spring.config等资源,进行第三阶段的加载

详细步骤如下

  • ConfigDataEnvironmentPostProcessor回调
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();
      //执行ConfigData加载和应用
      getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
   }
   catch (UseLegacyConfigProcessingException ex) {
      this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]",
            ex.getConfigurationProperty()));
        //这里兼容了springboot2.4之前版本的实现,可以通过spring.config.use-legacy-processing=true来调整为之前的实现
      //若抛出UseLegacyConfigProcessingException异常则使用老的方式(ConfigFileApplicationListener)进行外部化文件配置加载
      postProcessUsingLegacyApplicationListener(environment, resourceLoader);
   }
}
  • ConfigDataEnvironment对象的构造
ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
      ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
   Binder binder = Binder.get(environment); //1. 绑定当前Environment对象
   UseLegacyConfigProcessingException.throwIfRequested(binder);
   this.logFactory = logFactory;
   this.logger = logFactory.getLog(getClass());
   //2. 从属性spring.config.on-not-found中获取文件找不到的执行逻辑
   this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
         .orElse(ConfigDataNotFoundAction.FAIL);
   this.bootstrapContext = bootstrapContext;
   this.environment = environment;
   //3. 从spring.factories中获取ConfigDataLocationResolver实现。(可以自己实现,扩展点之一)
   //4. 同时这里面会传入boostrapper/resourceLoader/Binder等参数用于构造参数反射
   this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
   this.additionalProfiles = additionalProfiles;
   //5. 从spring.factories中获取所有的ConfigDataLoader并用反射进行实例化
   this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext);
   //6. 创建ConfigDataEnvironmentContributors对象,里面会根据spring.config.import / location等默认定位参数初始化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 ,此时还没有导入到Environment中,执行完毕之后应该都是BOUND_IMPORT,且此时绑定了spring.config / spring.profiles相关的配置属性信息
   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); //构建激活的上下文对象,此时对元计算平台进行设置
   //4. 带云计算平台参数上下文进行二次迭代
   contributors = processWithoutProfiles(contributors, importer, activationContext);
   //5. 构建profile
   activationContext = withProfiles(contributors, activationContext);
   //6. 带profile参数进行第三次迭代
   contributors = processWithProfiles(contributors, importer, activationContext);
   //7. 应用到Environment对象中
   applyToEnvironment(contributors, activationContext);
}

内容比较复杂,核心为ConfigDataEnvironmrntContributor的几个阶段的处理,可以看其中的内部类Kind

enum Kind {
    //包含了所有的Contributors
   ROOT,
    //上面我们刚创建就属于这个状态
   INITIAL_IMPORT,
    //已经将内部PropertySource应用到Environment中的Contributors
   EXISTING,
    //刚解析构造好ConfigData,还没有绑定spring.config / spring.profiles等环境参数
   UNBOUND_IMPORT,
        //已经绑定好环境参数阶段
   BOUND_IMPORT;
}

接下来继续跟processAndApply()方法

  • processInitial : 处理 Kind为INITIAL_IMPORT类型的Contributros ,这里面也是主要的解析配置的地方
    ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
            ConfigDataActivationContext activationContext) {
        //1. 获取Import阶段,分导入前导入后
        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阶段. 设置好ActivationContext(相关云计算平台参数进行第二轮的迭代),进行相关云平台过滤
            //3阶段. 进行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());
                // 进行占位符解析
                PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
                        result, activationContext, true);
                Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
                ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
                // 绑定ConfigDataProperties 并进行替换
                result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                        result.getRoot().withReplacement(contributor, bound));
                continue;
            }
            //2.封装Resolver,Loader等相关操作上下文对象
            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, //返回设置好Child Contributor的结果集,然后继续下一次迭代
                    result.getRoot().withReplacement(contributor, contributorAndChildren));
            processed++;
        }
    }
  • getNextToProcess() : 用来获取ConfigDataEnvironmentContributes中下次个满足解析的Contributor

      private ConfigDataEnvironmentContributor getNextToProcess(ConfigDataEnvironmentContributors contributors,
              ConfigDataActivationContext activationContext, ImportPhase importPhase) {
          for (ConfigDataEnvironmentContributor contributor : contributors.getRoot()) {
              /**
               * 1. 刚进来是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/匹配当前环境)
          return contributor.isActive(activationContext) && contributor.hasUnprocessedImports(importPhase);
      }
    //下面是一些列的判断方法,依次递进。返回true表示当前为激活环境,现在阶段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;
    }
    
    • 构造ConfigDataLocationResolverConfigDataLoader等上下文对象
              //2.封装Resolver,Loader等相关操作上下文对象
              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);
    
    • resolveAndLoad : 解析configDataLocationConfigDataResource,随后ConfingDataLoader#loadConfigData,并返回Map<ConfigDataResource, ConfigData>映射关系,具体解析流程,我直接截取了最核心的解析和加载代码,如下
      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进行加载和解析
          // ConfigDataResolutionResult 包含了ConfigDataLocation 和ConfigDataResource(解析结果)
              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) {
         //进行解析
          List<ConfigDataResolutionResult> resolved = resolve(location, () -> resolver.resolve(context, location));
          if (profiles == null) {
              return resolved;
          }
          //下面是第三阶段用来进行profile环境加载
          List<ConfigDataResolutionResult> profileSpecific = resolve(location,
                  () -> resolver.resolveProfileSpecific(context, location, profiles));
          return merge(resolved, profileSpecific);
      }
    
    //ConfigDataImport#load() , 核心使用刚才加载到的ConfigDataResource列表进行ConfigDataLoader#load加载
    //我们可以通过在META-INF/spring.factories中配置我们自己实现的ConfigDataLoader进行扩展加载其他格式的外部化环境,
    // 比如最后我会演示扩展实现一个加载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. 这里有个细节,为什么是从后往前遍历?因为之前解析profile的时候是从优先级低 -> 高
          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)又是一个扩展点
                      ConfigData loaded = this.loaders.load(loaderContext, resource);
                      if (loaded != null) {
                          result.put(resource, loaded);
                      }
                  }
                  catch (ConfigDataNotFoundException ex) {
                      handle(ex, location);
                  }
              }
          }
          return Collections.unmodifiableMap(result);
      }
    

    这边核心的三步我们就完成了两步,解析和加载,随后就是一些重复逻辑,加载另外两阶段的配置,这边挑一些细节来展示,我们回到ConfigDataEnvironment#processAndApply(),刚刚执行完processInitia()方法逻辑,解析和加载了第一阶段,随便进行云计算厂商的配置整合,核心在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 ,此时还没有导入到Environment中,执行完毕之后应该都是BOUND_IMPORT,且此时绑定了spring.config / spring.profiles相关的配置属性信息
          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); //构建激活的上下文对象,此时对元计算平台进行设置
          //4. 带云计算平台参数上下文进行二次迭代
          contributors = processWithoutProfiles(contributors, importer, activationContext);
          //5. 构建profile
          activationContext = withProfiles(contributors, activationContext);
          //6. 带profile参数进行第三次迭代
          contributors = processWithProfiles(contributors, importer, activationContext);
          //7. 应用到Environment对象中
          applyToEnvironment(contributors, activationContext);
      }
    
    • 自动探测和整合第三场云厂商 ,如k8s等.详细可以参考CloudPlatform这个类,里面有自动探测和通过配置相关环境变量的方法来进行设置
      private CloudPlatform deduceCloudPlatform(Environment environment, Binder binder) {
          for (CloudPlatform candidate : CloudPlatform.values()) {
    //尝试从Environment上下文中获取spring.main.cloud-platform,若有指定对应的云计算厂商则直接返回对应的CloudPlatform
              if (candidate.isEnforced(binder)) { 
                  return candidate;
              }
          }
    //从环境变量中寻找是否有对应云平台的环境变量参数,比如k8s(svc相关环境参数): 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 {
              //优先设置构造SpringApplication的addtionalProfile
              Set<String> additionalProfiles = new LinkedHashSet<>(this.additionalProfiles);
          //设置include profile
              additionalProfiles.addAll(getIncludedProfiles(contributors, activationContext));
          //设置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;
          }
      }
    
    • 随后完成第三阶段的解析和加载以及最后应用到Environment中
          //6. 带profile参数进行第三次迭代
          contributors = processWithProfiles(contributors, importer, activationContext);
          //7. 应用到Environment对象中
          applyToEnvironment(contributors, activationContext);
    
    2.3 两种版本不同的加载优先级如下
    
    //springboot2.4之前
    //location优先级为: spring.config.addtional-location > spring.config.location or default
    //这里的default指springboot默认加载位置 classpath:/ classpath:/config/ ...
    //profile优先级:
    //spring.profiles.active > spring.profiles.include
    //且这里如果有spring.profiles指定的多环境格式,如下,此时加载test环境的时候,spring.profiles=test也会随后加载
    
    name=default
    spring.profiles.active=dev,prod
    spring.profiles.include=config,test
    #---
    spring.profiles=test
    name=default#test
    
    //springboot2.4之后
    //location优先级为: spring.config.import > addtionial-location > location
    //profile优先级
    //spring.profiles.include > active(之间还多了一个spring.config.group)
    
    总结:

    springboot2.4和之前版本实现有较大差距,前者扩展了通过spring.config.import导入资源,并且资源加载来源更加宽广了,springboot内建的实现甚至可以从svn中加载配置。而下面也将进行简单的两个版扩展配置的方式

    spring boot 2.4之前, 只需要实现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扩展
    • 实现ConfigDataLocationResolver , 和ConfigDataResource , 这种自定义实现将可以解析custom:前缀的资源,实现参考了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;
        }
    }
    
    • 实现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);
        }
    
    //输出结果如下,成功加载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后缀
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容