PropertySourceLoader 族全解析
概述
PropertySourceLoader 是 Spring Boot 中负责将配置文件资源转换为 PropertySource 对象的核心接口。它包括 4 个内置实现,分别对应 .properties、.yml/.yaml、.json 和 .xml 四种配置文件格式。
本文深入拆解每个 Loader 的加载源码、行号追踪机制和适用场景。
本文基于 Spring Boot 3.x 源码分析。
1. PropertySourceLoader 接口定义
1.1 接口源码
java
// PropertySourceLoader.java
public interface PropertySourceLoader {
/**
* 返回此 Loader 支持的文件扩展名。
* 返回值不带开头的 .(点号)。
* 例如: ["properties"] 或 ["yml", "yaml"]
*/
String[] getFileExtensions();
/**
* 将 Resource 加载为 PropertySource 列表。
* 返回 List 而非单个 PropertySource,因为:
* - .yml 文件可能包含多个文档块(--- 分隔)
* - 每个文档块生成一个独立的 PropertySource
*
* @param name 属性源的名称
* @param resource 要加载的资源文件
* @return PropertySource 列表(可能为空)
* @throws IOException 加载失败时抛出
*/
List<PropertySource<?>> load(String name, Resource resource)
throws IOException;
}1.2 返回 List<PropertySource<?>> 的原因
单个 .properties 文件: → List[1] ← 只有 1 个 PropertySource
单个 .yml 文件 (含 ---): → List[N] ← N 个文档块对应 N 个 PropertySource1.3 注册方式
java
// META-INF/spring.factories (Spring Boot 3.x)
# PropertySourceLoader 实现
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader,\
org.springframework.boot.env.JsonPropertySourceLoader,\
org.springframework.boot.env.XmlPropertySourceLoader2. PropertiesPropertySourceLoader 加载 .properties
2.1 源码
java
// PropertiesPropertySourceLoader.java
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "properties", "xml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
// 1. 使用 OriginTrackedPropertiesLoader 逐行解析
// 这一步会记录每个属性值的行号
Map<String, Object> properties =
new OriginTrackedPropertiesLoader(resource).load();
// 2. 如果解析结果为空 → 返回空列表
if (properties.isEmpty()) {
return Collections.emptyList();
}
// 3. 创建 OriginTrackedMapPropertySource(带行号追踪)
return Collections.singletonList(
new OriginTrackedMapPropertySource(
name, properties));
}
}2.2 加载流程
.properties 文件
│
└─ PropertiesPropertySourceLoader.load()
│
└─ OriginTrackedPropertiesLoader.load()
│
├─ ① 逐行读取文件(BufferedReader)
│
├─ ② 按 = 或 : 分隔键值
│
├─ ③ 记录每行的 Origin(行号 + 列号)
│
└─ ④ 返回 Map<String, OriginTrackedValue>
│
└─ 包装为 OriginTrackedMapPropertySource3. OriginTrackedPropertiesLoader 的行号追踪
3.1 源码
java
// OriginTrackedPropertiesLoader.java
public class OriginTrackedPropertiesLoader {
private final Resource resource;
public OriginTrackedPropertiesLoader(Resource resource) {
this.resource = resource;
}
public Map<String, Object> load() throws IOException {
return load(true); // 默认进行行号追踪
}
public Map<String, Object> load(boolean trackOrigins) throws IOException {
// 临时 Map:存储扁平化后的属性
Map<String, Object> result = new LinkedHashMap<>();
try (Reader reader = new EncodedResource(this.resource)
.getCharacterEncoding() != null
? new InputStreamReader(this.resource.getInputStream(),
Objects.requireNonNull(
new EncodedResource(this.resource)
.getCharacterEncoding()))
: new InputStreamReader(this.resource.getInputStream())) {
// 逐行解析
int lineNumber = 0;
String line;
BufferedReader bufferedReader = new BufferedReader(reader);
while ((line = bufferedReader.readLine()) != null) {
lineNumber++;
// 跳过空行和注释
String trimmed = line.trim();
if (trimmed.isEmpty()
|| trimmed.startsWith("#")
|| trimmed.startsWith("!")) {
continue;
}
// 完整行号 = 文件行号
processLine(line, lineNumber, result, trackOrigins);
}
}
return result;
}
private void processLine(String line, int lineNumber,
Map<String, Object> result, boolean trackOrigins) {
// 查找分隔符位置 = 或 :
int separatorIndex = findSeparator(line);
// 键:分隔符前的部分(去掉尾部空白)
String key = line.substring(0, separatorIndex).trim();
// 值:分隔符后的部分(去掉前后空白和引号)
String value = line.substring(separatorIndex + 1).trim();
value = trimQuotes(value);
// 处理续行(\ 结尾 → 下一行继续)
if (value.endsWith("\\")) {
// 暂存,等待下一行继续
// ...
}
// 支持嵌套键(用 . 分隔) → 但最终以扁平化形式存储
if (trackOrigins) {
// 包装为 OriginTrackedValue(携带行号)
result.put(key, OriginTrackedValue.of(
value,
new TextResourceOrigin.Location(
lineNumber, separatorIndex + 1)));
} else {
result.put(key, value);
}
}
private int findSeparator(String line) {
// 查找 = 和 : 的位置,取更早出现的
int equalsIndex = line.indexOf('=');
int colonIndex = line.indexOf(':');
if (equalsIndex == -1 && colonIndex == -1) {
throw new IllegalStateException(
"Invalid property line: " + line);
}
if (equalsIndex == -1) return colonIndex;
if (colonIndex == -1) return equalsIndex;
return Math.min(equalsIndex, colonIndex);
}
private String trimQuotes(String value) {
// 去除值前后的单引号和双引号
if (value.length() >= 2) {
if ((value.startsWith("\"") && value.endsWith("\""))
|| (value.startsWith("'") && value.endsWith("'"))) {
return value.substring(1, value.length() - 1);
}
}
return value;
}
}3.2 解析示例
properties
# application.properties:1
spring.datasource.url=jdbc:mysql://localhost:3306/db # ← 第 2 行
spring.datasource.username=admin # ← 第 3 行
spring.datasource.password=secret123 # ← 第 4 行解析过程:
第 2 行: "spring.datasource.url=jdbc:mysql://localhost:3306/db"
separatorIndex = 23 (= 的位置)
key = "spring.datasource.url"
value = "jdbc:mysql://localhost:3306/db"
origin = Location(line=2, column=24)
第 3 行: "spring.datasource.username=admin"
key = "spring.datasource.username"
value = "admin"
origin = Location(line=3, column=28)3.3 与 YAML 行号追踪的对比
| 加载器 | 追踪方式 | 解析器 | 行号来源 |
|---|---|---|---|
OriginTrackedPropertiesLoader | 逐行解析 =/: | 自研 | BufferedReader.readLine() 计数 |
OriginTrackedYamlLoader | SnakeYaml 节点标记 | SnakeYaml Composer | Node.getStartMark().getLine() |
4. YamlPropertySourceLoader 加载 .yml / .yaml
4.1 源码
java
// 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. 使用 OriginTrackedYamlLoader 解析 YAML
// (含 --- 多文档块拆分 + 行号追踪)
List<Map<String, Object>> loaded =
new OriginTrackedYamlLoader(resource).load();
// 3. 如果解析结果为空
if (loaded.isEmpty()) {
return Collections.emptyList();
}
// 4. 每个文档块 → 独立的 OriginTrackedMapPropertySource
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;
}
}4.2 完整加载流程
YamlPropertySourceLoader.load("application.yml", resource)
│
├─ 1. resource.exists() → true
│
├─ 2. OriginTrackedYamlLoader.load()
│ └─ SnakeYaml Yaml.loadAll(inputStream)
│ ├─ Document 0: {server: {port: 8080}}
│ ├─ Document 1: {spring: {profiles: dev}, server: {port: 8081}}
│ └─ Document 2: {spring: {profiles: prod}, server: {port: 8082}}
│
├─ 3. Profile 匹配 (只保留匹配当前 profile 的文档)
│ └─ document #0 (默认) → 保留
│ └─ document #1 (dev) → 如果 active=dev → 保留
│ └─ document #2 (prod) → 如果 active≠prod → 丢弃
│
└─ 4. 生成 PropertySource 列表:
[OriginTrackedMapPropertySource("application.yml (document #0)"),
OriginTrackedMapPropertySource("application.yml (document #1)")]5. JsonPropertySourceLoader 加载 .json
5.1 源码
java
// JsonPropertySourceLoader.java
public class JsonPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "json" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
// 1. 检查资源是否存在
if (!resource.exists()) {
return Collections.emptyList();
}
// 2. 使用 Jackson 的 ObjectMapper 反序列化 JSON
try (InputStream inputStream = resource.getInputStream()) {
// 将 JSON 反序列化为 Map<String, Object>
@SuppressWarnings("unchecked")
Map<String, Object> source = (Map<String, Object>)
OBJECT_MAPPER.readValue(inputStream, Map.class);
// 3. 如果结果为空
if (source.isEmpty()) {
return Collections.emptyList();
}
// 4. 扁平化嵌套 JSON
// {server: {port: 8080}} → {server.port: 8080}
Map<String, Object> flattened =
flatten(source);
// 5. 包装为 MapPropertySource(注意:不带行号追踪)
return Collections.singletonList(
new MapPropertySource(name, flattened));
}
}
}5.2 JSON 的扁平化
java
// JsonPropertySourceLoader.java
private Map<String, Object> flatten(Map<String, Object> source) {
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();
if (entry.getValue() instanceof Map) {
// 递归处理嵌套对象
@SuppressWarnings("unchecked")
Map<String, Object> nested =
(Map<String, Object>) entry.getValue();
flatten(result, nested, key + ".");
} else if (entry.getValue() instanceof List) {
// 扁平化列表 → [0], [1] ...
flattenList(result, key,
(List<?>) entry.getValue());
} else {
// 基本类型 → 直接存储
result.put(key, entry.getValue());
}
}
}5.3 加载示例
json
// application.json
{
"server": {
"port": 8080
},
"spring": {
"datasource": {
"url": "jdbc:mysql://localhost:3306/db",
"username": "admin"
},
"redis": {
"cluster": {
"nodes": ["192.168.1.1:6379", "192.168.1.2:6379"]
}
}
}
}扁平化结果:
server.port → 8080
spring.datasource.url → jdbc:mysql://localhost:3306/db
spring.datasource.username → admin
spring.redis.cluster.nodes[0] → 192.168.1.1:6379
spring.redis.cluster.nodes[1] → 192.168.1.2:63795.4 与 YAML/Properties 行号追踪的差异
| 格式 | 加载器 | 行号追踪 | 原因 |
|---|---|---|---|
.properties | OriginTrackedPropertiesLoader | ✅ 支持 | 逐行解析,可以记录行号 |
.yml/.yaml | OriginTrackedYamlLoader | ✅ 支持 | SnakeYaml 提供 StartMark |
.json | JsonPropertySourceLoader | ❌ 不支持 | Jackson 不保留 JSON 原始行号 |
6. XmlPropertySourceLoader 加载 .xml
6.1 源码
java
// XmlPropertySourceLoader.java
public class XmlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "xml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
// 1. 检查资源是否存在
if (!resource.exists()) {
return Collections.emptyList();
}
// 2. DOM 解析 XML
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(resource.getInputStream());
// 3. 提取 <property> 元素
NodeList propertyNodes = document.getElementsByTagName("property");
// 4. 遍历并提取键值对
Map<String, Object> properties = new LinkedHashMap<>();
for (int i = 0; i < propertyNodes.getLength(); i++) {
Element element = (Element) propertyNodes.item(i);
String key = element.getAttribute("name");
String value = element.getTextContent().trim();
if (key != null && !key.isEmpty()) {
properties.put(key, value);
}
}
// 5. 包装为 MapPropertySource
return Collections.singletonList(
new MapPropertySource(name, properties));
}
}6.2 XML 格式要求
xml
<!-- application-config.xml -->
<properties>
<property name="server.port">8080</property>
<property name="spring.datasource.url">
jdbc:mysql://localhost:3306/db
</property>
<property name="spring.datasource.username">admin</property>
</properties>6.3 DOM 解析过程
DocumentBuilder.parse(inputStream)
│
└─ document.getDocumentElement()
└─ NodeList: <properties>
└─ childNodes: [<property>, <property>, <property>]
├─ element.getAttribute("name") → "server.port"
├─ element.getTextContent().trim() → "8080"
│
├─ element.getAttribute("name") → "spring.datasource.url"
├─ element.getTextContent().trim() → "jdbc:mysql://..."
│
└─ element.getAttribute("name") → "spring.datasource.username"
└─ element.getTextContent().trim() → "admin"6.4 与 PropertiesPropertySourceLoader 的关系
PropertiesPropertySourceLoader.getFileExtensions() 返回 ["properties", "xml"]——这是因为 .properties 文件的 XML 格式也可以通过 PropertiesPropertySourceLoader 加载:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="server.port">8080</entry>
</properties>| XML 格式 | 加载器 | 元素名 |
|---|---|---|
| Spring Boot XML 格式 (自定义) | XmlPropertySourceLoader | <property name="..."> |
| Java Properties XML 格式 (标准) | PropertiesPropertySourceLoader | <entry key="..."> |
7. ResourcePropertySource 包装任意 PropertySource
7.1 源码
java
// ResourcePropertySource.java
public class ResourcePropertySource extends PropertiesPropertySource {
/**
* 根据资源文件名自动选择加载器。
* 支持 .properties、.yml、.yaml、.json、.xml 等格式。
*/
public static ResourcePropertySource from(String name, Resource resource) {
// 1. 获取资源文件名
String filename = resource.getFilename();
// 2. 根据扩展名选择加载器
PropertySourceLoader loader = getLoaderForResource(filename);
if (loader != null) {
// 3. 使用加载器加载
try {
List<PropertySource<?>> loaded = loader.load(name, resource);
if (!loaded.isEmpty()) {
// 返回第一个加载的 PropertySource
return new ResourcePropertySource(
loaded.get(0), name);
}
} catch (IOException ex) {
throw new IllegalStateException(
"Failed to load resource: " + resource, ex);
}
}
// 4. 默认:按 Properties 加载
return new ResourcePropertySource(name, resource);
}
private static PropertySourceLoader getLoaderForResource(
String filename) {
if (filename == null) {
return null;
}
// 遍历所有注册的 PropertySourceLoader
for (PropertySourceLoader loader : getLoaders()) {
for (String extension : loader.getFileExtensions()) {
if (filename.endsWith("." + extension)) {
return loader;
}
}
}
return null;
}
}7.2 自动选择加载器逻辑
ResourcePropertySource.from("my", resource)
│
├─ resource.filename = "application.properties"
│ └─ loader = PropertiesPropertySourceLoader
│
├─ resource.filename = "application.yml"
│ └─ loader = YamlPropertySourceLoader
│
├─ resource.filename = "application.json"
│ └─ loader = JsonPropertySourceLoader
│
└─ resource.filename = "application.xml"
└─ loader = ? (先 XmlPropertySourceLoader,不匹配再尝试 PropertiesPropertySourceLoader)7.3 EncodedResource —— 编码支持
java
// EncodedResource.java (Spring Framework)
public class EncodedResource {
private final Resource resource;
private final String encoding;
public EncodedResource(Resource resource) {
this(resource, null); // 不指定编码 → 使用默认
}
public EncodedResource(Resource resource, String encoding) {
this.resource = resource;
this.encoding = encoding;
}
public Reader getReader() throws IOException {
if (this.encoding != null) {
// 使用指定编码创建 Reader
return new InputStreamReader(
this.resource.getInputStream(), this.encoding);
} else {
// 使用默认编码
return new InputStreamReader(
this.resource.getInputStream());
}
}
}在 OriginTrackedPropertiesLoader 中的使用:
java
// 尝试从资源中获取编码配置
// 先检查资源是否以 UTF-8 BOM 开头
// 再检查 spring.encoding 配置
EncodedResource encodedResource = new EncodedResource(resource);
Reader reader = encodedResource.getReader();8. 各 Loader 的 getFileExtensions() 返回值
8.1 汇总表
| Loader | getFileExtensions() | 支持的配置示例 | 行号追踪 |
|---|---|---|---|
PropertiesPropertySourceLoader | ["properties", "xml"] | application.properties | ✅ |
YamlPropertySourceLoader | ["yml", "yaml"] | application.yml | ✅ |
JsonPropertySourceLoader | ["json"] | application.json | ❌ |
XmlPropertySourceLoader | ["xml"] | application-config.xml | ❌ |
8.2 各 Loader 的扩展名专属特性
| 扩展名 | Loader | 特性 | 限制 |
|---|---|---|---|
.properties | PropertiesPropertySourceLoader | 逐行解析,完整行号追踪,=/: 分隔 | 无层级结构,不适合复杂配置 |
.xml (Properties XML) | PropertiesPropertySourceLoader | Java 标准 XML Properties 格式 | 需符合 <!DOCTYPE properties> |
.yml / .yaml | YamlPropertySourceLoader | 层级结构,多文档块 ---,profile 匹配 | 解析相对较慢 |
.json | JsonPropertySourceLoader | 与前端 JSON 格式一致 | 无行号追踪 |
.xml (Spring Boot) | XmlPropertySourceLoader | 自定义 <property name="..."> 格式 | 仅限 Spring Boot 项目使用 |
8.3 自定义 Loader 扩展
java
// 自定义 TOML 配置文件加载器
public class TomlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "toml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
// 使用 TOML 解析库 (如 tom4j) 解析
Toml toml = new Toml().read(resource.getInputStream());
Map<String, Object> map = toml.toMap();
// 扁平化嵌套
Map<String, Object> flattened = flatten(map);
return Collections.singletonList(
new MapPropertySource(name, flattened));
}
}注册到 META-INF/spring.factories:
# META-INF/spring.factories
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader,\
org.springframework.boot.env.JsonPropertySourceLoader,\
org.springframework.boot.env.XmlPropertySourceLoader,\
com.example.TomlPropertySourceLoader总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | PropertySourceLoader 接口 | getFileExtensions() + load(name, resource) → List<PropertySource<?>> |
| ② | PropertiesPropertySourceLoader | OriginTrackedPropertiesLoader.load() 逐行解析 =/: 分隔符 |
| ③ | OriginTrackedPropertiesLoader 行号追踪 | BufferedReader.readLine() 计数 + OriginTrackedValue.of() 包装行号 |
| ④ | YamlPropertySourceLoader | OriginTrackedYamlLoader → SnakeYaml loadAll() → 多文档拆分 → profile 匹配 |
| ⑤ | JsonPropertySourceLoader | Jackson ObjectMapper.readValue() → Map<String, Object> → 扁平化,无行号追踪 |
| ⑥ | XmlPropertySourceLoader | DOM DocumentBuilder.parse() → 提取 <property name="..." 元素 |
| ⑦ | ResourcePropertySource | 根据文件扩展名自动选择 Loader,支持 .properties/.yml/.yaml/.json/.xml |
| ⑧ | 各 Loader 扩展名 | PropertiesPropertySourceLoader(properties,xml)、YamlPropertySourceLoader(yml,yaml)、JsonPropertySourceLoader(json)、XmlPropertySourceLoader(xml) |