外部化配置
什么是外部化配置
在软件开发中,外部化配置指的是将应用程序的配置信息(如环境参数、连接地址、业务开关等)与代码逻辑分离,存储在外部资源中(如配置文件、环境变量、数据库等)。Spring Boot 的外部化配置机制并非凭空产生,而是源于软件开发中一系列实际需求的驱动
Spring Boot 外部化配置 (Externalized Configuration) 提供了一套强大的机制,允许我们将应用的配置 从代码中解耦出来,并通过多种外部来源进行灵活管理,从而打造出 可移植、可扩展、易于维护 的 Spring Boot 应用。
SpringBoot支持多种外部化配置,以便于开发者能够在不同的环境下,使用同一套应用程序代码。外部化配置的方式有多种:properties文件,yaml文件,Environment变量已经命令行参数等等。
为什么需要外部化配置
- 硬编码的痛点:若将配置直接写入代码(如数据库连接字符串、端口号),当环境变更时(如从测试环境部署到生产环境),必须修改代码并重新编译,这不仅增加了部署风险,还违背了 “开闭原则”。
- 外部化配置的优势:将配置移至外部文件(如
application.yml
),代码只需通过变量引用配置,环境变更时仅需修改配置文件,无需修改代码。而且外部化配置使得应用的配置管理更加清晰、结构化,易于扩展和维护,适应应用规模的增长。 - 环境多样性需求:开发、测试、预发、生产环境的基础设施(如服务器地址、缓存配置)往往不同,外部化配置可通过
Profile
机制(如
application-dev.yml
、application-prod.yml
)为不同环境定制专属配置,通过命令行参数--spring.profiles.active=prod
即可指定启动环境,无需修改代码或打包不同版本。 - 安全性提升 (Security Enhancement): 可以将敏感信息存储在更安全的地方 (例如环境变量、外部配置中心),避免硬编码在代码中。
多种配置同时出现的生效顺序
Spring Boot 按以下顺序加载配置源,后加载的会覆盖先加载的配置:
配置源优先级列表(由高到低)
DevTools 全局属性:
devtool
处于active
状态时,$HOME/.config/spring-boot
目录中的Devtool全局配置。@TestPropertySource
注解:测试类中指定的属性源。1
2
class MyServiceTest { ... }测试属性:
@SpringBootTest
测试时通过properties
参数指定。1
2
class MyAppTest { ... }命令行参数:通过
--key=value
传递,如--server.port=8080
。SPRING_APPLICATION_JSON
:内置在环境变量或系统属性中的 JSON 配置。1
export SPRING_APPLICATION_JSON='{"server.port": 8081, "app.name": "json-app"}'
ServletConfig 初始化参数:Servlet 的
<init-param>
配置。ServletContext 初始化参数:在
web.xml
中配置的<context-param>
。JNDI 属性:来自
java:comp/env
上下文,传统 Java EE 环境使用。Java 系统属性:通过
System.getProperty()
获取,如-Dapp.env=prod
。1
java -jar app.jar -Dapp.env=prod
OS 环境变量:通过
System.getenv()
获取,如DATABASE_URL
。1
export DATABASE_URL=jdbc:mysql://prod.db:3306/app
随机值配置:
random.*
配置(如random.int
、random.uuid
)。1
2app.secret=${random.uuid}
app.port=${random.int[10000,20000]}配置文件:
application.properties/.yml
及其 Profile 变体(如application-dev.properties
)。- 配置文件内的加载顺序如下
- 首先,同路径下
.properties
优先于.yml
(如application.properties
覆盖application.yml
)。 - 环境专属配置
application-{profile}.properties
高于默认配置application.properties
- 应用程序以外的
application-{profile}.properties或者application.properties
文件高于打包在应用程序内的application-{profile}.properties或者application.properties
,也就是 jar 包外的高于 jar包内的,这就是外部化配置生效的重要原因 - 类路径(内部)中的配置文件, 类下
/config
包 高于 类根路径 - 当前路径(项⽬所在的位置),
/config
⽬录的直接⼦⽬录 高于 当前下/config
⼦⽬录 高于 当前路径
- 首先,同路径下
- 配置文件内的加载顺序如下
@PropertySource
注解:标注在@Configuration
类上,加载指定配置文件。1
2
3
public class AppConfig { ... }默认属性:通过
SpringApplication.setDefaultProperties(Map)
定义的默认值。1
2SpringApplication app = new SpringApplication(MyApp.class);
app.setDefaultProperties(Collections.singletonMap("app.name", "default-app"));
这里列表按组优先级排序,也就是说,任何在高优先级属性源里设置的属性都会覆盖低优先级的相同属性。
所有参数均可由命令⾏传⼊,使⽤ --参数项 = 参数值
,将会被添加到环境变量中,并优先于配置⽂件 。
结论:配置可以写到很多位置,常⻅的优先级顺序:
- 命令⾏ > 配置⽂件 > spring application 配置
建议:⽤⼀种格式的配置⽂件。 .properties 和 结 .yml 同时存在 , 则 .properties 优先
包外 > 包内 ; 同级情况: profile 配置 > application 配置 properties配置 > yaml配置
命令⾏ > 包外config直接⼦⽬录 > 包外config⽬录 > 包外根⽬录 > 包内⽬录
配置文件的加载位置按优先级从高到低排列:
- 命令行参数(最高优先级,可覆盖一切)。
- 当前目录的
/config
子目录(如./config/application.yml
)。 - 当前目录(如
./application.yml
)。 - 类路径的
/config
包(如classpath:/config/application.yml
)。 - 类路径根目录(如
classpath:/application.yml
)。

