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
1
2
3
4
5
6protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
//1. 添加Random PropertySource到Environment中
RandomValuePropertySource.addToEnvironment(environment);
//2. 创建Loader内置类,传入Environment执行load方法
new Loader(environment, resourceLoader).load();
}- 创建
Load 对象
1
2
3
4
5
6
7
8
9
10
11Loader(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外部化配置资源
1
2
3
4
5
6
7
8void load() {
//调用静态方法进行加载
//1. 环境对象
//2. defaultProperties PropertySource 加载的profiles位置 spring.profiles.active / include
//3. 处理加载逻辑
FilteredPropertySource.apply(this.environment, DefaultPropertiesPropertySource.NAME, LOAD_FILTERED_PROPERTY,
this::loadWithFilteredProperties);
}-
替换如果
defaultProperties 存在的话,这个属性是SpringApplication构造的时候传入的属性源1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18static 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) 加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22private 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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57private 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 属性中1private Map<Profile, MutablePropertySources> loaded; //保存所有已经加载的PropertySource- 最后应用propertySource和Profile就大功告成了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32//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对象结果

实验结果图
1 2 3 4 5 6 7 8 9 10 11 | //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 - 通过
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 回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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对象的构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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() ,整个外部化配置解析的核心框架,这里能明显看到我上面说明的三大阶段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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); } |
内容比较复杂,核心为
1 2 3 4 5 6 7 8 9 10 11 12 | enum Kind { //包含了所有的Contributors ROOT, //上面我们刚创建就属于这个状态 INITIAL_IMPORT, //已经将内部PropertySource应用到Environment中的Contributors EXISTING, //刚解析构造好ConfigData,还没有绑定spring.config / spring.profiles等环境参数 UNBOUND_IMPORT, //已经绑定好环境参数阶段 BOUND_IMPORT; } |
接下来继续跟
-
processInitial : 处理Kind为INITIAL_IMPORT 类型的Contributros ,这里面也是主要的解析配置的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38private 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 等上下文对象
1
2
3
4//2.封装Resolver,Loader等相关操作上下文对象
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
result, contributor, activationContext);
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);- 获取默认的定位资源并进行解析加载
1
2
3
4
5
6//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 : 解析configDataLocation 为ConfigDataResource ,随后ConfingDataLoader#load 为ConfigData ,并返回Map 映射关系,具体解析流程,我直接截取了最核心的解析和加载代码,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58Map<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() 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void 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 这个类,里面有自动探测和通过配置相关环境变量的方法来进行设置
1
2
3
4
5
6
7
8
9
10private 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属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private 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中
1
2
3
4//6. 带profile参数进行第三次迭代
contributors = processWithProfiles(contributors, importer, activationContext);
//7. 应用到Environment对象中
applyToEnvironment(contributors, activationContext);2.3 两种版本不同的加载优先级如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//自定义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));
}
}- 配置文件
1
2
3#自定义ConfigDataLocationResolver -> ConfigDataLocation -> ConfigDataResource
org.springframework.boot.context.config.ConfigDataLocationResolver=\
boot.in.action.bootsourcelearning.configdata.CustomConfigDataLocationResolverspring boot2.4扩展
- 实现
ConfigDataLocationResolver , 和ConfigDataResource , 这种自定义实现将可以解析custom: 前缀的资源,实现参考了ConfigTreeDataLocationResolver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public 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
1
2
3
4
5
6
7
8public 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)));
}
}- 配置文件
1
2
3
4
5
6#自定义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- 测试输入程序如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public 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后缀 - 构造