Featured image of post Spring Boot 配置加载机制详解

Spring Boot 配置加载机制详解

应用配置从哪里来?谁覆盖谁?为什么改了配置不生效?把 Spring Boot 配置加载的全部规则拉直说明白

几乎每个 Spring Boot 项目都遇到过

排查问题时最让人崩溃的一类故障是——

配置改了重启了,行为还是老的。

经验稍丰富的开发会立刻反应过来:它从别处加载了更高优先级的配置,覆盖了你的修改。但这"别处"具体是哪里?覆盖顺序是什么?为什么有些配置改了没生效,有些反而立刻生效?

Spring Boot 的配置加载体系在文档里写得相对分散,本文把它完整拉直说一遍——从 application.yml 到环境变量、从 bootstrap.ymlEnvironmentPostProcessor、从 --server.port=8080 到 K8s ConfigMap,把所有渠道按优先级排成一张图。


一、Spring Boot 配置的"位置"全集

Spring Boot 能从这些地方读取配置(优先级从高到低):

优先级越靠上的源覆盖越靠下的源——同一个 key 多处定义时,最高优先级的值生效。

记住一个原则:

“外部覆盖内部、命令行覆盖文件、Profile 覆盖默认”——这三句话覆盖了 95% 的优先级问题。


二、为什么命令行优先级最高

Spring Boot 把命令行优先级设到最高,是为了给运维一个『不需要改代码也能改配置』的口子

1
java -jar app.jar --server.port=9999 --spring.datasource.url=...

无论 jar 包里 application.yml 写了什么,命令行说了算。

K8s 里也是这个意思——通过 ConfigMap 传环境变量,环境变量优先级高于 jar 内配置,就实现了"同一个 jar 部署到不同环境"。


三、application.yml 在哪些位置会被找到

Spring Boot 启动时按以下顺序查找配置文件Spring Boot 2.3+ 默认):

1
2
3
4
5
1. file:./config/*/    (运行目录 config 下的一级子目录,2.3+ 起最高优先级)
2. file:./config/      (运行目录的 config 子目录)
3. file:./             (运行目录)
4. classpath:/config/  (jar 内的 config/)
5. classpath:/         (jar 内根目录)
  • file: 开头表示文件系统位置
  • classpath: 是 jar 包内
  • 越靠前的位置优先级越高
  • file:./config/*/ 经常被忽视——K8s 把多个 ConfigMap 分别挂到 config/redis/config/db/ 等子目录时,就靠这一项被加载

这就是为什么"jar 同级目录放 application.yml"能覆盖 jar 内配置——是 #2 覆盖 #4 的体现。

文件名默认是 application,可以通过 spring.config.name 改:

1
java -jar app.jar --spring.config.name=myapp

会去找 myapp.yml / myapp.properties

spring.config.location 能完全自定义查找路径:

1
java -jar app.jar --spring.config.location=file:/etc/myapp/

四、Profile 机制:环境维度切换

激活 Profile

1
java -jar app.jar --spring.profiles.active=prod

或者环境变量:

1
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

Profile 文件命名

1
2
3
4
application.yml             ← 共享配置
application-dev.yml         ← dev profile 独占
application-test.yml        ← test profile 独占
application-prod.yml        ← prod profile 独占

激活某个 profile 时,application.ymlapplication-{profile}.yml 都会被加载,profile 文件覆盖通用文件。

多 Profile 同时激活

1
--spring.profiles.active=prod,monitoring,debug

按数组顺序加载,后面的覆盖前面的。这是做"基础 prod 配置 + 加监控 + 加调试日志"组合的标准姿势。

spring.profiles.include

不同于 active 是"激活什么",include 是"再附加什么":

1
2
3
4
# application-prod.yml
spring:
  profiles:
    include: monitoring

激活 prod 时自动也加载 application-monitoring.yml——保证一组配置成套生效,不用每次写命令行参数


五、bootstrap.yml vs application.yml

Spring Cloud 项目里还会出现 bootstrap.yml——它是 Spring Cloud 引入的,比 application.yml 更早加载

bootstrap.yml 的典型用途——配置"配置中心客户端本身"

1
2
3
4
5
6
7
8
9
# bootstrap.yml
spring:
  application:
    name: user-service
  cloud:
    nacos:
      config:
        server-addr: nacos.internal:8848
        namespace: prod

为什么必须更早加载?因为 Nacos / Apollo / Spring Cloud Config 的客户端要先连上配置中心,才能拉到 application.yml 里的业务配置——鸡生蛋、蛋生鸡的问题

Spring Cloud 2020.0 之后官方推荐用 spring.config.import 替代 bootstrap.yml

1
2
3
spring:
  config:
    import: optional:nacos:application.yml

bootstrap.yml 仍然兼容,且大量老项目还在用。


六、外部化配置:12-Factor 的最佳实践

12-Factor App 第三条:“Store config in the environment”——配置应当从环境而来,而不是写死在代码里。Spring Boot 的设计完全贴合这个理念:

1
2
3
4
5
6
# application.yml
spring:
  datasource:
    url: ${DB_URL:jdbc:mysql://localhost:3306/test}
    username: ${DB_USER:root}
    password: ${DB_PASS:}

${VAR:default} 语法——先看环境变量,没有就用默认值。这种写法的好处:

  • 同一个 jar 在 dev/test/prod 三套环境跑
  • K8s 里通过 ConfigMap / Secret 注入
  • 本地开发不需要 Secret,用默认值

K8s 部署时:

1
2
3
4
5
6
7
8
env:
  - name: DB_URL
    value: jdbc:mysql://prod-mysql:3306/order
  - name: DB_PASS
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

七、@Value vs @ConfigurationProperties

业务代码注入配置有两种主流方式。

@Value:单字段注入

1
2
@Value("${server.port:8080}")
private int port;

简单直接,但有几个限制:

  • 不支持类型校验——拼写错了运行时才发现
  • 不支持嵌套对象——只能拿单个值
  • 不便于测试——要在 test 里 mock 整个 Environment

@ConfigurationProperties:批量映射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Component
@ConfigurationProperties(prefix = "feature")
@Validated
public class FeatureConfig {
    @NotBlank
    private String name;
    @Min(0) @Max(100)
    private int threshold;
    private Map<String, String> tags;

    // getters / setters
}
1
2
3
4
5
6
feature:
  name: my-feature
  threshold: 50
  tags:
    region: cn
    env: prod

@ConfigurationProperties 是生产推荐姿势——类型安全、能 @Validated 校验、IDE 提示完整、便于单元测试。

@RefreshScope

配置中心场景下,要让配置变更立刻生效需要加 @RefreshScope

1
2
3
4
@Component
@RefreshScope
@ConfigurationProperties(prefix = "feature")
public class FeatureConfig { ... }

记住

  • @RefreshScope 不要加在 @Configuration
  • @ConfigurationProperties 自带的刷新已经够多数场景用,不一定需要 @RefreshScope
  • 复杂业务的"动态配置"建议直接通过事件订阅,而不是依赖 @RefreshScope 重建 Bean

八、调试技巧:搞清楚配置从哪里来

1. Actuator 的 /actuator/env

1
2
3
4
5
management:
  endpoints:
    web:
      exposure:
        include: env,configprops

调用 /actuator/env 能看到每个 PropertySource、每个 key 的来源和最终值——是排查"配置不生效"的最快办法。

1
2
3
4
5
6
{
  "name": "applicationConfig: [classpath:/application.yml]",
  "properties": {
    "server.port": { "value": 8080 }
  }
}

2. 启动日志 DEBUG

1
java -jar app.jar --debug

会打出每个 starter 的自动配置生效情况。

3. 启动参数检查

如果你怀疑有命令行参数在覆盖配置,看启动脚本里的所有 -D--,再看环境变量:

1
env | grep -i SPRING

很多"诡异覆盖"是 K8s ConfigMap 里悄悄塞了同名变量。


九、几个最常被踩的坑

坑 1:YAML 缩进与冒号后空格

1
2
server:
  port:8080            # ❌ 冒号后没空格 → 当作字符串
1
2
server:
  port: 8080           # ✓

坑 2:Profile 里的相对路径

1
2
3
4
# application-prod.yml
logging:
  file:
    name: logs/app.log    # 相对路径,相对于"工作目录",不是项目目录

部署到容器后工作目录可能不是你以为的位置——生产路径写绝对路径

坑 3:环境变量大小写转换

YAML 里的 spring.datasource.url 对应环境变量是 SPRING_DATASOURCE_URL——点变下划线、全大写。写成 spring.datasource.url 当环境变量是不会被认到的(虽然 Spring Boot 现在支持驼峰转 kebab,但环境变量必须全大写)。

坑 4:占位符递归

1
2
foo: ${bar}
bar: ${foo}              # ❌ 死循环

启动直接报错。但更隐蔽的是:

1
2
3
prefix: app
fullName: ${prefix}-${name:default}
name: ${prefix}-suffix

会展开成预期之外的字符串,自己看不出来。复杂占位符建议拉直写

坑 5:@ConfigurationProperties 没生效

最常见原因是没加 @Component@EnableConfigurationProperties

1
2
3
@Component
@ConfigurationProperties(prefix = "feature")
public class FeatureConfig { ... }

或者:

1
2
3
@Configuration
@EnableConfigurationProperties(FeatureConfig.class)
public class AppConfig { ... }

二者选其一。


十、生产 Checklist

部署前过一遍:

  • 不在代码 / yml 里硬编码密钥、数据库密码
  • 敏感配置通过环境变量 / Secret 注入
  • 不同环境分别有 application-{env}.yml
  • --spring.profiles.active 在启动脚本明确传入
  • 业务代码用 @ConfigurationProperties + @Validated
  • 路径相关配置写绝对路径
  • Actuator envconfigprops 在生产环境关闭或加权限
  • 大型项目的全局配置走 bootstrap.ymlspring.config.import
  • 启动后用 /actuator/env 抽查关键配置的来源

小结

把全文压一句:

Spring Boot 配置加载的本质是『多个 PropertySource 按优先级合并』——只要把『谁覆盖谁』这件事弄明白,所有"配置不生效"的问题都能 5 分钟定位。

记住几条核心规则:

  1. 优先级:命令行 > 环境变量 > 外部文件 > 内部文件 > 默认值
  2. Profile:通用 + profile 配置都加载,profile 覆盖通用
  3. 业务用 @ConfigurationProperties,少用 @Value
  4. 配置中心走 bootstrap.yml / spring.config.import
  5. 排查用 /actuator/env

把这些吃透,配置层基本不会再出意外。

使用 Hugo 构建
主题 StackJimmy 设计