美文网首页
SpringBoot外部化配置相关源码API剖析和扩展

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

作者: just_like_you | 来源:发表于2020-11-24 10:27 被阅读0次
    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后缀
      

    相关文章

      网友评论

          本文标题:SpringBoot外部化配置相关源码API剖析和扩展

          本文链接:https://www.haomeiwen.com/subject/tlciiktx.html