规律:最外层的最优先。
- 命令⾏ > 所有
- 包外 > 包内
- config⽬录 > 根⽬录
- profile > application
配置不同就都⽣效(互补),配置相同⾼优先级覆盖低优先级
各种外部化配置
导入配置
使⽤spring.config.import
可以导⼊额外配置
让你能在 Spring Boot 主配置文件(比如
application.properties
或
application.yml
)里,引入外部的配置文件或配置片段
,灵活扩展配置来源。
1 | # 1. 导入外部配置文件 |
spring.config.import=my.properties
:
告诉 Spring Boot,加载当前配置文件时,额外导入
my.properties
文件的配置
(my.properties
需和主配置文件在类路径或可识别路径下 )
优先级说明: 无论 spring.config.import
和 my.property=value
谁写在前,my.properties
里的
my.property
值,会覆盖主配置文件里的同名属性 。
这是因为导入的配置,在加载顺序和优先级上更
“高”,用于覆盖主配置里的默认或基础配置
这样一来,就实现了配置拆分,把大而全的配置拆成多个小文件(比如
db.properties
放数据库配置、redis.properties
放 Redis 配置 ),用 spring.config.import
聚合,方便维护,而且不同环境(开发 / 测试 /
生产)的差异化配置,可单独写文件,通过 spring.config.import
按环境引入
属性占位符
配置⽂件中可以使⽤
${name:default}
形式取出之前配置过的值。在配置文件里,动态引用已定义的属性值
,还能设置默认值,让配置更灵活、可复用。
在使用application.properties
中的值的时候,他们会从
Environment
中获取值,那就意味着,可以引用之前定义过的值,比如引用系统属性。具体做法如下:
1 | # 1. 定义基础属性 |
${app.name}
: 引用已定义的app.name
属性值(即MyApp
),最终app.description
里的{app.name}
会被替换成MyApp
。${username:Unknown}
: 尝试引用username
属性,如果username
没定义,就用默认值Unknown
兜底,避免配置缺失报错。
这个在如下情况下很有用
- 配置拼接:比如拼接服务地址
service.url=${host}:${port}
,只需维护host
和port
,动态组合成完整地址。让配置可拆分、可聚合,适配复杂项目的多环境、多模块配置管理,降低单个配置文件的复杂度。 - 默认值兜底:对于一些非必填、或环境差异大的配置(比如开发者本地的用户名
),用
:默认值
避免配置遗漏导致启动失败。实现配置的 “动态引用” 和 “默认兜底”,减少硬编码,提升配置的灵活性和可维护性
随机值配置
在配置文件里,生成随机的字符串、数字等 ,常用于测试环境的动态值(比如随机端口、随机密钥 ),避免固定值引发的冲突或安全问题。
配置文件中${random}
可以用来生成各种不同类型的随机值,从而简化了代码生成的麻烦
Spring Boot 内置了
RandomValuePropertySource
,RandomValuePropertySource
类重写了getProperty
方法,判断以random.
为前缀之后,进行了适当的处理,支持以下语法:
1 | # 1. 随机整数(范围可选,如 1000-9999) |
这样配置的效果:
- 启动应用时,
server.port
会随机生成 1000-9999 之间的整数,避免多个服务本地启动时端口冲突。 my.random.uuid
每次启动都会生成一个唯一的 UUID,可用于动态生成临时 Token、测试用的唯一标识等。
一般在生成随机的加密密钥(如 jwt.secret=${random.value}
),测试环境不用写死密钥,减少泄露风险。
命令行参数配置
默认情况下,SpringApplication将所有的命令行选项参数【以--
开头的参数,如--server.port=9000
】转换为属性,并将它们加入SpringEnvironment中,命令行属性的配置始终优先于其他的属性配置。
所以我们可以运行时通过命令行传入配置参数 ,覆盖配置文件里的默认值,灵活适配不同环境(尤其适合生产环境快速调整配置 )。
使用方式也比较简单,启动 Spring Boot 应用时,通过
--参数=值
的形式传参,示例:
1 | # 方式 1:jar 包启动(经典方式) |
命令行参数的优先级非常高
,会覆盖配置文件(application.properties
/application.yml
)、环境变量里的同名配置。
如果你不希望将命令行属性添加到Environment中,可以使用SpringApplication.setAddCommandLineProperties(false)
禁用它。
加密属性
配置文件里常包含 敏感信息(如数据库密码、API 密钥 ),直接明文存储有泄露风险。
Spring Boot不提供对加密属性值的任何内置支持,但是,它提供了修改Spring环境中的值所必需的挂钩点。所以Spring Boot 可通过扩展实现 配置加密,让敏感配置以密文存储,运行时解密使用。
以jasypt-spring-boot
扩展为例子,先导入如下依赖
1 | <dependency> |
在配置文件(或环境变量)里设置 加密密钥(生产环境建议用环境变量 / 启动参数,避免硬编码 ):
1 | # application.properties |
加密配置值,用 Jasypt 工具生成密文。比如命令行加密(或写代码加密 ):
1 | # 用 jasypt 命令行工具加密(假设密钥是上面的 MySuperSecretKey!2025 ) |
配置文件用密文,把加密后的字符串放到配置里,用 ENC(...)
包裹:
1 | spring.datasource.password=ENC(abcdef123456...) |
Spring Boot 启动时,jasypt-spring-boot
会自动识别
ENC(...)
格式的配置,用密钥解密,注入到对应的 Bean 中。
其中
- 编译期 / 运行期解密:通过自定义
PropertySource
或BeanPostProcessor
,拦截配置加载,对密文做解密处理。 - 密钥安全:生产环境密钥绝不能硬编码到配置文件!建议通过
环境变量(
export JASYPT_ENCRYPTOR_PASSWORD=xxx
)、启动参数(java -jar app.jar --jasypt.encryptor.password=xxx
)传入。
类型安全的属性配置
配置文件里的 key=value
是
松散的字符串配置,直接用 @Value("${key}")
注入时:
- 零散的
@Value
注入在配置多的时候,代码冗余且易出错。 - 无法做 类型校验(比如配置是
int
类型,但填了字符串 )。
例如:
1 | redis.host=localhost |
如果你使用 @Value
注入
1 | import org.springframework.beans.factory.annotation.Value; |
- 每个配置项都要写一个
@Value
注解,重复模板代码多。 - 如果有多个类需要这些配置,每个类都要重复写一遍
@Value
。
@ConfigurationProperties
让配置
结构化、类型安全,把配置映射到 Java
实体类,支持校验、嵌套。
使用步骤如下:
定义配置实体类,用
@ConfigurationProperties
绑定配置前缀1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
public class AppConfig {
private String name; // 对应 app.name
private int port; // 对应 app.port
private List<String> features; // 对应 app.features[0], app.features[1]...
// Getter + Setter 必须有(Lombok 可简化)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
// ...其他 Getter/Setter
}配置文件编写,在
application.properties
里按前缀写配置:1
2
3
4app.name=MyApp
app.port=8080
app.features[0]=security
app.features[1]=logging注入并且使用,在任意 Bean 中注入
AppConfig
,直接用结构化属性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import org.springframework.stereotype.Service;
public class MyService {
private final AppConfig appConfig;
public MyService(AppConfig appConfig) {
this.appConfig = appConfig;
}
public void printConfig() {
System.out.println("Name: " + appConfig.getName()); // 直接用实体类属性
System.out.println("Port: " + appConfig.getPort());
}
}而且可以加
@Validated
和 JSR303 注解,实现配置校验:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import java.util.List;
// 开启校验
public class AppConfig {
private String name;
private int port;
private List<String> features;
// Getter + Setter
}如果配置不符合规则(如
app.port=80
),启动时会直接报错,提前拦截配置错误。
通过@ContructorBinding
注解使用构造器绑定的方式:
简单说,它是 让 @ConfigurationProperties
类通过 构造器
完成属性绑定 的注解,核心解决 “配置类字段不可变(final)”
场景的绑定问题,也能让对象初始化更可控。
如果使用默认 @ConfigurationProperties
,它是用 Setter
方法来绑定属性,这会带来一些问题
- 字段必须可变:类里的字段得有 Setter(即
private String name; public void setName(String name) {...}
),无法用final
修饰。 - 初始化时机模糊:属性通过 Setter 零散赋值,若依赖多字段初始化,容易出现 “部分字段已赋值、部分未赋值” 的中间状态。
在 @ConfigurationProperties
类上,加上
@ConstructorBinding
,明确告诉 Spring
Boot:用构造器来绑定配置属性。
1 | import org.springframework.boot.context.properties.ConfigurationProperties; |
和常规 @ConfigurationProperties
一样,按前缀写配置:
1 | app.name=MyApp |
在需要的地方,通过 构造器注入 或
@Autowired
注入 AppConfig
即可:
1 | import org.springframework.stereotype.Service; |
注意:
必须配合
@ConfigurationProperties
:@ConstructorBinding
不能单独用,必须和@ConfigurationProperties
一起标注在类上。依赖注入方式: 若用构造器绑定,配置类必须能被 Spring 扫描到(或通过
@EnableConfigurationProperties
显式启用 )。 比如非@Component
标注的配置类,需要在启动类加:1
2
3
4
5
6
7
// 显式启用
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}