《Spring Boot 入门》6.3 外部化配置与优先级

本节把配置源按官方 4.x 顺序完整排一遍,并用实测回答「同一个键在五处定义时谁生效」:先给出从命令行、环境变量到默认属性的优先级表,再讲环境变量的命名转换、占位符与 random 值、--key=value 与 -D 的区别,最后给出配置加密的思路与选型。

本节目标:把配置源按官方顺序完整排一遍,并实测回答「同一个键在五处都写了值,到底哪一个生效」,同时掌握环境变量命名、占位符与加密选型。
适用版本:Spring Boot 4.1.x(Java 21)

6.3 外部化配置与优先级

6.1 节讲了怎么按环境分配置文件,6.2 节讲了怎么把配置绑成对象。还剩一个最关键的问题没回答:同一个键可能在命令行、环境变量、多个配置文件里同时出现,谁说了算?本节先给出官方顺序,再用一个实测把它验证出来。

配置数据从哪来

Spring Boot 4.x 用统一的 Config Data API 加载配置,默认会扫描这些位置(按加载顺序排列,越靠后越晚加载):

optional:classpath:/
optional:classpath:/config/
optional:file:./
optional:file:./config/
optional:file:./config/*/

每个位置下都会找 application.properties、application.yml 以及它们的 profile 变体。optional: 表示位置不存在时不报错;config/*/ 表示 config 下的任意一级子目录,方便一个目录放一个环境。

完整的配置源优先级

官方文档给出一张从低到高的表。为便于查阅,这里倒过来从高到低排——越靠上优先级越高,越能覆盖别人。

优先级配置源说明
最高Devtools 全局设置 ~/.config/spring-boot/仅 devtools 激活时生效
↑@TestPropertySource测试专用
↑测试类上的 properties 属性测试专用
↑命令行参数 --key=value部署时最常用的最高优先级
↑SPRING_APPLICATION_JSON内联 JSON
↑ServletConfig / ServletContext 初始化参数war 部署
↑JNDI 属性 java:comp/env
↑Java 系统属性 -Dkey=value
↑操作系统环境变量
↑random.*只读
↑Config data(application.yml 等)见下一小节
↑@PropertySource上下文刷新阶段才加入
最低默认属性 SpringApplication.setDefaultProperties

记忆锚点一句话:命令行 > 系统属性 > 环境变量 > 配置文件 > 代码默认值。

config data 内部也有顺序

上表里那个 “Config data” 本身还分层。从高到低:

优先级位置
高file:./config/*/application-{profile}.yml
↑file:./config/application-{profile}.yml
↑file:./application-{profile}.yml
↑classpath:/config/application-{profile}.yml
↑classpath:/application-{profile}.yml
↑file:./config/*/application.yml
↑file:./config/application.yml
↑file:./application.yml
↑classpath:/config/application.yml
低classpath:/application.yml

两条规律:外部赢内部(file: 高于 classpath:),profile 赢基线。把 application-prod.yml 放到 jar 外的 ./config/ 里,它就能盖过 jar 内打包的一切配置。

环境变量的命名转换

环境变量名受 POSIX 限制(只有大写字母、数字和下划线),所以要把属性名做一次转换:

点(.)变下划线(_),连字符(-)直接删除,然后整体转大写。

配置键(kebab-case)环境变量
spring.datasource.urlSPRING_DATASOURCE_URL
server.portSERVER_PORT
book.page-sizeBOOK_PAGESIZE
book.max-borrow-daysBOOK_MAXBORROWDAYS
spring.jpa.hibernate.ddl-autoSPRING_JPA_HIBERNATE_DDLAUTO

最常见的错误是在连字符处补一个下划线:BOOK_MAX_BORROW_DAYS、SPRING_JPA_HIBERNATE_DDL_AUTO 都不会生效——它们会被当成 book.max.borrow.days、spring.jpa.hibernate.ddl.auto,是完全不同的键。这个坑很隐蔽,因为值不报错,只是「没绑上」。

列表类型用逗号分隔:BOOK_CATEGORIES=小说,技术 能绑到 List<String>。结构复杂的配置用 SPRING_APPLICATION_JSON 塞一段 JSON。

占位符与默认值

配置值里可以引用其他属性,语法是 ${...}:

app:
  host: book.example.com
  port: 8080
  url: http://${app.host}:${app.port}
  timeout: ${app.timeout:3000}
  region: ${REGION:cn-north-1}
  • ${app.port} 引用属性,来源不限当前文件,任何配置源里的值都能引。
  • ${app.timeout:3000} 里的冒号后面是默认值,属性不存在时用它。
  • 默认值可以嵌套:${app.url:http://localhost:${app.port:8080}}。
  • 解析不到又没有默认值,启动直接报 Could not resolve placeholder 'xxx'。
  • 想输出字面量 ${,在 properties 里写成 \${。

@Value 和 @ConfigurationProperties 都支持占位符,区别在于前者把整个值当表达式解析,后者是在绑定阶段逐个解析。

随机值

RandomValuePropertySource 提供一组只读的 random.*:

app:
  instance-id: ${random.uuid}
  secret: ${random.value}
  nonce: ${random.long}
  port: ${random.int(20000,30000)}
  • ${random.value} 是 32 位十六进制字符串,适合当密钥种子。
  • ${random.uuid} 生成 UUID;${random.int}、${random.long} 是任意整数。
  • ${random.int(min,max)} 在给定区间内取一个整数。

坑:random.* 每次读取都会重新生成,不是稳定值。同一个键被两个 Bean 分别注入,会拿到两个不同的值。所以别把它当「本次启动的固定 ID」——那种场景要在代码里生成一次,再放进单例 Bean。

命令行参数与 -D 的区别

两种写法看着像,来源和优先级完全不同。

维度--key=value-Dkey=value
位置-jar 之后,传给应用-jar 之前,传给 JVM
归属Spring 的命令行属性源Java System properties
优先级高于系统属性和环境变量高于环境变量,低于命令行
可见性只有 Spring 看得到System.getProperty() 也能读
写法只能 --key=value只能 -Dkey=value
# 命令行参数:应用级最高优先级
java -jar book-service.jar --spring.profiles.active=prod --server.port=9090

# JVM 系统属性:必须写在 -jar 之前
java -Dserver.port=9090 -Dspring.profiles.active=prod -jar book-service.jar

# 环境变量
SERVER_PORT=9090 SPRING_PROFILES_ACTIVE=prod java -jar book-service.jar

还有两个属性只能在上述「早于环境准备」的渠道里设置:spring.config.location(替换默认配置位置)和 spring.config.additional-location(追加位置)。写进 application.yml 是无效的——因为它们决定了去哪里读 application.yml。

配置加密的思路与选型

生产环境的密码不能明文躺在 Git 里。做法很多,这里只讲思路和选型,不绑定具体库。

方案密钥放哪适用注意
对称加密后写进配置,启动时解密环境变量 / 启动脚本单体、中小团队主密钥不能与密文同仓
配置中心集中下发配置中心自身鉴权多环境多服务引入中心化依赖
密钥管理服务(KMS)云 IAM云上生产有网络与权限依赖
平台原生 Secret 对象容器编排平台容器化注意存储加密与 RBAC
运行时向密钥服务拉取服务端高合规要求增加启动期依赖

无论选哪种,五条原则不变:

  1. 主密钥与密文分离存储,走不同渠道,不能进同一个仓库。
  2. 支持轮换,且轮换不需要重新打包。
  3. 有审计:谁、在什么时候、读了哪个密钥。
  4. 最小权限:dev 环境拿不到 prod 的密钥。
  5. 配置里是明文还是密文要一眼可辨,用命名约定(如 db-password-encrypted)标出来。

实测:同一个键在五处定义

光看表容易忘,我们做一次实测。给图书服务加一个 book.service.timeout,在五个地方都写值:

定义位置值
application.yml1000
application-prod.yml2000
环境变量 BOOK_SERVICE_TIMEOUT3000
系统属性 -Dbook.service.timeout4000
命令行 --book.service.timeout5000

用一个探针 Bean 在启动后把最终值打出来:

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
class ConfigProbe implements ApplicationRunner {
    private final Environment env;

    ConfigProbe(Environment env) {
        this.env = env;
    }

    @Override
    public void run(ApplicationArguments args) {
        System.out.println("book.service.timeout = " + env.getProperty("book.service.timeout"));
    }
}

一次性把所有来源都打开:

BOOK_SERVICE_TIMEOUT=3000 \
java -Dbook.service.timeout=4000 -jar book-service.jar \
  --spring.profiles.active=prod \
  --book.service.timeout=5000

输出:

book.service.timeout = 5000

命令行赢了。 接着逐个撤掉,就能把优先级链条完整验证一遍:

操作生效值谁赢
五个都在5000命令行参数
去掉命令行参数4000Java 系统属性
再去掉 -D3000操作系统环境变量
再去掉环境变量2000application-prod.yml
把 profile 换成 dev1000基线 application.yml

这个顺序和本节开头的表完全吻合:命令行 > 系统属性 > 环境变量 > profile 配置 > 基线配置。

调试这类问题时,/actuator/env 会列出所有属性源及各自的取值,/actuator/configprops 展示绑定后的配置对象,用 management.endpoints.web.exposure.include=env,configprops 打开即可(生产环境务必加访问控制)。

本节常见坑速查

现象原因处理
环境变量不生效连字符处补了下划线删掉连字符,如 BOOK_PAGESIZE
占位符报 Could not resolve placeholder引用的键不存在且没给默认值补键或加 ${x:default}
spring.config.location 无效写进了 application.yml改用命令行或环境变量
随机值前后不一致random.* 每次读取都重算在代码里生成一次并存 Bean
改了配置文件却没生效命令行或环境变量覆盖了它查 /actuator/env
生产密码明文入库没做外部化见加密选型表

小结

  • 配置源有严格顺序:命令行 > 系统属性 > 环境变量 > Config data > @PropertySource > 默认属性。
  • Config data 内部再分两层:外部赢内部(file: 高于 classpath:),profile 赢基线。
  • 环境变量的转换规则是「点变下划线、连字符删除、全大写」;BOOK_MAX_BORROW_DAYS 这类写法是错的。
  • ${x} 引用属性,${x:default} 给默认值,可以嵌套;random.* 每次读取都会重新生成,不是稳定值。
  • --key=value 是应用级参数,优先级最高;-Dkey=value 是 JVM 系统属性,必须写在 -jar 之前。
  • 配置加密的关键不在算法,而在「主密钥与密文分离、可轮换、可审计、最小权限」。

配置和绑定都讲完了,接下来的问题是:spring-boot-starter-webmvc 这样的 starter 里到底装了什么,Spring Boot 又是怎么把自动配置类挑出来的?下一章从 starter 的内部结构讲起。

阅读导航:上一节:6.2 @ConfigurationProperties 类型安全配置 · 下一节:7.1 Starter 的解剖 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计