改了 application.yml 重启了八次,配置还是不生效
十一月底给服务加一个超时配置,我在 application.yml 里加了这么一行:
http:
connect-timeout: 5000
然后重启,打印出来还是默认值 3000。我以为没编译进去,clean 重新打包,还是 3000。删掉 target 目录重来,还是 3000。来回折腾了快一个小时。
最后发现,启动脚本里有这么一段:
java -jar order-service.jar \
--spring.config.location=/data/conf/ \
--http.connect-timeout=3000
运维同学很早以前加的命令行参数,我完全不知道。命令行参数的优先级比 yml 高,所以无论我怎么改配置文件,都会被它覆盖掉。
Spring Boot 2.0 的 17 种配置源
官方文档里列了一张从高到低的优先级表,一共 17 条。我把它们整理了一下,并标注了实际项目里哪些会用到:
| 顺序 | 配置源 | 我们项目用到 |
|---|---|---|
| 1 | Devtools 全局配置(~/.spring-boot-devtools.properties) | 否 |
| 2 | 测试类上的 @TestPropertySource | 是 |
| 3 | @SpringBootTest 的 properties 属性 | 否 |
| 4 | 命令行参数 | 是(这次的坑) |
| 5 | SPRING_APPLICATION_JSON(环境变量里的内联 JSON) | 否 |
| 6 | ServletConfig 初始化参数 | 否 |
| 7 | ServletContext 初始化参数 | 否 |
| 8 | JNDI 属性(java:comp/env) | 否 |
| 9 | Java 系统属性(System.getProperties()) | 是 |
| 10 | 操作系统环境变量 | 是 |
| 11 | random.* 随机值 | 否 |
| 12 | jar 外部的 profile 配置 application-{profile}.yml | 是 |
| 13 | jar 内部的 profile 配置 application-{profile}.yml | 是 |
| 14 | jar 外部的 application.yml | 是 |
| 15 | jar 内部的 application.yml | 是 |
| 16 | @Configuration 类上的 @PropertySource | 否 |
| 17 | SpringApplication.setDefaultProperties 指定的默认值 | 否 |
不需要全背,记住这几条关键的就够了:
- 命令行参数(第 4)> 系统属性(第 9)> 环境变量(第 10)> jar 外配置(12/14)> jar 内配置(13/15)。
- 带 profile 的(12/13)永远压过不带 profile 的(14/15)。这是很多人搞反的一条。
- jar 包外的配置压过 jar 包内的。这是为了不改包就能改配置,是运维改配置的常规手段。
- 第 16 条
@PropertySource的优先级低于 application.yml,这点反直觉。我以前以为自己用@PropertySource指定的文件优先级最高,实际它只能作为补充,会被 application.yml 覆盖。
怎么知道某个配置最终是从哪来的
这是我觉得最有用的一个技巧。加 actuator 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# Spring Boot 2.0 里大部分端点默认不暴露,要显式开
management.endpoints.web.exposure.include=env,beans,health
然后访问 /actuator/env/http.connect-timeout:
$ curl http://localhost:8080/actuator/env/http.connect-timeout
{
"property": {
"source": "commandLineArgs",
"value": "3000"
},
"activeProfiles": ["prod"],
"propertySources": [
{ "name": "commandLineArgs", "property": { "value": "3000" } },
{ "name": "applicationConfig: [file:./config/application.yml]", "property": { "value": "5000" } },
{ "name": "applicationConfig: [classpath:/application.yml]", "property": { "value": "3000" } }
]
}
propertySources 数组就是按优先级从高到低排的,第一个生效。这个接口直接把"为什么我的配置不生效"的答案摆出来了,比猜快得多。
如果不想加 actuator,也可以临时写个小接口打印:
@RestController
public class EnvController {
@Autowired
private Environment env;
@GetMapping("/debug/env")
public String get() {
return env.getProperty("http.connect-timeout") + " / " +
Arrays.toString(env.getActiveProfiles());
}
}
profile 激活的正确姿势
我们项目有三个 profile:dev、test、prod。文件结构是:
src/main/resources/
├── application.yml # 公共配置
├── application-dev.yml
├── application-test.yml
└── application-prod.yml
激活方式有好几种,优先级从高到低:
# 1. 命令行参数(最高)
java -jar app.jar --spring.profiles.active=prod
# 2. 系统属性
java -Dspring.profiles.active=prod -jar app.jar
# 3. 环境变量(Linux 下点和横线要换成下划线)
export SPRING_PROFILES_ACTIVE=prod
# 4. application.yml 里写(最低,会被上面的覆盖)
spring:
profiles:
active: dev
关于环境变量那条要特别注意:Spring Boot 的宽松绑定规则会把 SPRING_PROFILES_ACTIVE 映射成 spring.profiles.active。环境变量里的 _ 会被当成 .,大写会被转成小写。这条规则对所有配置都适用,所以 HTTP_CONNECT_TIMEOUT 能覆盖 http.connect-timeout。
我在测试环境踩过这个坑:yml 里写的 active: test,但Ubuntu 服务器上有人 export 了 SPRING_PROFILES_ACTIVE=dev,结果连的是开发库。数据被我清了一次才意识到。
另外 Spring Boot 2.0 有个 spring.profiles.include,可以在激活某个 profile 时额外再激活别的:
# application-prod.yml
spring:
profiles:
include:
- prod-db
- prod-mq
这样可以把大配置文件拆开,比往一个 yml 里堆 800 行清爽多了。我们后来把数据库、Redis、MQ 的配置都拆成了独立文件。
spring.config.location 和 additional-location 的区别
这是另一个让我懵了半天的点。我们启动脚本里用了 --spring.config.location:
java -jar app.jar --spring.config.location=/data/conf/
它的语义是替换默认位置。加上这一句之后,Spring Boot 就不去 classpath 下找 application.yml 了,只认 /data/conf/ 目录。所以我把配置改在 resources 里,压根不会被加载——这才是那天"改了 8 次不生效"的另一个原因,命令行参数之外,配置文件的位置也被替换了。
Spring Boot 2.0 新增了 spring.config.additional-location,语义是追加:
java -jar app.jar --spring.config.additional-location=/data/conf/
这样 classpath 里的配置和外部目录的配置都会被加载,且外部目录的优先级更高。这才符合"默认配置打进包里,环境差异配置放外面"的预期。我把启动脚本改成了这个。
yml 语法的几个坑
坑一:缩进不能用 Tab。yml 只认空格,用 Tab 会在启动时报:
org.yaml.snakeyaml.scanner.ScannerException: while scanning for the next token
found character '\t(TAB)' that cannot start any token
补充一句:application.properties 和 application.yml 同时存在时,Spring Boot 2.0 里 properties 的优先级更高(都在同一位置时,properties 排在 yml 前面)。我们项目统一用 yml,避免混用。
坑二:数字和字符串要分清。我写过这样的配置:
order:
no: 0123456 # 被解析成八进制!
yml 里以 0 开头的数字会被当成八进制。要加引号:
order:
no: "0123456"
还有个经典的:version: 1.10 读出来是字符串 "1.10",但 version: 1.0 会被解析成浮点数 1.0,打印出来变成 "1.0" 而不是 "1.00"。凡是可能含前导零、尾随零或者纯数字的,一律加引号。
坑三:多 profile 文档的写法。Spring Boot 2.0 支持在一个 yml 里用 --- 分隔不同 profile:
server:
port: 8080
---
spring:
profiles: dev
server:
port: 8081
---
spring:
profiles: prod
server:
port: 8080
注意 spring.profiles 是单数形式,别顺手写成 spring.profiles.active,而且每个文档块之间必须严格对齐,--- 要顶格。我第一次写的时候把 --- 跟着上面的层级缩进了一格,结果整个文件被当成一段普通文本,profile 根本没生效。
自定义配置的绑定方式
最后是配置怎么读到代码里。我一开始用 @Value:
@Component
public class HttpClientConfig {
@Value("${http.connect-timeout:3000}")
private int connectTimeout;
}
${...:3000} 里的冒号后面是默认值,配置不存在时用它,不会因为缺配置导致启动失败。这个写法我一直保留着。
但配置项一多,@Value 就散得到处都是。我们后来统一用 @ConfigurationProperties:
@Component
@ConfigurationProperties(prefix = "http")
@Data
public class HttpProperties {
private int connectTimeout = 3000;
private int readTimeout = 10000;
private int maxRetry = 3;
private List<String> whiteHosts = new ArrayList<>();
}
http:
connect-timeout: 5000
read-timeout: 15000
max-retry: 2
white-hosts:
- api.example.com
- pay.example.com
两个好处:一是配置集中,二是支持松散绑定——yml 里写 connect-timeout、connectTimeout、甚至 CONNECT_TIMEOUT 都能绑上。IDE 还能给出补全提示(加个 spring-boot-configuration-processor 依赖就能生成元数据)。
注意 @ConfigurationProperties 需要字段有 setter,或者用构造器绑定。我们用了 Lombok 的 @Data,省事。
排查清单
现在遇到配置不生效,我按这个顺序查:
/actuator/env/{key}看生效的是哪个 source;- 检查 jar 同目录下有没有
config/目录(Spring Boot 默认会扫./config/和./); - 检查启动脚本里的
--spring.config.location和命令行参数; env | grep -i spring查环境变量;- 确认激活的 profile 是不是你以为的那个;
- 确认改的文件有没有被打进 jar(
jar tf app.jar | grep yml)。
那天最后我把启动脚本里的 --http.connect-timeout=3000 删掉了,改成在 /data/conf/application-prod.yml 里统一管理。运维同学后来也认同这个做法:命令行参数适合临时的、一次性的覆盖,长期配置还是应该落在配置文件里,不然谁都不知道线上到底跑了什么值。