PropertySource 链构建
概述
Spring Boot 的外部化配置通过 PropertySource 链实现——多个 PropertySource 按优先级构成一个有序列表,调用 Environment.getProperty() 时从最高优先级开始查找,找到即返回。
本文深入拆解 PropertySource 链的构建、解析、追踪和测试覆盖的全部 10 个细节点。
本文基于 Spring Boot 3.x 源码分析。
1. 整体架构
Environment.getProperty("server.port")
│
└─ PropertySourcesPropertyResolver
│
└─ 遍历 PropertySource 链(按优先级降序)
│
├─ 1. 命令行参数 CommandLinePropertySource ← 最高优先级
├─ 2. JNDI 属性 JndiPropertySource
├─ 3. 系统属性 PropertiesPropertySource (-Dkey=value)
├─ 4. OS 环境变量 SystemEnvironmentPropertySource (env)
├─ 5. Random 属性 RandomValuePropertySource
├─ 6. application.yml OriginTrackedMapPropertySource
├─ 7. application-dev.yml OriginTrackedMapPropertySource
├─ 8. @PropertySource EnumerableCompositePropertySource
├─ 9. spring.factories 默认 DefaultPropertiesPropertySource
└─ 10. ... ← 最低优先级2. StandardServletEnvironment.customizePropertySources()
2.1 源码
java
// StandardServletEnvironment.java
public class StandardServletEnvironment extends StandardEnvironment {
public static final String SERVLET_CONFIG_INIT_PARAM_PROPERTY_SOURCE_NAME =
"servletConfigInitParams";
public static final String SERVLET_CONTEXT_INIT_PARAM_PROPERTY_SOURCE_NAME =
"servletContextInitParams";
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
// 1. ServletConfig 初始化参数(最高)
propertySources.addLast(
new ServletConfigPropertySource(SERVLET_CONFIG_INIT_PARAM_PROPERTY_SOURCE_NAME,
getServletConfig()));
// 2. ServletContext 初始化参数
propertySources.addLast(
new ServletContextPropertySource(SERVLET_CONTEXT_INIT_PARAM_PROPERTY_SOURCE_NAME,
getServletContext()));
// 3. JNDI 属性
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(
new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
// 4. 系统属性(由父类 StandardEnvironment 添加)
super.customizePropertySources(propertySources);
}
}
// StandardEnvironment.java
public class StandardEnvironment extends AbstractEnvironment {
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME =
"systemProperties";
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME =
"systemEnvironment";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
// 4. 系统属性 (-Dkey=value)
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
getSystemProperties()));
// 5. 系统环境变量 (OS env)
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
getSystemEnvironment()));
}
}2.2 七级排序
| 优先级 | PropertySource 名称 | 类型 | 来源 |
|---|---|---|---|
| 最高 | servletConfigInitParams | ServletConfigPropertySource | web.xml/Servlet 3.0 getInitParameter() |
| ▲ | servletContextInitParams | ServletContextPropertySource | web.xml/ServletContext.setInitParameter() |
| ▲ | jndiProperties | JndiPropertySource | JNDI java:comp/env |
| ▲ | systemProperties | PropertiesPropertySource | Java 系统属性 System.getProperties() |
| ▲ | systemEnvironment | SystemEnvironmentPropertySource | OS 环境变量 System.getenv() |
| ▲ | Random | RandomValuePropertySource | random.int/random.long 随机值 |
| 最低 | application.yml/配置源 | OriginTrackedMapPropertySource | Spring Boot 配置文件 |
注意:
customizePropertySources()按addLast()顺序添加,所以越早添加的优先级越高(因为查找时从列表头部开始遍历)。
3. 17 个 PropertySource 的完整排序
3.1 Spring Boot 3.x 默认的 17 个配置源
启动时完整的 PropertySource 链(按优先级降序):
| # | PropertySource 名称 | 类型 | 说明 |
|---|---|---|---|
| 1 | servletConfigInitParams | ServletConfigPropertySource | Servlet 配置参数 (最高) |
| 2 | servletContextInitParams | ServletContextPropertySource | Servlet 上下文参数 |
| 3 | jndiProperties | JndiPropertySource | JNDI 属性 |
| 4 | systemProperties | PropertiesPropertySource | -D 系统属性 |
| 5 | systemEnvironment | SystemEnvironmentPropertySource | OS 环境变量 |
| 6 | random | RandomValuePropertySource | 随机值 |
| 7 | application-config: [classpath:/application.yml] (document #0) | OriginTrackedMapPropertySource | 默认 YAML 文档 |
| 8 | application-config: [classpath:/application.yml] (document #1) | OriginTrackedMapPropertySource | Profile 特定 YAML 文档 |
| 9 | application-config: [classpath:/application-dev.yml] | OriginTrackedMapPropertySource | application-dev.yml |
| 10 | application-config: [classpath:/application-prod.yml] | OriginTrackedMapPropertySource | application-prod.yml |
| 11 | class path resource [custom.yml] | OriginTrackedMapPropertySource | spring.config.import 导入 |
| 12 | @PropertySource('classpath:config.properties') | EnumerableCompositePropertySource | @PropertySource 导入 |
| 13 | defaultProperties | DefaultPropertiesPropertySource | SpringApplication.setDefaultProperties() |
| 14 | Inlined Test Properties | MapPropertySource | @SpringBootTest(properties=...) |
| 15 | [MockPropertySource] | MockPropertySource | 测试中通过 @TestPropertySource 添加 |
| 16 | @TestPropertySource 导入 | OriginTrackedMapPropertySource | @TestPropertySource(locations=...) |
| 17 | @DynamicPropertySource | MapPropertySource | @DynamicPropertySource 动态注册 |
3.2 如何查看实际链
java
// 在配置类中注入 Environment 并打印
@Component
public class PropertySourcePrinter
implements ApplicationRunner {
@Autowired
private Environment env;
@Override
public void run(ApplicationArguments args) {
if (env instanceof ConfigurableEnvironment) {
MutablePropertySources sources =
((ConfigurableEnvironment) env).getPropertySources();
int i = 1;
for (PropertySource<?> ps : sources) {
System.out.println((i++) + ". " + ps.getName());
}
}
}
}4. DefaultPropertiesPropertySource 加载
4.1 源码
java
// DefaultPropertiesPropertySource.java
public class DefaultPropertiesPropertySource
extends PropertiesPropertySource {
public static final String NAME = "defaultProperties";
public DefaultPropertiesPropertySource(Map<String, Object> source) {
super(NAME, source);
}
// 创建默认属性源
static DefaultPropertiesPropertySource create(SpringApplication application) {
// 从 SpringApplication 的 defaultProperties 属性创建
Map<String, Object> defaultProperties =
application.getDefaultProperties();
if (defaultProperties != null && !defaultProperties.isEmpty()) {
return new DefaultPropertiesPropertySource(defaultProperties);
}
return null;
}
}4.2 设置默认属性
java
// 方式 1:SpringApplication.setDefaultProperties()
SpringApplication app = new SpringApplication(MyApp.class);
Properties props = new Properties();
props.setProperty("server.port", "9090");
app.setDefaultProperties(props);
// 方式 2:SpringApplicationBuilder
new SpringApplicationBuilder(MyApp.class)
.properties("server.port=9090")
.run(args);
// 方式 3:application.yml 中 spring.config.import
spring:
config:
import: "classpath:extra-config.yml"4.3 DefaultPropertiesPropertySource 在链中的位置
优先级:高 → 低
...
systemEnvironment
random
application.yml
...
DefaultPropertiesPropertySource ← 倒数第二位
...特点:DefaultPropertiesPropertySource 的优先级低于大部分配置文件,仅高于测试中的 @TestPropertySource。这意味着用户可以通过配置文件覆盖默认值。
5. CommandLinePropertySource 解析
5.1 两种命令行格式
java
// SimpleCommandLinePropertySource.java
public class SimpleCommandLinePropertySource
extends CommandLinePropertySource<CommandLineArgs> {
public SimpleCommandLinePropertySource(String... args) {
// 解析命令行参数
super(new SimpleCommandLineArgsParser().parse(args));
}
}bash
# 格式 1:--key=value (Spring Boot 标准)
java -jar myapp.jar --server.port=8080 --spring.profiles.active=dev
# 格式 2:--key value (传统方式)
java -jar myapp.jar --server.port 8080 --spring.profiles.active dev5.2 SimpleCommandLineArgsParser 解析
java
// SimpleCommandLineArgsParser.java
class SimpleCommandLineArgsParser {
CommandLineArgs parse(String... args) {
CommandLineArgs commandLineArgs = new CommandLineArgs();
for (String arg : args) {
if (arg.startsWith("--")) {
// 去掉 "--" 前缀
String optionText = arg.substring(2);
String optionName;
String optionValue;
// 尝试按 = 分隔
int indexOfEquals = optionText.indexOf('=');
if (indexOfEquals > -1) {
// --key=value
optionName = optionText.substring(0, indexOfEquals);
optionValue = optionText.substring(indexOfEquals + 1);
} else {
// --key value(值在下次循环中)
optionName = optionText;
optionValue = null; // 等待后续参数
}
// 如果已经是某选项的值
if (commandLineArgs.containsOption(optionName)) {
commandLineArgs.addOption(optionName, optionValue);
} else {
// 非选项参数(无 -- 前缀)
commandLineArgs.addNonOptionArg(arg);
}
}
}
return commandLineArgs;
}
}5.3 解析结果
bash
java -jar myapp.jar --server.port=8080 --spring.profiles.active=dev,prod解析后:
CommandLineArgs {
optionArgs = {
"server.port" → ["8080"],
"spring.profiles.active" → ["dev,prod"]
}
nonOptionArgs = [] // 无前缀的参数
}5.4 为什么命令行参数优先级最高
java
// AbstractApplicationContext 初始化的 PropertySources
// 命令行参数被添加到链的最前面
propertySources.addFirst(
new SimpleCommandLinePropertySource(args));addFirst() 使其成为链中第一个被查找的 PropertySource,确保命令行参数可以覆盖任何配置文件中的设置。
6. @TestPropertySource 在测试中的覆盖
6.1 测试中的 PropertySource 链
java
@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
@ActiveProfiles("test")
class MyServiceTest {
// ...
}6.2 测试环境加载源码
java
// MergedContextConfiguration.java
public class MergedContextConfiguration {
// 获取测试专属的 PropertySource 属性
private String[] getPropertySourceProperties() {
// 从 @TestPropertySource 注解中提取 properties 属性
// 以及 locations 属性指向的文件
List<String> properties = new ArrayList<>();
// 1. 获取 @TestPropertySource(locations=...) 中的文件
for (TestPropertySource tps : retrieveTestPropertySources()) {
// 加载 locations 中的文件
for (String location : tps.locations()) {
properties.addAll(loadPropertiesFromFile(location));
}
// 2. 获取 @TestPropertySource(properties=...) 中的内联属性
for (String inline : tps.properties()) {
properties.add(inline);
}
}
return properties.toArray(new String[0]);
}
}6.3 测试属性的注册
java
// SpringBootTestContextBootstrapper.java
class SpringBootTestContextBootstrapper
extends AbstractTestContextBootstrapper {
@Override
protected TestContextBootstrapper buildDefaultMergedContextConfiguration() {
// 创建 MergedContextConfiguration 时
MergedContextConfiguration config = createMergedContextConfiguration();
// 将 @TestPropertySource 的属性添加到 Environment 中
addTestPropertySources(config);
// 添加到 PropertySource 链末尾的后面——优先级低于所有 Spring Boot 配置源
// 但高于应用代码中的属性
addPropertiesToEnvironment(config.getPropertySourceProperties());
}
}6.4 测试系统的优先级
| 测试属性源 | 加载机制 | 优先级 |
|---|---|---|
@TestPropertySource(properties=...) | MapPropertySource 追加到链尾 | 最高(测试内联属性) |
@TestPropertySource(locations=...) | OriginTrackedMapPropertySource | 高 |
@ActiveProfiles 激活的 application-test.yml | OriginTrackedMapPropertySource | 中 |
application.yml 默认值 | OriginTrackedMapPropertySource | 低 |
SpringApplication.setDefaultProperties() | DefaultPropertiesPropertySource | 最低 |
7. PropertySource.getProperty() 委托链
7.1 委托链源码
java
// AbstractEnvironment.java
public class AbstractEnvironment implements ConfigurableEnvironment {
@Override
public String getProperty(String key) {
// 委托给 PropertySourcesPropertyResolver
return this.propertyResolver.getProperty(key);
}
}
// PropertySourcesPropertyResolver.java
public class PropertySourcesPropertyResolver
implements PropertyResolver {
// 核心方法:遍历所有 PropertySource
@Override
public String getProperty(String key) {
return getProperty(key, String.class, true);
}
protected <T> T getProperty(String key, Class<T> targetType,
boolean resolveNestedPlaceholders) {
// 1. 遍历 PropertySource 链(从索引 0 开始——优先级最高)
for (PropertySource<?> propertySource : this.propertySources) {
// 2. 尝试从当前 PropertySource 获取值
Object value = propertySource.getProperty(key);
if (value != null) {
// 3. 找到值 → 解析嵌套占位符(如需)
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
// 4. 类型转换
if (targetType != null && targetType != String.class) {
value = this.conversionService.convert(
value, targetType);
}
return (T) value;
}
// 5. 当前 PropertySource 中没找到 → 继续下一个
}
return null; // 所有 PropertySource 都找不到 → 返回 null
}
}7.2 委托链图示
Environment.getProperty("server.port")
│
└─ PropertySourcesPropertyResolver.getProperty("server.port")
│
├─ PropertySource[0]: SimpleCommandLinePropertySource
│ └─ getProperty("server.port") → null (没传命令行参数)
│
├─ PropertySource[1]: ServletConfigPropertySource
│ └─ getProperty("server.port") → null
│
├─ PropertySource[2]: ServletContextPropertySource
│ └─ getProperty("server.port") → null
│
├─ PropertySource[3]: JndiPropertySource
│ └─ getProperty("server.port") → null
│
├─ PropertySource[4]: PropertiesPropertySource(systemProperties)
│ └─ getProperty("server.port") → null
│
├─ PropertySource[5]: SystemEnvironmentPropertySource
│ └─ getProperty("server.port") → null
│
├─ PropertySource[6]: RandomValuePropertySource
│ └─ getProperty("server.port") → null
│
├─ PropertySource[7]: OriginTrackedMapPropertySource(application.yml)
│ └─ getProperty("server.port") → "8080" ← ★ 找到!
│
└─ return "8080"7.3 类型转换
java
// 使用 ConversionService 进行类型转换
@Value("${server.port}")
private int port; // String "8080" → int 8080
// PropertySourcesPropertyResolver 中的转换
@Override
public <T> T getProperty(String key, Class<T> targetType) {
// 找到 String 值后,通过 ConversionService.convert() 转换
return this.conversionService.convert(value, targetType);
}7.4 占位符解析
java
@Override
protected String getPropertyAsRawString(String key) {
// 查找但不解析嵌套占位符
for (PropertySource<?> propertySource : this.propertySources) {
Object value = propertySource.getProperty(key);
if (value != null) {
return String.valueOf(value);
}
}
return null;
}
// resolveNestedPlaceholders() 递归解析 ${...}
// 例如: ${db.${db.type}.url} → 先解析 db.type → "mysql" → 再解析 db.mysql.url8. OriginTrackedMapPropertySource 追踪
8.1 与普通 MapPropertySource 的对比
java
// 普通 MapPropertySource
public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> {
@Override
public Object getProperty(String name) {
return this.source.get(name); // 直接返回值
}
}
// OriginTrackedMapPropertySource
public class OriginTrackedMapPropertySource
extends MapPropertySource {
private final Map<String, Origin> origins; // 额外记录每个属性的来源
public OriginTrackedMapPropertySource(String name, Map<String, Object> source) {
super(name, source);
// 从 source 中提取 Origin 信息
this.origins = extractOrigins(source);
}
private Map<String, Origin> extractOrigins(Map<String, Object> source) {
Map<String, Origin> origins = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : source.entrySet()) {
if (entry.getValue() instanceof OriginTrackedValue) {
// 值中包含行号信息 → 记录
origins.put(entry.getKey(),
((OriginTrackedValue) entry.getValue()).getOrigin());
}
}
return origins;
}
// 提供 origin 查询方法
public Origin getOrigin(String name) {
return this.origins.get(name);
}
}8.2 行号追踪的完整路径
SnakeYaml 解析 YAML
→ OriginTrackedYamlLoader.OriginTrackingConstructor
→ 记录每个值的 Node.getStartMark() 行号
→ OriginTrackedValue.of(value, line, column)
→ 存入 OriginTrackedMapPropertySource
→ origins Map 记录 [key → Origin(line, column)]
→ Actuator /env 端点展示
GET /actuator/env/server.port
{
"property": {
"value": "8080",
"origin": "application.yml:2:7"
}
}9. @PropertySource 导入的文件
9.1 注册时机
@PropertySource 注解的处理不在启动阶段完成,而是在 refresh() 阶段:
java
// ConfigurationClassParser.java
private void processPropertySource(AnnotationAttributes propertySource) {
// 解析 @PropertySource 注解
String name = propertySource.getString("name");
String[] locations = propertySource.getStringArray("value");
boolean ignoreResourceNotFound =
propertySource.getBoolean("ignoreResourceNotFound");
// 加载资源文件
for (String location : locations) {
// 解析占位符(支持 ${...} 表达式中的路径)
String resolvedLocation = this.environment
.resolveRequiredPlaceholders(location);
// 加载 PropertySource
Resource resource = this.resourceLoader
.getResource(resolvedLocation);
// 注册到 Environment
addPropertySource(name, resource);
}
}9.2 加载的文件追加到链中
java
private void addPropertySource(String name, Resource resource) {
// 创建 PropertySource
PropertySource<?> propertySource;
if (resource.getFilename().endsWith(".properties")) {
propertySource = new ResourcePropertySource(name, resource);
} else if (resource.getFilename().endsWith(".yml")
|| resource.getFilename().endsWith(".yaml")) {
// YAML 文件使用 YamlPropertySourceLoader
propertySource = new YamlPropertySourceLoader()
.load(name, resource).get(0);
} else {
// XML 等其他格式
propertySource = new XmlPropertySource(name, resource);
}
// 将新 PropertySource 放到链的最前面(最高优先级)
// 这样 @PropertySource 导入的文件可以覆盖 application.yml 中的配置
this.environment.getPropertySources()
.addFirst(propertySource);
}9.3 在自动配置中的应用
java
@Configuration
@PropertySource("classpath:custom-datasource.properties")
// ↑ 这个文件会在 refresh() 阶段被加载,优先级高于 application.yml
public class DataSourceConfig {
@Value("${datasource.url}")
private String url;
}@PropertySource 加载的文件通过 addFirst() 添加到链中,因此可以覆盖 application.yml 中的同名配置。
10. PropertySourcesPropertyResolver 的缓存
10.1 缓存实现
java
// PropertySourcesPropertyResolver.java
public class PropertySourcesPropertyResolver implements PropertyResolver {
// 缓存已解析的属性值
private final Cache cache = new Cache();
@Override
public String getProperty(String key) {
// 1. 先查缓存
String cached = this.cache.get(key);
if (cached != null) {
return cached;
}
// 2. 遍历 PropertySource 链
for (PropertySource<?> propertySource : this.propertySources) {
Object value = propertySource.getProperty(key);
if (value != null) {
String stringValue = String.valueOf(value);
// 3. 写入缓存
this.cache.put(key, stringValue);
return stringValue;
}
}
// 4. 未找到 → 写入 null 到缓存(避免重复遍历)
this.cache.put(key, null);
return null;
}
// 内部缓存类
private static class Cache {
private final ConcurrentHashMap<String, String> cache =
new ConcurrentHashMap<>();
String get(String key) {
return this.cache.get(key);
}
void put(String key, String value) {
this.cache.put(key, value);
}
}
}10.2 缓存的 Key 和 Value
| Key | Value | 说明 |
|---|---|---|
"server.port" | "8080" | 找到的属性值 |
"server.port" | null | 所有 PropertySource 中都不存在 |
"spring.datasource.url" | "jdbc:mysql://..." | 找到的属性值 |
10.3 缓存的过期
java
// 修改 PropertySources 时清空缓存
@Override
public void replacePropertySources(MutablePropertySources propertySources) {
this.propertySources = propertySources;
this.cache.clear(); // ← 清空缓存
}
// 添加新 PropertySource 时清空缓存
@Override
public void addPropertySource(String name, PropertySource<?> propertySource) {
// ...
this.cache.clear();
}10.4 缓存对性能的影响
java
// Spring Boot 启动过程中,同一个属性可能被多次查询
// 例如:每次 @Value("${server.port}") 都会触发 getProperty()
// 缓存避免了每次都要遍历 17+ 个 PropertySource
@Value("${server.port}")
private int port1;
@Value("${server.port}")
private int port2; // 第二次查询直接从缓存获取11. MockPropertySource 在测试中的优先级
11.1 使用示例
java
@SpringBootTest
@TestPropertySource(
locations = "classpath:test.properties",
properties = {
"server.port=9090",
"spring.datasource.url=jdbc:h2:mem:testdb"
})
@ActiveProfiles("test")
class MyServiceTest {
@Autowired
private Environment env;
}11.2 MockPropertySource 的加载顺序
java
// SpringBootTestContextBootstrapper.java
private PropertySource<?> createTestPropertySource(
MergedContextConfiguration config) {
// 1. 获取 @TestPropertySource(properties=...) 中的内联属性
String[] inlinedProperties = config.getPropertySourceProperties();
// 2. 创建 MockPropertySource(名称固定)
MockPropertySource mockPropertySource =
new MockPropertySource("testProperties");
// 3. 解析内联属性(key=value 格式)
for (String property : inlinedProperties) {
int separator = property.indexOf('=');
if (separator > 0) {
String key = property.substring(0, separator).trim();
String value = property.substring(separator + 1).trim();
mockPropertySource.withProperty(key, value);
}
}
// 4. 添加到 PropertySource 链的最前面
// 使用 addFirst 确保测试属性比 application.yml 优先级更高
environment.getPropertySources().addFirst(mockPropertySource);
return mockPropertySource;
}11.3 优先级
测试启动时的 PropertySource 链(测试类中):
优先级:高
│
├─ @TestPropertySource(properties=...) ← 内联属性(最高)
│ "server.port=9090"
│
├─ @TestPropertySource(locations=...) ← 外部属性文件
│ "classpath:test.properties"
│
├─ @ActiveProfiles → application-test.yml ← Profile 特定配置
│
├─ 命令行参数 ← (如果传了)
│
├─ application.yml ← 默认配置
│
└─ DefaultProperties ← 默认值(最低)11.4 @DynamicPropertySource 的进一步覆盖
java
@SpringBootTest
@Testcontainers
class MyServiceTest {
// 动态注册属性——优先级高于 @TestPropertySource
@DynamicPropertySource
static void configureProperties(
DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url",
() -> "jdbc:tc:mysql:8://localhost/testdb");
registry.add("spring.datasource.username",
() -> "test");
}
}@DynamicPropertySource 通过 addFirst() 添加到链的最前面,因此可以覆盖 @TestPropertySource 中定义的属性。
总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | StandardServletEnvironment 七级排序 | servletConfigInitParams > servletContextInitParams > jndiProperties > systemProperties > systemEnvironment > random > application.yml |
| ② | 17 个 PropertySource 完整排序 | 从 servletConfigInitParams 到 @DynamicPropertySource 共 17 个 |
| ③ | DefaultPropertiesPropertySource | SpringApplication.setDefaultProperties() 设置的默认值,优先级倒数第二 |
| ④ | CommandLinePropertySource 解析 | --key=value 格式,addFirst() 成为链头 |
| ⑤ | @TestPropertySource 测试覆盖 | 内联属性 > 外部文件 > application-test.yml > application.yml |
| ⑥ | 委托链 | Environment.getProperty() → 遍历所有 PropertySource → 找到即返回 |
| ⑦ | OriginTrackedMapPropertySource | 额外维护 Map<String, Origin> 记录每个属性的行号 |
| ⑧ | @PropertySource 导入 | refresh() 阶段通过 addFirst() 追加到链最前面 |
| ⑨ | 缓存 | ConcurrentHashMap 缓存已解析的属性值,避免重复遍历 |
| ⑩ | MockPropertySource 测试优先级 | @TestPropertySource > @ActiveProfiles > application.yml |