Spring Boot ConfigData 源码解析:配置资源发现、激活上下文、Profile 推导、递归 Import 与 ContributorIterator 后序遍历
阅读说明
本文以 Spring Boot 2.7.18 的实现为基线,沿着 ConfigDataEnvironment.processAndApply() 的真实执行链路展开。
文章中的代码主要保留方法名、条件和数据结构,省略日志、异常包装和与主线无关的细节,便于观察控制流。
全文严格区分五个动作:
- resolve:把
ConfigDataLocation解析为ConfigDataResource。 - load:
Loader把 Resource 读取为ConfigData和PropertySource。 - bind:从单个
PropertySource绑定spring.config.import与spring.config.activate。 - active:Contributor 在当前
ActivationContext下满足激活条件。 - apply:最终把有效的
PropertySource加入真实Environment。
ConfigData 不是"读取完 application.yml 就结束"的文件加载器,而是一个分阶段求解配置依赖图的过程。每一阶段都会持续展开当前能够确定的 import,直到 Contributor Tree 达到稳定状态。Profile 确定后,再补充 profile-specific 资源,最后统一应用到 Environment。
2. 启动入口与初始 Contributor Tree
ConfigData 的执行入口位于 Environment 准备阶段。
SpringApplication 在创建和刷新 ApplicationContext 之前,先准备 ConfigurableEnvironment。EnvironmentPostProcessorApplicationListener 调用 ConfigDataEnvironmentPostProcessor,后者构造 ConfigDataEnvironment 并执行 processAndApply()。
此时以下工作均尚未开始:
- BeanDefinition 扫描;
- 自动配置类解析;
- 单例 Bean 创建;
ApplicationContext刷新。
入口调用链如下:
SpringApplication.run()
└─ prepareEnvironment()
└─ EnvironmentPostProcessorApplicationListener
└─ ConfigDataEnvironmentPostProcessor.postProcessEnvironment()
└─ ConfigDataEnvironment.processAndApply()
2.1 初始 Contributor 的来源
ConfigDataEnvironment 构造时,会读取当前 Environment 中已经存在的 PropertySource,并将它们包装成 Kind.EXISTING 的 Contributor。
这些来源通常包括:
commandLineArgssystemPropertiessystemEnvironmentdefaultProperties
它们参与早期 Binder 查询和占位符解析,但不会被 ConfigData 机制再次导入。
随后,框架根据以下配置创建 INITIAL_IMPORT 节点:
spring.config.importspring.config.additional-locationspring.config.location
若没有显式配置 spring.config.location,则使用默认搜索位置,例如:
classpath:/
classpath:/config/
file:./
file:./config/
file:./config/*/
初始位置采用 Contributor 表示,而不是立即调用 Resolver,是为了让初始位置和后续 spring.config.import 位置都经过相同的处理流程:
选择待处理节点
↓
resolveAndLoad
↓
生成子 Contributor
↓
替换不可变树
初始树的逻辑结构如下:
ROOT
├─ EXISTING: commandLineArgs
├─ EXISTING: systemProperties
├─ EXISTING: systemEnvironment
├─ INITIAL_IMPORT: spring.config.import
├─ INITIAL_IMPORT: spring.config.additional-location
├─ INITIAL_IMPORT: spring.config.location / 默认搜索位置
└─ EXISTING: defaultProperties
INITIAL_IMPORT 不对应某个已经加载的文件,也不贡献 PropertySource。它只持有一组 ConfigDataLocation,用于在 withProcessedImports() 中触发第一次 resolveAndLoad()。
4. 贯穿全文的配置样例
下面的配置同时包含:
- 无条件 import;
- CloudPlatform 条件;
- Profile 条件;
- 递归 import;
- profile-specific 文件。
假定运行环境最终被识别为 Kubernetes,基础文档激活 dev Profile。
application.yml 的文档 0:
spring:
profiles:
active: dev
config:
import: classpath:common.yml
app:
source: base
application.yml 的文档 1:
spring:
config:
activate:
on-cloud-platform: kubernetes
import: classpath:k8s.yml
platform:
mode: kubernetes
application.yml 的文档 2:
spring:
config:
activate:
on-profile: dev
import: classpath:feature.yml
feature:
mode: dev-document
common.yml:
spring:
config:
import: classpath:db.yml
common:
enabled: true
db.yml:
db:
url: jdbc:mysql://localhost/base
k8s.yml:
platform:
service-discovery: dns
feature.yml:
feature:
enabled: true
application-dev.yml:
spring:
config:
import: classpath:dev-db.yml
app:
source: application-dev
dev-db.yml:
db:
pool-size: 20
这里必须区分"文件"和"文档"。
application.yml 会被 Loader 一次性读取,但其中三个 YAML 文档会成为三个独立 PropertySource,随后生成三个独立 Contributor。
每个 Contributor 都有自己的:imports、activate、active 状态、children。
因此,一个文件已经被 Loader 读取,并不表示文件中的每个文档都在当前阶段生效。
6. processAndApply 的三次导入处理
processAndApply() 会调用三次 withProcessedImports(),但 ImportPhase 只有两种:
BEFORE_PROFILE_ACTIVATIONAFTER_PROFILE_ACTIVATION
前两次调用都属于 BEFORE,差异在于 ActivationContext 的完整度。第三次在 Profiles 创建后进入 AFTER。
主干流程如下:
processAndApply() {
importer = new ConfigDataImporter(...);
contributors = processInitial(contributors, importer);
activationContext =
createActivationContext(contributors.getBinder(...));
contributors =
processWithoutProfiles(contributors, importer, activationContext);
activationContext =
withProfiles(contributors, activationContext);
contributors =
processWithProfiles(contributors, importer, activationContext);
applyToEnvironment(contributors, activationContext, ...);
}
6.1 第一次:context 为 null 的 BEFORE
processInitial() 传入 null ActivationContext。
ImportPhase.get(null) 返回 BEFORE_PROFILE_ACTIVATION。
没有 spring.config.activate 的文档,在 properties.isActive(null) 中仍然 active,因为其 activate 字段为 null。
声明了任意 activate 条件的文档会进入 Activate.isActive(null),并直接返回 false。
这一阶段不只是读取 application.yml 一层。
无条件文档的 imports 会持续展开,例如:
application.yml
↓ import
common.yml
↓ import
db.yml
外层 while 会一直运行,直到整条当前可见的无条件依赖链都完成绑定与 import 处理。
6.2 第二次:CloudPlatform 已知,Profiles 仍为 null 的 BEFORE
第一次达到稳定状态后,createActivationContext() 使用当前 Environment 与 Contributor-backed Binder 推导 CloudPlatform,并明确把 profiles 设置为 null。
推导顺序是:先检查配置中是否强制指定 CloudPlatform,再调用 CloudPlatform.getActive(environment)。
processWithoutProfiles() 再次调用 withProcessedImports()。
当前 phase 仍然是 BEFORE,但此前因为 on-cloud-platform 而 inactive 的文档,现在可以重新判断。
平台匹配时,这些文档的 import 会被展开。
只声明 on-profile 的文档仍然 inactive。
同时声明平台和 Profile 的文档要求两个条件同时满足,因此要等待第三阶段。
6.3 Profile 推导与第三次 AFTER
withProfiles() 从当前 Contributor Tree 构造 Binder,过滤带 IGNORE_PROFILES 的 PropertySource,并读取:
spring.profiles.activespring.profiles.defaultspring.profiles.includespring.profiles.groupadditionalProfiles
Profiles 构造器还会展开 Profile group,并使用 LinkedHashSet 保持顺序和终止循环。
随后 activationContext.withProfiles(profiles) 生成同时包含 CloudPlatform 与 Profiles 的新上下文。
processWithProfiles() 第三次调用 withProcessedImports()。
此时 activationContext.getProfiles() != null,所以 ImportPhase.get() 返回 AFTER_PROFILE_ACTIVATION。
这个阶段具备两项新增能力:
on-profile文档可以判断为 active;- Resolver 除了普通
resolve(),还会执行resolveProfileSpecific()。
标准文件 Resolver 因此能够发现:application-dev.yml、common-dev.yml、application-prod.yml。
8. 使用真实数据结构推演完整执行过程
8.1 初始树
忽略 EXISTING 节点后,初始树如下:
ROOT
└─ INITIAL_IMPORT
imports = [classpath:/]
children = {}
8.2 第一次 BEFORE:加载基础文档并展开无条件 import
INITIAL_IMPORT 被选中。它没有激活条件,imports 非空,并且 children 中没有 BEFORE key。
Resolver 在 profiles == null 时只执行普通 resolve,找到 application.yml。
Loader 读取三个 YAML 文档,生成一个 ConfigData。
asContributors() 按 PropertySource 逆序生成三个 UNBOUND_IMPORT,并通过 INITIAL_IMPORT.withChildren(BEFORE, children) 挂入新树:
INITIAL_IMPORT
└─ BEFORE
├─ UNBOUND application.yml#2
├─ UNBOUND application.yml#1
└─ UNBOUND application.yml#0
此时三个文档都已经物理读取,但尚未绑定 activate 和 import。
后序 priority-order 遍历首先找到 application.yml#2。由于 Kind 为 UNBOUND_IMPORT,不检查 active,执行 withBoundProperties(),得到:
BOUND application.yml#2
activate.on-profile = dev
imports = [feature.yml]
当前 context 为 null,因此 isActive(null) == false,feature.yml 暂不展开。
接下来绑定 application.yml#1:
BOUND application.yml#1
activate.on-cloud-platform = kubernetes
imports = [k8s.yml]
context 仍为 null,所以同样 inactive,k8s.yml 暂不展开。
最后绑定 application.yml#0:
BOUND application.yml#0
activate = null
imports = [common.yml]
spring.profiles.active = dev
它在 null context 下 active,因此下一轮会被选中,加载 common.yml,并写入 BEFORE children。
common.yml 先成为 UNBOUND common.yml,随后绑定为:
BOUND common.yml
imports = [db.yml]
它没有激活条件,所以继续加载 db.yml。db.yml 绑定后没有 imports,不再成为待处理节点。
第一次 BEFORE 达到稳定状态后的树如下:
INITIAL_IMPORT
└─ BEFORE
├─ BOUND application.yml#2
│ activate.on-profile = dev
│ imports = [feature.yml]
│ 当前 inactive
│
├─ BOUND application.yml#1
│ activate.on-cloud-platform = kubernetes
│ imports = [k8s.yml]
│ 当前 inactive
│
└─ BOUND application.yml#0
imports = [common.yml]
└─ BEFORE
└─ BOUND common.yml
imports = [db.yml]
└─ BEFORE
└─ BOUND db.yml
此时 application.yml#1 和 application.yml#2 已经完成 load 与 bind,但没有展开内部 import。
第一次阶段结束的条件不是"所有文件都激活",而是当前树中不存在:
UNBOUND_IMPORT- 以及不存在"active 且 BEFORE 尚未处理的 Contributor"
8.3 第二次 BEFORE:平台条件配置加入
createActivationContext() 推导出 CloudPlatform.KUBERNETES、profiles = null。
第二次后序遍历中,application.yml#1 的平台条件匹配:
activate.on-cloud-platform = kubernetes
当前平台 = Kubernetes
因此 isActive(context) == true,其 BEFORE key 尚不存在,所以加载 k8s.yml。
application.yml#1
activate.on-cloud-platform = kubernetes
isActive(context) = true
children[BEFORE] = [k8s.yml]
k8s.yml 完成绑定后没有 imports。
application.yml#2 仍然要求 dev Profile,而 profiles == null:
application.yml#2
activate.on-profile = dev
isActive(context) = false
所以保持 inactive。
8.4 withProfiles:从 active 配置中推导 dev
withProfiles() 创建的 Binder 会过滤 IGNORE_PROFILES,并对关键绑定启用 FAIL_ON_BIND_TO_INACTIVE_SOURCE。
基础文档 application.yml#0 当前 active,其中的 spring.profiles.active: dev 会被读取。
Profiles 同时处理:
spring.profiles.activespring.profiles.defaultspring.profiles.includespring.profiles.groupadditionalProfiles
完成 group 展开后,最终上下文为:
CloudPlatform = KUBERNETES
Profiles.active = [dev]
8.5 第三次 AFTER:Profile 条件与 profile-specific 文件加入
application.yml#2 重新判断时:
on-profile = dev
Profiles 接受 dev
因此变为 active。它的 AFTER key 不存在,所以 feature.yml 在 AFTER 阶段被加载并绑定。
已经在 BEFORE 处理过 imports 的节点,其 AFTER key 仍然不存在。因此,同一个 imports 会带着 Profiles 再交给 Resolver。
Resolver 执行:
resolve(location)
resolveProfileSpecific(location, profiles)
普通资源已经存在于 ConfigDataImporter.loaded,会被去重。只有新出现的 profile-specific Resource 才会进入加载结果。
INITIAL_IMPORT 在 AFTER 阶段重新解析 classpath:/。普通 application.yml 已经加载并被去重。
StandardConfigDataLocationResolver 针对 dev 生成 application-dev.yml。
StandardConfigDataLoader 会为该 PropertySource 标记 PROFILE_SPECIFIC。
application-dev.yml 先作为 UNBOUND_IMPORT 加入 AFTER children,再绑定得到:
imports = [dev-db.yml]
当前 Profile 已知,并且该文档 active,所以 dev-db.yml 继续在 AFTER 阶段展开。
最终树的大致结构如下:
INITIAL_IMPORT
├─ AFTER
│ └─ BOUND application-dev.yml
│ imports = [dev-db.yml]
│ └─ AFTER
│ └─ BOUND dev-db.yml
│
└─ BEFORE
├─ BOUND application.yml#2
│ activate.on-profile = dev
│ └─ AFTER
│ └─ BOUND feature.yml
│
├─ BOUND application.yml#1
│ activate.on-cloud-platform = kubernetes
│ └─ BEFORE
│ └─ BOUND k8s.yml
│
└─ BOUND application.yml#0
└─ BEFORE
└─ BOUND common.yml
└─ BEFORE
└─ BOUND db.yml
application.yml 的条件文档最初都位于 INITIAL_IMPORT 的 BEFORE children,因为它们来自第一次普通解析。它们内部的 import 在之后变为 active 时,会写入对应 phase 的 children。
application-dev.yml 来自 resolveProfileSpecific(),属于 profile-specific 资源,并在树结构调整后位于 AFTER 高优先级区域。
10. BEFORE 与 AFTER 的真实区别
ImportPhase 描述的是:当前节点的 imports 在 Profile 推导之前执行,还是在 Profile 推导之后执行。
它不直接表示节点 inactive 或 active。
ImportPhase.get() 的逻辑只有一个判断:
phase = activationContext != null
&& activationContext.getProfiles() != null
? AFTER_PROFILE_ACTIVATION
: BEFORE_PROFILE_ACTIVATION;
因此 BEFORE 表示 Profiles 尚未确定,AFTER 表示 Profiles 已经确定。
10.1 Resolver 行为的差异
ConfigDataImporter 从 ActivationContext 中取得 Profiles,并传给 ConfigDataLocationResolvers.resolve(...)。
Resolvers 总是先调用普通解析 resolver.resolve(context, location)。
如果 Profiles 为 null,直接返回普通解析结果。
如果 Profiles 不为 null,还会执行 resolver.resolveProfileSpecific(context, location, profiles)。
因此:
- 普通解析:
resolve(location) - Profile 已知后的解析:
resolve(location)+resolveProfileSpecific(location, profiles)
对于标准文件位置,resolveProfileSpecific() 会遍历 accepted profiles,并把 Profile 传入 StandardConfigDataReference。
例如:
- 目录
classpath:/产生application-dev.yml候选 - 明确文件
common.yml产生common-dev.yml候选
StandardConfigDataLoader 检测 resource.getProfile() != null 时,会给对应 PropertySource 添加 PROFILE_SPECIFIC。
10.2 同一个 imports 处理两次
一个 BOUND Contributor 的 imports 在 BEFORE 完成后,只会拥有 children[BEFORE]。
进入 AFTER 时,hasUnprocessedImports(AFTER) 仍然返回 true。因此,同一个 Location 会在两种上下文中各解析一次。
第一次:Profiles = null,只解析普通资源。
第二次:Profiles = [dev],解析普通资源 + profile-specific 资源。
ConfigDataImporter.loaded 使用 ConfigDataResource 去重,所以普通资源不会重复执行 Loader.load()。
第二次解析的价值在于发现此前不存在的 profile-specific Resource。
"处理两次"指的是:同一个配置位置在两种上下文中各执行一次 resolve,并不表示同一个文件一定被物理读取两次。
10.3 PROFILE_SPECIFIC 的树结构调整
withChildren(AFTER, children) 会调用 moveProfileSpecific()。
该方法会在 BEFORE 子树中寻找带 PROFILE_SPECIFIC Option 的子节点,把它们从原位置移出,去掉临时 Option 后加入当前节点的 AFTER children。
这个调整让 profile-specific 来源进入 Profile 激活后的高优先级区域,同时保留非 profile-specific import 的原有父子关系。
下面两个状态相关,但含义不同:
fromProfileSpecificImport:来自ConfigDataResolutionResult.isProfileSpecific(),表示 Resource 是由resolveProfileSpecific()产生的。PROFILE_SPECIFICOption:附着在PropertySource上,用于后续树结构与优先级调整。
12. Options、去重、异常与最终优先级
12.1 ConfigData.Option 的来源与作用
Options 由 ConfigDataLoader 在构造 ConfigData 时提供,并按照 PropertySource 传递给 ofUnboundImport()。
Spring Boot 不会根据"本地配置、远程配置、配置中心"等类型统一推断这些标记。自定义 Loader 决定返回哪些 Options。
| Option | 设置位置 | 消费位置 | 作用 |
|---|---|---|---|
IGNORE_IMPORTS | Loader 或 EMPTY_LOCATION 固定选项 | withBoundProperties() | 绑定后移除 spring.config.import,普通属性仍可贡献 |
IGNORE_PROFILES | 由 Loader 决定 | withProfiles()、include 扫描、非法属性校验 | 不让该 PropertySource 参与 active、default、include、group 推导 |
PROFILE_SPECIFIC | StandardConfigDataLoader 在 resource.profile != null 时设置 | moveProfileSpecific() | 把 profile-specific 来源移动到 AFTER 高优先级区域 |
EMPTY_LOCATION 使用 IGNORE_IMPORTS,表示该 Location 合法,但没有实际 PropertySource,不应再从空节点中读取 import。
IGNORE_PROFILES 的精确定义是:忽略该 PropertySource 中影响 Profile 推导的属性。它不等同于"远程配置必然忽略 Profile"。
12.2 Resource 去重与循环 import
ConfigDataImporter 维护 Set<ConfigDataResource> loaded。
候选 Resource 已存在时,不会再次调用 Loader。
对应 Location 会被记录到 loadedLocations。
假设 A import B,B import A:
load A
loaded = [A]
A import B
load B
loaded = [A, B]
B 再解析出 A 时
loaded.contains(A) == true
→ 不会生成新的 Contributor,循环自然终止
去重依据是 ConfigDataResource.equals() 与 ConfigDataResource.hashCode(),而不是字符串 Location。
不同 Location 解析到同一个 Resource 时,同样只加载一次。
同一个 Location 在 AFTER 阶段解析出新的 profile-specific Resource 时,因为 Resource 不同,仍然可以加载。
12.3 optional、空位置与强制位置检查
optional: 前缀和 Resource 自身的 optional 状态,会被 ConfigDataImporter 记录。
ConfigDataNotFoundException 对 optional 位置采用忽略策略。非 optional 位置则按照 spring.config.on-not-found 的策略处理。
Resolver 可能返回一个合法的空目录 Resource。Loader 返回 ConfigData.EMPTY,随后创建 EMPTY_LOCATION Contributor。
最终 apply 前,checkMandatoryLocations() 会收集 active Contributor 中的非 optional imports,再减去:
- 树中已经出现的 Location
loadedLocationsoptionalLocations
仍然存在的 Location 被视为没有成功解析的强制 import。
12.4 最终 apply 与属性优先级
applyToEnvironment() 先执行:
- 非法 Profile 属性校验;
- 强制位置检查。
随后使用 ContributorIterator 遍历整棵树。
只有满足以下条件的节点会加入 Environment:
Kind == BOUND_IMPORT
PropertySource != null
在最终 ActivationContext 下 active
结构化逻辑如下:
for (contributor : contributors) {
if (kind == BOUND_IMPORT
&& propertySource != null) {
if (contributor.isActive(finalContext)) {
environment
.getPropertySources()
.addLast(propertySource);
}
}
}
inactive 节点仍然留在 Contributor Tree 中,但不会进入 Environment。
命令行参数、systemProperties 等 EXISTING PropertySource 已经位于 Environment 中,并且在 ConfigData 结果之前,所以保持更高优先级。
ConfigData 节点内部依赖 AFTER → BEFORE → parent 的遍历顺序。
最后 DefaultPropertiesPropertySource 会被移动到末尾,维持默认属性最低优先级。
14. 完整执行模型
ConfigData 的完整执行可以压缩为以下连续过程。
第一步,包装已有 Environment PropertySource,创建 INITIAL_IMPORT,组成 ROOT。
ROOT
├─ EXISTING
├─ INITIAL_IMPORT
└─ EXISTING
第二步,执行第一次 BEFORE。
context = null
Profiles = null
CloudPlatform = null
持续加载并绑定无条件文档,递归展开无条件 imports。
第三步,创建 ActivationContext。从 Environment 与 Contributor Binder 推导 CloudPlatform,Profiles 仍然为 null。
第四步,执行第二次 BEFORE。重新判断平台条件,持续展开 on-cloud-platform 文档的 imports。
第五步,计算 Profiles:
- 过滤
IGNORE_PROFILES - 读取 active
- 读取 default
- 读取 include
- 读取 additionalProfiles
- 展开 group
第六步,执行第三次 AFTER。
- 重新判断 on-profile 条件。
- 每个 imports 带着 Profiles 再解析。
- 发现
application-dev.yml、common-dev.yml、其他 profile-specific Resource。 - 使用 loaded Resource 集合去重,并继续递归展开新节点。
第七步,每个阶段内部都执行同一套不动点算法:
后序 priority-order 遍历
↓
选择一个待处理节点
↓
绑定或者加载
↓
withReplacement 生成新树
↓
从根重新遍历
↓
直到当前阶段稳定
第八步,执行最终 apply。
- 校验强制位置
- 校验非法 Profile 属性
- 只加入 active BOUND PropertySource
- 按照
AFTER → BEFORE → parent排序 - 设置 Environment 的 defaultProfiles 和 activeProfiles
最终心智模型如下:
- Contributor Tree 是 ConfigData 的中间表示
- ActivationContext 决定当前哪些节点可以继续展开
- ImportPhase 决定位置解析时是否拥有 Profiles
- ContributorIterator 给出优先级后序遍历
- withProcessedImports 的 while 把当前可求解的配置依赖持续展开到稳定
- applyToEnvironment 才把最终结果交给真实 Environment