YAML 配置文件加载
概述
YAML(.yml / .yaml)是 Spring Boot 最常用的配置文件格式。从原始文件到 PropertySource 中的扁平键值对,中间经过了多文档解析、profile 匹配、行号追踪、占位符替换等多个处理环节。
本文深入拆解 YAML 加载的完整流程,涵盖从 SnakeYaml 解析到最终属性源注册的全部 8 个细节点。
本文基于 Spring Boot 3.x 源码分析。
1. 整体加载流程
application.yml 文件
│
├─ ConfigDataEnvironmentPostProcessor ← EnvironmentPostProcessor
│ └─ ConfigDataLoader.load()
│ └─ ConfigDataLocationResolver.resolve()
│ └─ StandardConfigDataLoader
│ └─ PropertySourceLoader.load()
│
├─ YamlPropertySourceLoader.load() ← 入口
│ └─ YamlProcessor.process()
│ └─ SnakeYaml: Yaml.loadAll() ← 解析原始 YAML
│ └─ 多文档块拆分
│ └─ 按 profile 匹配
│ └─ 扁平化为 Map<String, Object>
│ └─ OriginTrackedMapPropertySource
│ └─ 注册到 Environment
│
└─ PropertySourcesPlaceholderConfigurer ← refresh() 阶段
└─ postProcessBeanFactory()
└─ ${...} 占位符替换2. YamlPropertySourceLoader.load() 实现
2.1 源码
// YamlPropertySourceLoader.java
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
// 支持两种扩展名
return new String[] { "yml", "yaml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) {
// 1. 检查资源是否存在
if (!resource.exists()) {
return Collections.emptyList();
}
// 2. 创建 YamlProcessor 并加载
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(
resource).load();
// 3. 如果加载结果为空,返回空列表
if (loaded.isEmpty()) {
return Collections.emptyList();
}
// 4. 将每个文档块转换为 PropertySource
List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
for (int i = 0; i < loaded.size(); i++) {
String propertySourceName = name + " (document #" + i + ")";
propertySources.add(
new OriginTrackedMapPropertySource(
propertySourceName, loaded.get(i)));
}
return propertySources;
}
}2.2 返回多个 PropertySource 的场景
一个 application.yml 文件可能包含多个文档块(--- 分隔),每个文档块对应一个独立的 PropertySource。
# 文档 0:默认配置
spring:
application:
name: myapp
---
# 文档 1:dev profile
spring:
config:
activate:
on-profile: dev
server:
port: 8081输出两个 PropertySource:
application.yml (document #0) → name=myapp
application.yml (document #1) → server.port=8081 (only if dev profile active)3. YamlProcessor 解析 --- 多文档块
3.1 SnakeYaml.loadAll() 的使用
// OriginTrackedYamlLoader.java
class OriginTrackedYamlLoader extends YamlProcessor {
List<Map<String, Object>> load() {
// 使用 SnakeYaml 的 Yaml.loadAll() 解析 YAML
// loadAll() 会按 --- 分隔符将文档拆分为多个 Java 对象
List<Map<String, Object>> result = new ArrayList<>();
try (InputStream inputStream = new BufferedInputStream(
this.resource.getInputStream())) {
// 创建 SnakeYaml 的 Yaml 实例
Yaml yaml = createYaml();
// loadAll() 返回 Iterable<Object>,每个元素对应一个文档块
for (Object object : yaml.loadAll(inputStream)) {
if (object != null) {
// 每个文档块被解析为 Map<String, Object>
result.add((Map<String, Object>) object);
}
}
} catch (IOException ex) {
throw new IllegalStateException(
"Failed to load YAML from " + this.resource, ex);
}
return result;
}
private Yaml createYaml() {
// 创建 SnakeYaml 实例,使用 OriginTrackingConstructor
// 以保留原始位置信息(行号)
return new Yaml(new OriginTrackingConstructor());
}
}3.2 文档拆分示例
# application.yml 内容
server:
port: 8080
---
spring:
profiles: dev
server:
port: 8081
---
spring:
profiles: prod
server:
port: 8082SnakeYaml.loadAll() 处理:
loadAll() 遍历结果:
Document 0: {server: {port: 8080}}
Document 1: {spring: {profiles: dev}, server: {port: 8081}}
Document 2: {spring: {profiles: prod}, server: {port: 8082}}4. Profile 文档块的匹配
4.1 Profile 匹配逻辑
// YamlProcessor.java
public class YamlProcessor {
private final List<Document> documents = new ArrayList<>();
protected List<Map<String, Object>> process(
MatchCallback callback, Environment environment) {
// 1. 加载所有文档块
List<Map<String, Object>> loaded = load();
// 2. 逐个文档块判断是否匹配当前 profile
for (Map<String, Object> map : loaded) {
// 检查文档块中是否有 spring.profiles 或 spring.config.activate.on-profile
String profile = getProfile(map);
if (profile == null || matchesProfile(profile, environment)) {
// 3. 匹配 → 执行回调
Map<String, Object> flattened = flatten(map, profile);
callback.process(flattened, null);
}
}
return loaded;
}
private boolean matchesProfile(String profile, Environment environment) {
// 解析文档块声明的 profile
String[] profiles = StringUtils.trimArrayElements(
StringUtils.commaDelimitedListToStringArray(profile));
// 与当前 Environment 的活跃 profile 比较
for (String activeProfile : environment.getActiveProfiles()) {
for (String required : profiles) {
if (activeProfile.equals(required)) {
return true;
}
}
}
// 如果文档块声明了 "!dev"(排除 dev),则当前是 dev 时不匹配
for (String required : profiles) {
if (required.startsWith("!")) {
String excluded = required.substring(1);
if (environment.acceptsProfiles(
Profiles.of(excluded))) {
return false;
}
}
}
return false;
}
}4.2 匹配规则
# 规则 1:只匹配 dev profile
spring:
config:
activate:
on-profile: dev
# 规则 2:匹配 dev 或 test(逗号分隔 = OR)
spring:
config:
activate:
on-profile: dev,test
# 规则 3:排除 dev(! 前缀)
spring:
config:
activate:
on-profile: "!dev"
# 规则 4:无 profile 声明 → 所有 profile 都包含(默认文档块)
server:
port: 80804.3 新旧 profile 语法的对比
| 版本 | 语法 | 示例 |
|---|---|---|
| Spring Boot 2.x | spring.profiles | spring.profiles: dev |
| Spring Boot 3.x | spring.config.activate.on-profile | spring.config.activate.on-profile: dev |
Spring Boot 3.x 兼容两种语法,但推荐使用新语法。
5. OriginTrackedYamlLoader 的行号追踪
5.1 OriginTrackingConstructor 实现
// OriginTrackedYamlLoader.java 的内部类
private static class OriginTrackingConstructor
extends Constructor {
@Override
protected Object constructObject(Node node) {
// 当构造 SnakeYaml 的节点时,记录该节点在源文件中的位置
if (node instanceof MappingNode) {
// 遍历该 Map 的所有子节点
for (NodeTuple tuple : ((MappingNode) node).getValue()) {
// 为每个 key-value 对记录起始行号
Mark startMark = tuple.getKeyNode().getStartMark();
// startMark 包含: 行号 (line)、列号 (column)、字符索引 (index)
}
}
return super.constructObject(node);
}
}5.2 OriginTrackedValue 包装
// OriginTrackedValue.java
public class OriginTrackedValue implements OriginProvider {
private final Object value;
private final Origin origin;
private OriginTrackedValue(Object value, Origin origin) {
this.value = value;
this.origin = origin;
}
// 创建带行号追踪的值
static OriginTrackedValue of(Object value, int line, int column) {
// 创建 TextResourceOrigin 记录文件位置
TextResourceOrigin origin = new TextResourceOrigin(
null, new TextResourceOrigin.Location(line, column));
return new OriginTrackedValue(value, origin);
}
@Override
public Origin getOrigin() {
return this.origin; // 返回源文件位置
}
public Object getValue() {
return this.value; // 返回实际值
}
}5.3 行号在扁平化过程中的传递
# application.yml:10
server:
port: 8080 # ← 第 11 行扁平化后:
key = "server.port"
value = OriginTrackedValue.of("8080", line=11, column=3)5.4 行号最终用途
OriginTrackedValue 在 OriginTrackedMapPropertySource.getProperty() 中被提取:
// OriginTrackedMapPropertySource.java
public class OriginTrackedMapPropertySource extends MapPropertySource {
@Override
public Object getProperty(String name) {
Object value = super.getProperty(name);
if (value instanceof OriginTrackedValue) {
// 将 OriginTrackedValue 展开为实际值(丢弃 origin 信息)
return ((OriginTrackedValue) value).getValue();
}
return value;
}
// 专为 Actuator 提供的 origin 查询方法
public Origin getOrigin(String name) {
Object value = super.getProperty(name);
if (value instanceof OriginTrackedValue) {
return ((OriginTrackedValue) value).getOrigin();
}
return null;
}
}最终在 GET /actuator/env/xxx 端点的响应中展示:
{
"property": {
"value": "8080",
"origin": "application.yml:11:3"
}
}6. 多文档占位符 ${...} 解析
6.1 两阶段解析
占位符 \${...} 的解析不在 YamlPropertySourceLoader 中完成,而是分为两个阶段:
| 阶段 | 时机 | 处理器 | 作用范围 |
|---|---|---|---|
| 阶段 1 | prepareEnvironment() 中 | ConfigDataEnvironmentPostProcessor + PropertySourcesPlaceholdersResolver | 仅 ConfigData 加载时专用的占位符 |
| 阶段 2 | refresh() 中 | PropertySourcesPlaceholderConfigurer | 所有 PropertySource 中的占位符 |
6.2 PropertySourcesPlaceholderConfigurer 的处理
// PropertySourcesPlaceholderConfigurer.java
public class PropertySourcesPlaceholderConfigurer
implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) {
// 获取所有 PropertySource
PropertySources propertySources = getAppliedPropertySources();
// 创建 PropertySourcesPlaceholderResolver
PropertySourcesPlaceholderResolver resolver =
new PropertySourcesPlaceholderResolver(propertySources);
// 遍历所有 BeanDefinition,替换其中的 ${...} 占位符
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition bd = beanFactory.getBeanDefinition(beanName);
// 替换 property values 中的占位符
resolvePlaceholders(bd, resolver);
}
}
private void resolvePlaceholders(BeanDefinition bd,
PropertySourcesPlaceholderResolver resolver) {
// 使用 PropertyPlaceholderHelper 递归解析
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(
"${", "}", ":", true); // true = 忽略不可解析的占位符
// 递归替换(支持嵌套占位符: ${db.${db.type}.url})
for (MutablePropertyValues pv : bd.getPropertyValues()) {
String resolved = helper.replacePlaceholders(
pv.getValue().toString(), resolver::resolvePlaceholder);
pv.setValue(resolved);
}
}
}6.3 嵌套占位符支持
# application.yml
db:
type: mysql
mysql:
url: jdbc:mysql://localhost:3306/db
oracle:
url: jdbc:oracle:thin:@localhost:1521:db
# 引用时使用嵌套占位符
datasource:
url: ${db.${db.type}.url} # 解析为: ${db.mysql.url} → jdbc:mysql://localhost:3306/db7. @PropertySource 加载 YAML 的兼容
7.1 问题
@PropertySource 注解的标准用法只能加载 .properties 文件:
@Configuration
@PropertySource("classpath:db.properties") // ✅ 正常加载
@PropertySource("classpath:db.yml") // ❌ 3.x 前不支持7.2 兼容实现
Spring Boot 通过 PropertySourceLoader 统一接口实现了 .yml/.yaml 文件的兼容加载:
// PropertySourceLoader.java
public interface PropertySourceLoader {
/** 返回支持的文件扩展名 */
String[] getFileExtensions();
/** 加载属性源 */
List<PropertySource<?>> load(String name, Resource resource)
throws IOException;
}Spring Boot 内置的加载器注册在 spring.factories 中:
# META-INF/spring.factories
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader7.3 @PropertySource + YAML 的使用
@Configuration
// Spring Boot 通过 @PropertySource 的 factory 参数指定 YAML 加载器
@PropertySource(
value = "classpath:custom-config.yml",
factory = YamlPropertySourceLoader.class // ← 指定 YAML 加载器
)
public class CustomConfig {
// ...
}7.4 ResourcePropertySource 的自动检测
// ResourcePropertySource.java
public class ResourcePropertySource extends PropertiesPropertySource {
public static ResourcePropertySource from(String name, Resource resource) {
// 根据文件扩展名自动选择加载器
PropertySourceLoader loader = getLoaderForResource(resource);
if (loader != null) {
// 使用匹配的加载器(YamlPropertySourceLoader 或 PropertiesPropertySourceLoader)
List<PropertySource<?>> loaded = loader.load(name, resource);
// ... 返回第一个 PropertySource
}
// 默认使用 Properties 加载
return new ResourcePropertySource(name, resource);
}
}8. YAML 列表与对象的扁平化
8.1 扁平化流程
// YamlProcessor.java
private Map<String, Object> flatten(Map<String, Object> source,
String profile) {
Map<String, Object> result = new LinkedHashMap<>();
flatten(result, source, "");
return result;
}
private void flatten(Map<String, Object> result,
Map<String, Object> source, String prefix) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = prefix + entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
// 递归处理嵌套 Map
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
flatten(result, map, key + ".");
} else if (value instanceof List) {
// 处理列表 → 展开为 list[0]、list[1] ...
flattenList(result, key, (List<?>) value);
} else {
// 基本类型 → 直接存储
result.put(key, value);
}
}
}
private void flattenList(Map<String, Object> result,
String prefix, List<?> list) {
for (int i = 0; i < list.size(); i++) {
Object value = list.get(i);
String key = prefix + "[" + i + "]";
if (value instanceof Map) {
// 列表元素是对象 → 递归扁平化
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
flatten(result, map, key + ".");
} else {
// 列表元素是基本类型
result.put(key, value);
}
}
}8.2 扁平化示例
# 原始 YAML
myapp:
cache:
ttl: 3600
enabled: true
servers:
- host: 192.168.1.1
port: 8080
- host: 192.168.1.2
port: 8081
tags:
- web
- api扁平化结果:
myapp.cache.ttl = 3600
myapp.cache.enabled = true
myapp.servers[0].host = 192.168.1.1
myapp.servers[0].port = 8080
myapp.servers[1].host = 192.168.1.2
myapp.servers[1].port = 8081
myapp.tags[0] = web
myapp.tags[1] = api8.3 在 Binder 中的还原
扁平化的 key 在绑定阶段被 Binder 还原为结构化对象:
// Binder 在处理 myapp.servers[0].host 时
// 自动识别出 myapp.servers 是一个 List<ServerConfig>
// 并为索引 0 和 1 创建对应的 ServerConfig 对象9. application-{profile}.yml 的加载优先级
9.1 文件查找顺序
ConfigDataLocationResolver 负责解析配置文件名,按以下顺序尝试加载:
# 1. 默认配置
application.yml
# 2. profile 特定配置(按 active 顺序)
application-dev.yml ← 如果 dev 是 active
application-test.yml ← 如果 test 是 active
application-prod.yml ← 如果 prod 是 active
# 3. 被 include 的配置
application-common.yml ← 如果 common 被 spring.profiles.include9.2 加载源码
// StandardConfigDataLocationResolver.java
private List<StandardConfigDataResource> getResources(
ConfigDataLocation location, boolean root) {
List<StandardConfigDataResource> resources = new ArrayList<>();
// 1. 加载无 profile 的默认文件
resources.addAll(getDefaultConfigResources(location));
// 2. 加载 profile 特定的文件
for (String profile : this.environment.getActiveProfiles()) {
resources.addAll(
getProfileSpecificResources(location, profile));
}
// 3. 按优先级排序
resources.sort(Comparator.comparingInt(
StandardConfigDataResource::getPriority));
return resources;
}9.3 优先级示例
# application.yml —— 基础配置
server:
port: 8080
spring:
application:
name: myapp
---
spring:
config:
activate:
on-profile: dev
server:
port: 8081
---
spring:
config:
activate:
on-profile: prod
server:
port: 8082# application-dev.yml —— 专门的 dev 配置(文件名匹配)
server:
port: 9090加载优先级:
| PropertySource | 优先级 | server.port |
|---|---|---|
application.yml (document #0 — 默认) | 低 | 8080 |
application.yml (document #1 — dev profile) | 中 | 8081 |
application-dev.yml | 高 | 9090 |
最终 server.port = 9090(application-dev.yml 覆盖了 application.yml 中的 dev 文档块)
9.4 spring.profiles.include 的影响
# application.yml
spring:
profiles:
active: dev
include: common,audit加载的文件顺序:
1. application.yml (基础)
2. application-common.yml ← include 优先加载
3. application-audit.yml ← include 继续加载
4. application-dev.yml ← active 最后加载(优先级最高)active 的 profile 文件比 include 的文件优先级更高。
总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | YamlPropertySourceLoader.load() | 按 --- 分割文档块,每个块生成一个 OriginTrackedMapPropertySource |
| ② | YamlProcessor 解析 --- | SnakeYaml.loadAll() 按分隔符拆分 → Map<String, Object> 列表 |
| ③ | Profile 匹配 | spring.config.activate.on-profile 属性与 Environment.getActiveProfiles() 比对,支持 ,(OR)和 !(排除) |
| ④ | 行号追踪 | OriginTrackingConstructor 通过 SnakeYaml.getStartMark() 记录行列号 → 包装为 OriginTrackedValue |
| ⑤ | ${...} 占位符 | 两阶段解析:ConfigData 阶段 → refresh() 阶段 PropertySourcesPlaceholderConfigurer 递归替换 |
| ⑥ | @PropertySource 兼容 | PropertySourceLoader 统一接口 + factory = YamlPropertySourceLoader.class |
| ⑦ | YAML 列表扁平化 | Map 递归带前缀 → List 展开为 [0]/[1] 索引 → Binder 还原为结构化对象 |
| ⑧ | application-{profile}.yml 优先级 | default < profile 文档块 < application-{profile}.yml,active > include |