资深 Java 面试到 Agent 开发工程师 · 第 24 集
假设 application.properties 里写着 lab.timeout=80,程序读出的却是 90,启动命令末尾还带着 --lab.timeout=90。两处配置都被加载了,启动参数的优先级更高,所以最终取到 90。
Spring Boot 中,一个配置名可以同时存在于文件、环境变量、JVM 参数和命令行参数里。程序取值时按配置源的顺序查找,找到第一个非空值就返回。排查这类问题,要找到实际胜出的配置源,再检查程序是在何时把这个值写进对象的。
文件里的值,为什么没有成为最终值?
PropertySource 是一组有名字的配置,例如 systemProperties 保存 JVM 系统属性,systemEnvironment 保存进程环境变量。Environment 把这些配置源组织起来,提供 getProperty() 等统一读取方法。
以 Spring Boot 2.6.13 的常见启动方式为例,下面这些来源中,靠上的配置能够覆盖靠下的同名配置:
| 常见来源(从高到低) | 写法示例 |
|---|---|
| 命令行参数 | java -jar app.jar --lab.timeout=90 |
| Java 系统属性 | java -Dlab.timeout=75 -jar app.jar |
| 操作系统环境变量 | LAB_TIMEOUT=70 |
| 配置数据,包括 properties、YAML、profile 文件 | lab.timeout=80 |
SpringApplication.setDefaultProperties() |
程序设置的默认值 |
这是常用来源的节选。Boot 的完整顺序还包括测试属性、SPRING_APPLICATION_JSON、Servlet/JNDI 等来源;遇到它们,需要放回完整顺序判断。应用也可以添加自定义配置源,因此不能只凭一张表断言所有项目的最终值。
同一个配置在文件中出现多次,也有顺序。默认配置搜索中,外部 profile 文件高于外部普通文件,外部普通文件又高于包内 profile 文件和包内普通文件。自定义 spring.config.location、配置导入或位置分组以后,应按实际加载结果确认;显式设置 spring.config.location 会替换默认搜索位置。
profile 是一组按环境启用的配置。激活 prod 后,application-prod.properties 可以覆盖同位置的 application.properties。在同一位置同时存在 properties 和 YAML 时,Boot 建议保持一种格式;该版本会优先采用 properties。

表中的数字来自一组逐项增加来源的对照:普通文件为 80,prod 文件为 60;启用 prod 读到 60,再加 LAB_TIMEOUT=70 读到 70,再加 -Dlab.timeout=75 读到 75,最后加 --lab.timeout=90 读到 90。文件没有被启动参数改写,取值过程选择了另一处值。
覆盖发生在读取时,还是把文件改掉了?
Spring 的 PropertySourcesPropertyResolver负责从配置源读取值。下面保留决定查找顺序的部分,省略日志和空集合处理:
java
for (PropertySource<?> propertySource : this.propertySources) {
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
// 解析值里的 ${other.key},不是把它当成普通字符串
value = resolveNestedPlaceholders((String) value);
}
return convertValueIfNecessary(value, targetValueType);
}
}
return null;
propertySources 的迭代顺序就是本次查找顺序。返回第一个非空值以后,后面的来源不再参与这次读取。类型转换发生在返回前,例如把字符串 "90" 转成整数。空字符串也是非 null 值,会挡住低优先级来源;如果目标是整数,后续还可能转换失败。
因此,修复时只改低优先级的文件没有作用。部署脚本仍带着 --lab.timeout=90,文件从 80 改到 100,程序依然读到 90。需要删除过时的启动参数,或把应由部署控制的值改在部署配置中,然后重新启动并核对最终来源。
环境变量另有名称映射。lab.timeout 可以用 LAB_TIMEOUT 提供,@ConfigurationProperties 绑定还支持宽松命名:规范属性名中的点替换成下划线、连字符去掉,再转大写。例如 lab.connection-timeout 对应 LAB_CONNECTIONTIMEOUT。排查时同时核对规范名和实际变量名,不能靠任意加下划线猜测是否等价。
读到配置以后,怎样进入 Java 对象?
假设超时必须大于零,应用把相关值绑定到同一个对象:
java
@ConfigurationProperties(prefix = "lab")
@Validated
public class LabProperties {
@Min(1)
private int timeout;
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
}
通过 @EnableConfigurationProperties(LabProperties.class) 注册它。prefix="lab" 表示把 lab 下的属性匹配到字段;@Validated 要求执行已配置的约束校验,@Min(1) 拒绝小于 1 的数字。应用需要 Bean Validation 实现才能完成这项校验。
ConfigurationPropertiesBindingPostProcessor在 Bean 初始化之前识别这样的对象并绑定:
java
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}
ConfigurationPropertiesBean.get() 提取配置前缀、绑定方式和校验注解;内部 Binder 使用适配后的配置源,匹配 lab.timeout,把文本转换成字段类型,再通过这里的 setter 写入对象。校验约束不成立时,绑定失败,默认会使这个 Bean 的创建失败并阻止应用正常启动。
绑定还可能遇到两类不同错误:lab.timeout=abc 无法转换为整数;lab.timeout=0 可以转换成整数,但违反 @Min(1)。只检查文件"有没有这个键",不能排除这两种错误。
@Value("${lab.timeout}") 也会从环境解析占位符,但它用于把单个表达式的结果注入字段或参数;@ConfigurationProperties 适合把同一前缀的一组设置绑定为结构化对象,并做类型转换及校验。两者都不会因为对象里有个字段,就自动监视磁盘文件。
怎么查出到底是哪一处配置赢了?
读出最终值,只能确认结果。要确定来源,可以在受控诊断中查看 Boot 的配置属性来源,并读取首个匹配项的 origin。origin 是配置来源说明,文件配置通常包含文件名和行列,环境变量会指出实际变量名:
java
ConfigurationPropertyName name =
ConfigurationPropertyName.of("lab.timeout");
for (ConfigurationPropertySource source :
ConfigurationPropertySources.get(environment)) {
ConfigurationProperty property = source.getConfigurationProperty(name);
if (property != null) {
System.out.println(property.getValue());
System.out.println(property.getOrigin());
break; // 第一个匹配项就是这次查找采用的来源
}
}
这里的类型来自 Boot 的 context.properties.source 包。代码使用规范属性名,因此能通过配置源适配器识别环境变量形式。不要把所有配置无差别打印到日志;密码和令牌也可能在同一环境中。定位某个非敏感键即可。
如果项目已经启用 Actuator,可以在授权的管理入口用 env 查看配置源,用 configprops 查看配置对象的绑定信息;端点是否暴露、权限和敏感值遮蔽由项目配置决定。不要为了查一个超时值,把管理端点直接暴露到公网。
核对时还要看真实进程的参数、环境、工作目录与激活的 profile。修改本机的文件,却启动了另一个工作目录下的应用,也会得到完全合理但不符合预期的值。来源记录能把"文件读错"和"文件被覆盖"分开。
文件改对了,为什么已创建的对象还是旧值?
配置文件加载到配置源、配置对象绑定,是启动过程中的两个步骤。文件被读取后,通常保存在内存中的配置源里。直接修改磁盘文件,既不保证内存配置源改变,也不保证已经创建的配置对象重新绑定。

