在 Spring 体系中,所有配置的读取最终都要经过 Environment 对象,而 Environment 内部维护的就是一组 PropertySource。理解 PropertySource 的工作机制,是解决配置不生效、配置被覆盖、多环境切换失败等一系列问题的关键。Spring Boot 在标准 Spring 的基础上对 PropertySource 做了大量扩展,加入了 application.properties、profile 专属配置、命令行参数、随机值等十几个内置来源,它们的优先级关系常常让开发者摸不着头脑。本文将从原理、整合方式、常见问题三个层面完整梳理这一主题。

一、PropertySource 的底层原理与环境抽象
PropertySource 可以理解为配置的来源抽象,它本质上是对某个键值对集合的包装。Spring 提供了多种实现,比如 MapPropertySource 基于一个 Map,ResourcePropertySource 基于 properties 文件,SystemEnvironmentPropertySource 包装系统环境变量,ServletConfigPropertySource 包装 Servlet 初始化参数。每一个 PropertySource 都有一个 name 属性,环境按顺序遍历这些 PropertySource,第一个包含指定 key 的来源返回的值就是最终生效的值,这就是所谓的一等优先级原则。
Environment 的实现类 StandardEnvironment 内部持有一个 MutablePropertySources 对象,它内部是一个 CopyOnWriteArrayList,保存了所有已注册的 PropertySource。所谓配置优先级,本质上就是这个列表的排列顺序。通过 environment.getPropertySources() 可以直接操作这个列表,比如 addFirst、addLast、addBefore、addAfter 等方法可以精确控制某个来源的位置,这也是解决配置冲突的重要手段。
在传统 Spring 项目中,默认只有系统属性和系统环境变量两个来源,需要通过 XML 中的 context:property-placeholder 或 @PropertySource 注解手动注册新的来源。而 Spring Boot 通过 SpringApplication 启动流程和 ConfigFileApplicationListener(新版本中为 ConfigDataEnvironmentPostProcessor)自动注册了一整套来源,形成了完整的优先级链。理解这一差异,是从 Spring 迁移到 Spring Boot 时最容易踩坑的地方。
二、Spring Boot 中的配置来源与优先级顺序
Spring Boot 官方文档给出了完整的配置优先级列表,从高到低依次是:Devtools 全局配置、测试类上的 @TestPropertySource 注解、测试代码中的 properties 属性、命令行参数、SPRING_APPLICATION_JSON 中的属性、ServletConfig 初始化参数、ServletContext 初始化参数、JNDI 属性、Java 系统属性、操作系统环境变量、RandomValuePropertySource、jar 包外的 application-{profile}.properties、jar 包内的 application-{profile}.properties、jar 包外的 application.properties、jar 包内的 application.properties、@PropertySource 注解指定的来源、默认属性。数字越小优先级越高,命令行参数之所以能覆盖一切文件配置,正是因为它在 PropertySource 列表中被放在了最前面。
需要注意的是,@PropertySource 指定的来源优先级相当靠后,几乎会被所有 application 系列文件覆盖。很多开发者以为在类上加了 @PropertySource 之后,里面的配置就是最高优先级,结果发现本地文件里的值怎么都读不到,原因就在这里。可以用下面的代码打印出当前环境的全部来源及顺序,直观感受优先级链:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(DemoApplication.class).run(args);
// 获取环境对象并遍历所有 PropertySource
MutablePropertySources sources =
context.getEnvironment().getPropertySources();
for (PropertySource<?> source : sources) {
System.out.println(source.getName());
}
}
}运行后可以看到类似 commandLineArgs、systemProperties、systemEnvironment、applicationConfig: [classpath:/application.properties] 这样的名称列表,顺序即优先级顺序。排查配置不生效的问题时,这个列表是第一手证据:如果某个来源排在 applicationConfig 之前,它自然会覆盖文件中的同名配置。
三、整合方式实战:三种加载外部配置的方法
1. 使用 @PropertySource 注解加载额外文件
@PropertySource 是最直接的整合方式,适合加载 jar 包外的自定义配置文件,比如 config/redis.properties。标准用法如下:
@Configuration
@PropertySource(value = "classpath:config/redis.properties",
encoding = "UTF-8",
ignoreResourceNotFound = true)
public class RedisConfig {
@Value("${redis.host:127.0.0.1}")
private String redisHost;
@Value("${redis.port:6379}")
private int redisPort;
}这里有三个细节值得注意。encoding 属性解决 properties 文件的中文乱码问题,因为 JDK 原生读取 properties 使用 ISO-8859-1 编码,不指定 UTF-8 时中文会变成乱码。ignoreResourceNotFound 设为 true 可以避免文件缺失导致启动失败,适合可选配置场景。@Value 中的冒号提供了默认值,当配置来源完全不存在该 key 时不会抛出 IllegalArgumentException。此外,如果需要按 profile 加载不同文件,可以配合 @ConfigurationProperties 或 spring.profiles.active 使用 spring 配置的占位符形式,也可以直接用 @PropertySource 的多值配置声明多个来源。
2. 通过 EnvironmentPostProcessor 注册自定义来源
当需要在所有 application.properties 之前或之后插入配置时,@PropertySource 就不够用了,因为它注册的来源优先级太低。此时可以实现 EnvironmentPostProcessor 接口,在 Spring Boot 准备环境的阶段介入:
public class CustomEnvironmentPostProcessor
implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(
ConfigurableEnvironment environment,
SpringApplication application) {
Map<String, Object> props = new HashMap<>();
props.put("custom.token", "abc123");
MapPropertySource source =
new MapPropertySource("customSource", props);
// addFirst 表示插入到最前面,优先级最高
environment.getPropertySources().addFirst(source);
}
}实现类还需要在 src/main/resources/META-INF/spring.factories 文件中注册才会被自动加载,内容为 org.springframework.boot.env.EnvironmentPostProcessor=完整类名。这种方式的典型应用是配置中心客户端,比如从 Nacos 或 Apollo 拉取远程配置后插入到本地环境,保证远程配置覆盖本地文件。插入位置的选择很关键:用 addFirst 则远程配置完全覆盖本地,用 addLast 则只作为兜底配置,多数配置中心采用前者。
3. 通过命令行和启动参数动态注入
命令行参数是最灵活的配置来源,Spring Boot 会自动把以 -- 开头的参数解析为属性,例如 java -jar app.jar --server.port=8081 --spring.profiles.active=prod。在 IDEA 中调试时,可以在 Run Configuration 的 Program arguments 中填写同样的参数。如果参数较多,也可以通过 SPRING_APPLICATION_JSON 环境变量一次性传入 JSON 格式的多个属性,Spring Boot 会解析成一个独立的 PropertySource。这种方式在容器化部署中特别常用,通过 docker run -e 传入环境变量,再由 relaxed binding 规则将环境变量名映射到配置属性名,比如 SPRING_DATASOURCE_URL 映射为 spring.datasource.url。
四、常见问题排查与注意事项
第一个常见问题是配置不生效。排查思路是先明确该 key 可能存在哪些来源,再通过上面打印 PropertySource 列表的方式确认优先级。典型场景是环境变量中残留了旧配置,其优先级高于 application.properties,文件里怎么改都不起作用。第二个问题是 @PropertySource 与 @ConfigurationProperties 组合时的坑:@ConfigurationProperties 默认只从 Environment 中绑定,@PropertySource 注册的来源可以被读取,但如果在 Bean 初始化之后才注册来源,绑定已经完成,配置自然读不到,因此要保证配置类和属性来源在同一个配置阶段加载。
第三个问题是多环境切换。spring.profiles.active 可以通过环境变量、命令行、application.properties 多种方式设置,遵循同样的优先级链。激活 profile 后,application-{profile}.properties 会插入到 application.properties 之前,形成覆盖关系。如果需要运行时动态添加 PropertySource,可以在任何能拿到 ConfigurableApplicationContext 的地方调用 environment.getPropertySources().addLast(source),但要注意时机:Bean 属性注入发生在容器刷新早期,运行时添加的来源只能影响后续通过 Environment 显式读取的场景,无法改变已注入的字段值。
最后一个建议是养成良好的配置管理习惯:优先使用 application.yml 统一管理,敏感信息通过环境变量或配置中心注入,公共配置放 application.yml,环境差异放 application-{profile}.yml。掌握 PropertySource 的优先级链之后,配置问题基本都能在几分钟内定位到根源,不再需要靠反复重启来碰运气。
Spring BootPropertySource多环境配置修改时间:2026-09-05 08:40:43