Administrator
发布于 2018-11-28 / 1399 阅读
24

application.yml 配置不生效?Spring Boot 配置优先级梳理

改了 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 条。我把它们整理了一下,并标注了实际项目里哪些会用到:

顺序配置源我们项目用到
1Devtools 全局配置(~/.spring-boot-devtools.properties)
2测试类上的 @TestPropertySource
3@SpringBootTest 的 properties 属性
4命令行参数是(这次的坑)
5SPRING_APPLICATION_JSON(环境变量里的内联 JSON)
6ServletConfig 初始化参数
7ServletContext 初始化参数
8JNDI 属性(java:comp/env)
9Java 系统属性(System.getProperties())
10操作系统环境变量
11random.* 随机值
12jar 外部的 profile 配置 application-{profile}.yml
13jar 内部的 profile 配置 application-{profile}.yml
14jar 外部的 application.yml
15jar 内部的 application.yml
16@Configuration 类上的 @PropertySource
17SpringApplication.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-timeoutconnectTimeout、甚至 CONNECT_TIMEOUT 都能绑上。IDE 还能给出补全提示(加个 spring-boot-configuration-processor 依赖就能生成元数据)。

注意 @ConfigurationProperties 需要字段有 setter,或者用构造器绑定。我们用了 Lombok 的 @Data,省事。

排查清单

现在遇到配置不生效,我按这个顺序查:

  1. /actuator/env/{key} 看生效的是哪个 source;
  2. 检查 jar 同目录下有没有 config/ 目录(Spring Boot 默认会扫 ./config/./);
  3. 检查启动脚本里的 --spring.config.location 和命令行参数;
  4. env | grep -i spring 查环境变量;
  5. 确认激活的 profile 是不是你以为的那个;
  6. 确认改的文件有没有被打进 jar(jar tf app.jar | grep yml)。

那天最后我把启动脚本里的 --http.connect-timeout=3000 删掉了,改成在 /data/conf/application-prod.yml 里统一管理。运维同学后来也认同这个做法:命令行参数适合临时的、一次性的覆盖,长期配置还是应该落在配置文件里,不然谁都不知道线上到底跑了什么值。

参考