对照实验中,在 Bean 已经绑定为 90 以后,通过程序给 Environment 放入一个最高优先级的配置源,值设为 999。再次调用 environment.getProperty("lab.timeout") 得到 999,原对象的 getTimeout() 仍然返回 90。即使读取入口已能看到新值,普通绑定对象也没有自动重建。
如果希望改配置后生效,有两种明确的运行方式:启动配置修改后重启应用,重新加载并绑定;或者使用项目中确实支持的动态配置/刷新机制,核对它更新的是配置源、绑定对象,还是连连接池等使用方也会重建。Spring Cloud 等组件的刷新行为,需要按实际组件和对象生命周期验证,不能由 Boot 的普通绑定推断。
读取配置后还会有下一层状态。例如代码用超时值创建了客户端,后来只更新 LabProperties.timeout,客户端内部原有的超时设置仍可能不变。确认生效要检查真正执行请求的客户端,而不止是配置对象的 getter。
常见面试题怎么答?
配置文件和环境变量都写了同一个属性,最终听谁的?
在 Boot 默认配置顺序中,操作系统环境变量高于配置文件,JVM 系统属性和命令行参数还可能继续覆盖它。判断具体应用时,应查看实际 PropertySource 顺序与该键的来源。自定义来源、测试属性和配置导入都可能影响结果。
@ConfigurationProperties 绑定失败和配置被覆盖,怎么区分?
覆盖后仍可能是合法值,程序会正常启动,但最终来源与预期不同;绑定失败则是最终选中的值无法转换,或转换后不满足约束。先确认胜出来源,再看转换和校验异常中的属性名、输入值及原因。只改没有被采用的文件无法解决问题。
Environment 读到了新值,@ConfigurationProperties 一定更新吗?
普通启动绑定不会自动更新现存对象。Environment 的查询与对象的字段值是不同状态;重新绑定、重建 Bean 及更新使用方需要额外机制。磁盘文件修改还要更早检查是否重新加载进配置源。
读完自测
-
普通文件写 80,prod 文件写 60,启动命令带
--lab.timeout=90。现在把 prod 文件改成 100,再重启,应该读到多少?参考要点:仍是 90,命令行参数优先。删除该参数并确认没有其他高优先级来源后,prod 文件里的 100 才有机会成为最终值。
-
启动时报超时属性无法绑定,发现环境变量
LAB_TIMEOUT=abc,而文件里是合法的 80。能否依靠文件里的默认值继续启动?参考要点:该环境变量先被选中,转换失败不会因此自动退回文件里的 80。修正或删除错误来源,并检查约束;宽松命名只负责匹配名称,不会把错误值变成合法值。
-
动态配置把配置对象的超时从 90 改为 100,但客户端仍用 90,接下来查哪里?
参考要点:查客户端创建及更新路径。客户端可能只在构造时复制配置,必须通过其更新 API 或重建对象才能改变行为;同时确认新请求确实使用更新后的实例。