1. 什么是外部化配置?
个人理解是spring提供的属性配置和环境切换功能。核心Api为Environment抽象,而springboot的配置文件(proepreties/yaml)的加载和其密不可分,springboot会从默认的location位置加载数据源并设置到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
filteredProperties, Consumer > 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
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 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
loaded; //保存所有已经加载的PropertySource - 最后应用propertySource和Profile就大功告成了
//1. 应用PropertySource private void addLoadedPropertySources() { MutablePropertySources destination = this.environment.getPropertySources(); List
loaded = new ArrayList<>(this.loaded.values()); Collections.reverse(loaded); String lastAdded = null; Set 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 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
2.2 springboot 2.4版本配置加载
这个版本springboot重构了之前的外部化文件加载方式,并且添加了对各大元计算平台的支持,如Kubernetes的ConfigMap等. 重构了之前使用PropertySourceLoader进行外部化配置地址 -> propertySource的转变,其中核心Api类图如下
核心步骤
- 通过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
- 通过
ConfigDataLoader
将ConfigDataLocationResolver
解析好的资源进行加载,将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 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 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 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 imports = contributor.getImports();
this.logger.trace(LogMessage.format("Processing imports %s", imports));
//4. 解析到Map
Map 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; }
- 构造
ConfigDataLocationResolver
,ConfigDataLoader
等上下文对象
//2.封装Resolver,Loader等相关操作上下文对象 ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext( result, contributor, activationContext); ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
- 获取默认的定位资源并进行解析加载
//3. 从ConfigDataLocationContributor(ConfigDataProperties)中获取ConfigDataLocation(资源路径对象) List
imports = contributor.getImports(); this.logger.trace(LogMessage.format("Processing imports %s", imports)); //4. 解析到Map Map imported = importer.resolveAndLoad(activationContext, locationResolverContext, loaderContext, imports); -
resolveAndLoad
: 解析configDataLocation
为ConfigDataResource
,随后ConfingDataLoader#load
为ConfigData
,并返回Map
映射关系,具体解析流程,我直接截取了最核心的解析和加载代码,如下
Map
resolveAndLoad(ConfigDataActivationContext activationContext, ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext, List locations) { try { //1. 初始化import阶段profile为空 , 这个第三阶段会派上用场 Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null; //2. 使用ConfigDateLocationResolver进行加载和解析 // ConfigDataResolutionResult 包含了ConfigDataLocation 和ConfigDataResource(解析结果) List 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 resolve(ConfigDataLocationResolver> resolver, ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) { //进行解析 List resolved = resolve(location, () -> resolver.resolve(context, location)); if (profiles == null) { return resolved; } //下面是第三阶段用来进行profile环境加载 List 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 load(ConfigDataLoaderContext loaderContext, List candidates) throws IOException { Map 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
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
> load(String name, Resource resource) throws IOException { Map result = new ObjectMapper().readValue(resource.getURL(),new TypeReference - 配置文件
#自定义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
{ 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 resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) { List 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
{ @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后缀
- 构造