Spring Boot外部化配置:18种PropertySource的加载顺序
Spring Boot外部化配置:18种PropertySource的加载顺序
适读人群:Spring Boot开发者,关注配置管理和多环境部署的后端工程师 | 阅读时长:约17分钟
开篇故事
有一次部署线上环境,我在application-prod.yml里配置了数据库连接,本以为万无一失。结果到了线上一看,用的还是测试环境的数据库地址。
当时急得我直接登上服务器,grep了一遍,发现根本原因是:运维在启动脚本里加了-Dspring.datasource.url=...系统属性,而系统属性的优先级比application-prod.yml高,把我的配置覆盖了。
这件事让我意识到:不搞清楚Spring Boot各种配置源的优先级,你根本不知道最终生效的是哪个配置。尤其是项目大了之后,配置来源可能有十几个,一旦出问题,排查起来非常痛苦。
今天我们就来把Spring Boot官方文档里那18种PropertySource的加载顺序彻底搞清楚,还要看看背后的源码实现。
一、为什么叫"外部化配置"
Spring Boot的配置来源非常多样:命令行参数、系统环境变量、JVM系统属性、配置文件、注解……Spring把这些统称为"外部化配置"(Externalized Configuration),目的是让同一套代码在不同环境运行时,只需要改配置,不需要改代码。
核心抽象是PropertySource<T>,它代表一个配置源,提供getProperty(String name)方法。所有PropertySource组成一个有序列表(MutablePropertySources),优先级高的放前面,查找时从前往后找第一个能返回非null值的。
二、18种PropertySource的加载顺序
2.1 完整优先级列表(数字越小优先级越高)
2.2 配置文件的4个层级(最复杂的部分)
配置文件(application.yml等)的优先级内部还有4层:
高 → 低:
1. jar包外面的 application-{profile}.yml(当前目录 > config子目录)
2. jar包外面的 application.yml
3. jar包里面的 application-{profile}.yml(classpath根 > classpath:/config/)
4. jar包里面的 application.yml具体搜索路径顺序:
1. file:./config/*/ (当前目录的config子目录的子目录)
2. file:./config/ (当前目录的config子目录)
3. file:./ (当前目录)
4. classpath:/config/ (classpath的config目录)
5. classpath:/ (classpath根)2.3 源码实现:ConfigDataEnvironmentPostProcessor
Spring Boot 2.4之后,配置文件加载由ConfigDataEnvironmentPostProcessor负责:
// ConfigDataEnvironmentPostProcessor.java(Spring Boot 3.x)
public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProcessor,
Ordered {
public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
new ConfigDataEnvironment(
this.logFactory, this.bootstrapContext, environment,
application.getResourceLoader(), application.getAdditionalProfiles(),
this.environmentUpdateListener)
.processAndApply();
}
}ConfigDataEnvironment.processAndApply()会触发一系列ConfigDataLoader(如StandardConfigDataLoader)来加载各种配置文件。
2.4 PropertySourcesLoader加载顺序
// ConfigDataLocationResolvers(Spring Boot 3.x)处理配置文件位置
// 默认搜索的位置在 ConfigDataEnvironment 中定义:
private static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static {
List<ConfigDataLocation> locations = new ArrayList<>();
// optional: 表示找不到不报错
locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));
locations.add(ConfigDataLocation.of(
"optional:file:./;optional:file:./config/;optional:file:./config/*/"));
DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
}三、完整代码示例
3.1 用代码验证PropertySource优先级
@SpringBootApplication
public class PropertySourceDebug {
public static void main(String[] args) {
// 演示:通过命令行参数覆盖配置
// java -jar app.jar --app.name=CommandLine -Dapp.name=JvmProp
SpringApplication app = new SpringApplication(PropertySourceDebug.class);
app.setDefaultProperties(Map.of("app.name", "DefaultProps"));
ConfigurableApplicationContext ctx = app.run(args);
ConfigurableEnvironment env = ctx.getEnvironment();
// 打印所有PropertySource及其优先级
System.out.println("=== PropertySource列表(优先级从高到低)===");
for (PropertySource<?> ps : env.getPropertySources()) {
System.out.println(" [" + ps.getClass().getSimpleName() + "] " + ps.getName());
}
// 查看最终生效的值
System.out.println("\n最终 app.name = " + env.getProperty("app.name"));
}
}3.2 自定义PropertySource从远程配置中心加载
// 自定义PropertySource:从Apollo配置中心拉取配置
public class ApolloPropertySource extends PropertySource<Map<String, Object>> {
private static final Logger log = LoggerFactory.getLogger(ApolloPropertySource.class);
public ApolloPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
@Override
public Object getProperty(String name) {
return getSource().get(name);
}
// 工厂方法:从Apollo拉取配置
public static ApolloPropertySource load(String serverUrl, String appId,
String namespace) {
Map<String, Object> properties = new HashMap<>();
try {
// 实际通过HTTP调用Apollo Open API
RestTemplate restTemplate = new RestTemplate();
String url = serverUrl + "/openapi/v1/envs/PRO/apps/" + appId
+ "/namespaces/" + namespace + "/releases/latest";
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
if (response.getBody() != null) {
List<Map<String, String>> items = (List<Map<String, String>>)
response.getBody().get("items");
if (items != null) {
items.forEach(item ->
properties.put(item.get("key"), item.get("value")));
}
}
log.info("Loaded {} properties from Apollo namespace: {}",
properties.size(), namespace);
} catch (Exception e) {
log.warn("Failed to load Apollo config, using defaults", e);
}
return new ApolloPropertySource("apollo-" + namespace, properties);
}
}
// 通过EnvironmentPostProcessor注入
@Order(Ordered.HIGHEST_PRECEDENCE + 15) // 比application.yml优先级高
public class ApolloEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
String apolloServerUrl = environment.getProperty("apollo.server-url");
String appId = environment.getProperty("spring.application.name", "unknown");
String namespace = environment.getProperty("apollo.namespace", "application");
if (apolloServerUrl == null) {
return; // 没配置Apollo,跳过
}
ApolloPropertySource apolloPS = ApolloPropertySource.load(
apolloServerUrl, appId, namespace);
// 加到application.yml之前(优先级更高)
environment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
apolloPS
);
}
}3.3 多环境配置最佳实践
# src/main/resources/application.yml(基础配置,所有环境通用)
spring:
application:
name: my-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
server:
port: 8080
# 占位符,具体值由各profile覆盖
app:
feature:
enable-cache: true
---
# src/main/resources/application-dev.yml(开发环境)
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:mysql://localhost:3306/mydb_dev
username: dev_user
password: dev_pass
logging:
level:
com.example: DEBUG
---
# src/main/resources/application-prod.yml(生产环境)
spring:
config:
activate:
on-profile: prod
datasource:
url: jdbc:mysql://${DB_HOST:prod-db.internal}:3306/mydb
username: ${DB_USER}
password: ${DB_PASSWORD} # 从环境变量读取,不写死
logging:
level:
com.example: WARN生产启动命令:
java -jar app.jar \
--spring.profiles.active=prod \
--DB_HOST=10.0.1.100 \
--DB_USER=prod_user \
--DB_PASSWORD=secret123
# 或者通过环境变量:export DB_PASSWORD=secret123四、踩坑实录
坑1:@PropertySource无法加载yml文件
现象:@PropertySource("classpath:custom.yml"),但配置没有生效。
根因:@PropertySource默认只支持.properties格式,不支持.yml。要加载YAML,需要自定义PropertySourceFactory:
// 自定义支持YAML的PropertySourceFactory
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name,
EncodedResource resource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
Properties properties = factory.getObject();
assert properties != null;
return new PropertiesPropertySource(
name != null ? name : resource.getResource().getFilename(),
properties);
}
}
// 使用
@Configuration
@PropertySource(value = "classpath:custom.yml",
factory = YamlPropertySourceFactory.class)
public class CustomConfig {
@Value("${custom.property}")
private String customProperty;
}坑2:@PropertySource的优先级低于application.yml
很多人以为@PropertySource的优先级高,但实际上它比application.yml还低。如果你想用@PropertySource覆盖application.yml,是做不到的。
解决方案:用EnvironmentPostProcessor在application.yml加载之前把你的配置插入。
坑3:系统环境变量的名称转换规则
Spring Boot会把环境变量名做松散绑定(Relaxed Binding):
环境变量:SPRING_DATASOURCE_URL
等价于:spring.datasource.url
等价于:spring.datasource-url
等价于:SPRING.DATASOURCE.URL但有一个常见坑:.在很多操作系统的环境变量名中是不合法的。所以正确写法是用_代替.和-。
# 错误(某些系统不支持):
export spring.datasource.url=jdbc:mysql://...
# 正确:
export SPRING_DATASOURCE_URL=jdbc:mysql://...坑4:profile激活顺序导致配置被意外覆盖
现象:同时激活了dev和feature-x两个profile,feature-x的配置意外覆盖了dev的配置。
根因:Spring Boot 2.4之后,profile-specific配置文件按照spring.profiles.active的声明顺序,后声明的优先级更高:
# spring.profiles.active=dev,feature-x
# feature-x 比 dev 后声明,优先级更高
# 如果 application-feature-x.yml 有同名属性,会覆盖 application-dev.yml解决方案:理解这个规则后,按预期顺序声明profile;或者用spring.profiles.group对profile分组。
五、总结与延伸
配置优先级的实用记忆口诀:命令行 > 环境变量 > JVM参数 > application.yml > 默认值。
生产实践建议:
- 敏感配置(密码、Key)通过环境变量注入,不写进文件
- 环境差异配置通过
application-{profile}.yml管理 - 用
spring.config.location可以覆盖整个配置文件搜索路径 - 出现配置问题,加
--debug或-Dspring.boot.admin.client.enabled=false查看ConditionEvaluationReport和PropertySource列表
下一篇聊Spring DevTools热加载,看看它为什么要用两个ClassLoader来实现类的热替换。
