外部化配置 (Externalized Configuration)
Spring Boot 允许你将配置外部化,以便可以在不同环境中使用相同的应用程序代码。你可以使用多种外部配置源,包括 Java 属性文件、YAML 文件、环境变量和命令行参数。
属性值可以通过 @Value 注解直接注入到 Bean 中,通过 Spring 的 Environment 抽象访问,或通过 @ConfigurationProperties 绑定到结构化对象。
1. 配置优先级
Spring Boot 使用特定的 PropertySource 顺序来允许合理地覆盖值。后面的属性源可以覆盖前面定义的值。
配置源按以下顺序考虑(数字越大优先级越高):
- 默认属性(通过
SpringApplication.setDefaultProperties指定) @Configuration类上的@PropertySource注解- 配置数据文件(如
application.properties) RandomValuePropertySource(仅限random.*属性)- 操作系统环境变量
- Java 系统属性(
System.getProperties()) - JNDI 属性(来自
java:comp/env) ServletContext初始化参数ServletConfig初始化参数SPRING_APPLICATION_JSON中的属性- 命令行参数
- 测试中的
properties属性 - 测试中的
@DynamicPropertySource注解 - 测试中的
@TestPropertySource注解 - Devtools 全局设置属性
配置文件优先级
配置数据文件按以下顺序考虑:
- jar 包内的应用属性(
application.properties和 YAML 变体) - jar 包内的特定 Profile 应用属性(
application-{profile}.properties和 YAML 变体) - jar 包外的应用属性(
application.properties和 YAML 变体) - jar 包外的特定 Profile 应用属性(
application-{profile}.properties和 YAML 变体)
建议
建议在整个应用程序中坚持使用一种格式。如果在同一位置同时有 .properties 和 YAML 格式的配置文件,.properties 优先。
示例
假设你开发了一个使用 name 属性的 @Component:
@Component
public class MyBean {
@Value("${name}")
private String name;
// ...
}@Component
class MyBean {
@Value("\${name}")
private val name: String? = null
// ...
}在应用程序类路径中(例如在 jar 内),你可以有一个 application.properties 文件为 name 提供合理的默认值。在新环境中运行时,可以在 jar 外提供覆盖 name 的 application.properties 文件。对于一次性测试,可以使用特定的命令行开关启动(例如 java -jar app.jar --name="Spring")。
2. 访问命令行属性
默认情况下,SpringApplication 将任何命令行选项参数(即以 -- 开头的参数,如 --server.port=9000)转换为 property 并将其添加到 Spring Environment 中。命令行属性始终优先于基于文件的属性源。
如果不希望将命令行属性添加到 Environment,可以使用 SpringApplication.setAddCommandLineProperties(false) 禁用它们。
3. JSON 应用属性
环境变量和系统属性通常有限制,某些属性名称无法使用。为了解决这个问题,Spring Boot 允许你将属性块编码为单个 JSON 结构。
应用程序启动时,任何 spring.application.json 或 SPRING_APPLICATION_JSON 属性都将被解析并添加到 Environment。
例如,可以在 UN*X shell 的命令行中将 SPRING_APPLICATION_JSON 属性作为环境变量提供:
$ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar也可以作为系统属性提供:
$ java -Dspring.application.json='{"my":{"name":"test"}}' -jar myapp.jar或作为命令行参数:
$ java -jar myapp.jar --spring.application.json='{"my":{"name":"test"}}'4. 外部应用属性
Spring Boot 在应用程序启动时会自动从以下位置查找并加载 application.properties 和 application.yaml 文件:
- 从类路径
- 类路径根目录
- 类路径
/config包
- 从当前目录
- 当前目录
- 当前目录中的
config/子目录 config/子目录的直接子目录
列表按优先级排序(较低项的值覆盖较早项)。
4.1 自定义配置文件名
如果不喜欢 application 作为配置文件名,可以通过指定 spring.config.name 环境属性切换到另一个文件名:
$ java -jar myproject.jar --spring.config.name=myproject4.2 自定义配置位置
你还可以使用 spring.config.location 环境属性引用显式位置。此属性接受一个或多个要检查的位置的逗号分隔列表:
$ java -jar myproject.jar --spring.config.location=\
optional:classpath:/default.properties,\
optional:classpath:/override.properties提示
如果位置是可选的且你不介意它们不存在,请使用前缀 optional:。
4.3 可选位置
默认情况下,当指定的配置数据位置不存在时,Spring Boot 将抛出 ConfigDataLocationNotFoundException,应用程序将不会启动。
如果要指定位置但不介意它并不总是存在,可以使用 optional: 前缀。可以将此前缀与 spring.config.location 和 spring.config.additional-location 属性以及 spring.config.import 声明一起使用。
4.4 通配符位置
如果配置文件位置在最后一个路径段中包含 * 字符,则将其视为通配符位置。加载配置时会展开通配符,以便也检查直接子目录。
例如,如果你有一些 Redis 配置和一些 MySQL 配置,你可能希望将这两部分配置分开,同时要求两者都存在于 application.properties 文件中。这可能导致两个单独的 application.properties 文件挂载在不同的位置,例如 /config/redis/application.properties 和 /config/mysql/application.properties。在这种情况下,通配符位置 config/*/ 将导致两个文件都被处理。
::: note 注意 通配符位置必须只包含一个 *,并且对于目录搜索位置以 */ 结尾,对于文件搜索位置以 */<filename> 结尾。 :::
4.5 Profile 特定文件
除了 application 属性文件外,Spring Boot 还会尝试使用命名约定 application-{profile} 加载特定于 profile 的文件。例如,如果你的应用程序激活了名为 prod 的 profile 并使用 YAML 文件,则 application.yaml 和 application-prod.yaml 都将被考虑。
特定于 profile 的属性从与标准 application.properties 相同的位置加载,特定于 profile 的文件始终覆盖非特定文件。如果指定了多个 profile,则应用最后获胜策略。
4.6 导入额外数据
应用程序属性可以使用 spring.config.import 属性从其他位置导入更多配置数据。导入在发现时处理,并被视为紧接在声明导入的文档下方插入的附加文档。
例如,你的类路径 application.properties 文件中可能有以下内容:
spring.application.name=myapp
spring.config.import=optional:file:./dev.propertiesspring:
application:
name: "myapp"
config:
import: "optional:file:./dev.properties"这将触发当前目录中 dev.properties 文件的导入(如果存在这样的文件)。导入的 dev.properties 中的值将优先于触发导入的文件。
4.7 使用环境变量
在云平台(如 Kubernetes)上运行应用程序时,你经常需要读取平台提供的配置值。你可以使用环境变量,也可以使用配置树。
你甚至可以将整个配置以属性或 yaml 格式存储在(多行)环境变量中,并使用 env: 前缀加载它们。假设有一个名为 MY_CONFIGURATION 的环境变量,内容如下:
my.name=Service1
my.cluster=Cluster1使用 env: 前缀可以从此变量导入所有属性:
spring.config.import=env:MY_CONFIGURATIONspring:
config:
import: "env:MY_CONFIGURATION"4.8 使用配置树
在云平台上运行应用程序时,你经常需要读取平台提供的配置值。许多云平台现在允许你将配置映射到挂载的数据卷中。例如,Kubernetes 可以卷挂载 ConfigMaps 和 Secrets。
有两种常见的卷挂载模式:
- 单个文件包含一组完整的属性(通常写为 YAML)
- 多个文件写入目录树,文件名成为"键",内容成为"值"
对于第一种情况,你可以直接使用 spring.config.import 导入 YAML 或属性文件。对于第二种情况,你需要使用 configtree: 前缀,以便 Spring Boot 知道它需要将所有文件公开为属性。
例如,假设 Kubernetes 已挂载以下卷:
etc/
config/
myapp/
username
password要导入这些属性,可以将以下内容添加到 application.properties 或 application.yaml 文件:
spring.config.import=optional:configtree:/etc/config/spring:
config:
import: "optional:configtree:/etc/config/"然后你可以从 Environment 以常规方式访问或注入 myapp.username 和 myapp.password 属性。
4.9 属性占位符
application.properties 和 application.yaml 中的值在使用时会通过现有的 Environment 进行过滤,因此你可以引用先前定义的值(例如,从系统属性或环境变量)。标准的 ${name} 属性占位符语法可以在值中的任何位置使用。
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}app:
name: "MyApp"
description: "${app.name} is a Spring Boot application written by ${username:Unknown}"4.10 使用多文档文件
Spring Boot 允许你将单个物理文件拆分为多个逻辑文档,每个文档独立添加。文档按从上到下的顺序处理。后面的文档可以覆盖前面文档中定义的属性。
对于 application.yaml 文件,使用标准的 YAML 多文档语法。三个连续的连字符表示一个文档的结束和下一个文档的开始:
spring:
application:
name: "MyApp"
---
spring:
application:
name: "MyCloudApp"
config:
activate:
on-cloud-platform: "kubernetes"对于 application.properties 文件,使用特殊的 #--- 或 !--- 注释来标记文档拆分:
spring.application.name=MyApp
#---
spring.application.name=MyCloudApp
spring.config.activate.on-cloud-platform=kubernetes4.11 激活属性
有时仅在满足特定条件时激活给定的一组属性很有用。你可以使用 spring.config.activate.* 有条件地激活属性文档。
以下激活属性可用:
| 属性 | 说明 |
|---|---|
on-profile | 必须匹配的 profile 表达式 |
on-cloud-platform | 必须检测到的 CloudPlatform |
例如,以下内容指定第二个文档仅在 Kubernetes 上运行时以及"prod"或"staging" profile 处于活动状态时才处于活动状态:
myprop=always-set
#---
spring.config.activate.on-cloud-platform=kubernetes
spring.config.activate.on-profile=prod | staging
myotherprop=sometimes-setmyprop: "always-set"
---
spring:
config:
activate:
on-cloud-platform: "kubernetes"
on-profile: "prod | staging"
myotherprop: "sometimes-set"5. 加密属性
Spring Boot 不提供任何内置的属性值加密支持,但它提供了修改 Spring Environment 中包含的值所需的钩子点。EnvironmentPostProcessor 接口允许你在应用程序启动之前操作 Environment。
如果你需要一种安全的方式来存储凭据和密码,Spring Cloud Vault 项目提供了在 HashiCorp Vault 中存储外部化配置的支持。
6. 使用 YAML
YAML 是 JSON 的超集,因此是指定分层配置数据的便捷格式。只要类路径上有 SnakeYAML 库,SpringApplication 类就会自动支持 YAML 作为属性的替代方案。
6.1 将 YAML 映射到属性
YAML 文档需要从其分层格式转换为可与 Spring Environment 一起使用的扁平结构。例如,考虑以下 YAML 文档:
environments:
dev:
url: "https://dev.example.com"
name: "Developer Setup"
prod:
url: "https://another.example.com"
name: "My Cool App"为了从 Environment 访问这些属性,它们将被扁平化如下:
environments.dev.url=https://dev.example.com
environments.dev.name=Developer Setup
environments.prod.url=https://another.example.com
environments.prod.name=My Cool AppYAML 列表也需要扁平化。它们表示为带有 [index] 解引用器的属性键:
my:
servers:
- "dev.example.com"
- "another.example.com"转换为:
my.servers[0]=dev.example.com
my.servers[1]=another.example.com7. 配置随机值
RandomValuePropertySource 对于注入随机值(例如,注入秘密或测试用例)很有用。它可以生成整数、长整数、uuid 或字符串:
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number-less-than-ten=${random.int(10)}
my.number-in-range=${random.int[1024,65536]}my:
secret: "${random.value}"
number: "${random.int}"
bignumber: "${random.long}"
uuid: "${random.uuid}"
number-less-than-ten: "${random.int(10)}"
number-in-range: "${random.int[1024,65536]}"8. 类型安全的配置属性
使用 @Value("${property}") 注解注入配置属性有时会很麻烦,特别是如果你使用多个属性或你的数据本质上是分层的。Spring Boot 提供了一种使用属性的替代方法,该方法允许强类型 Bean 管理和验证应用程序的配置。
8.1 JavaBean 属性绑定
可以绑定声明标准 JavaBean 属性的 Bean:
@ConfigurationProperties("my.service")
public class MyProperties {
private boolean enabled;
private InetAddress remoteAddress;
private final Security security = new Security();
// getters / setters...
public static class Security {
private String username;
private String password;
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
// getters / setters...
}
}@ConfigurationProperties("my.service")
class MyProperties {
var isEnabled = false
var remoteAddress: InetAddress? = null
val security = Security()
class Security {
var username: String? = null
var password: String? = null
var roles: List<String> = ArrayList(setOf("USER"))
}
}8.2 构造函数绑定
前面的示例可以以不可变方式重写:
@ConfigurationProperties("my.service")
public class MyProperties {
private final boolean enabled;
private final InetAddress remoteAddress;
private final Security security;
public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) {
this.enabled = enabled;
this.remoteAddress = remoteAddress;
this.security = security;
}
// getters...
public static class Security {
private final String username;
private final String password;
private final List<String> roles;
public Security(String username, String password, @DefaultValue("USER") List<String> roles) {
this.username = username;
this.password = password;
this.roles = roles;
}
// getters...
}
}@ConfigurationProperties("my.service")
class MyProperties(val enabled: Boolean, val remoteAddress: InetAddress,
val security: Security) {
class Security(val username: String, val password: String,
@param:DefaultValue("USER") val roles: List<String>)
}8.3 启用 @ConfigurationProperties 注解的类型
Spring Boot 提供了绑定 @ConfigurationProperties 类型并将其注册为 Bean 的基础设施。你可以逐个类启用配置属性,也可以启用与组件扫描类似的配置属性扫描。
要启用配置属性,请在任何 @Configuration 类上使用 @EnableConfigurationProperties 注解:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties.class)
public class MyConfiguration {
}@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties::class)
class MyConfiguration要使用配置属性扫描,请将 @ConfigurationPropertiesScan 注解添加到你的应用程序:
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {
}@SpringBootApplication
@ConfigurationPropertiesScan("com.example.app", "com.example.another")
class MyApplication8.4 使用 @ConfigurationProperties 注解的类型
要使用 @ConfigurationProperties Bean,你可以像注入任何其他 Bean 一样注入它们:
@Service
public class MyService {
private final MyProperties properties;
public MyService(MyProperties properties) {
this.properties = properties;
}
public void openConnection() {
Server server = new Server(this.properties.getRemoteAddress());
server.start();
}
}@Service
class MyService(val properties: MyProperties) {
fun openConnection() {
val server = Server(properties.remoteAddress)
server.start()
}
}8.5 宽松绑定
Spring Boot 使用一些宽松的规则将 Environment 属性绑定到 @ConfigurationProperties Bean,因此 Environment 属性名称和 Bean 属性名称之间不需要完全匹配。
例如,以下 @ConfigurationProperties 类:
@ConfigurationProperties("my.main-project.person")
public class MyPersonProperties {
private String firstName;
// getters / setters...
}可以使用以下任何属性名称:
| 属性 | 说明 |
|---|---|
my.main-project.person.first-name | Kebab case,推荐在 .properties 和 YAML 文件中使用 |
my.main-project.person.firstName | 标准驼峰式语法 |
my.main-project.person.first_name | 下划线表示法 |
MY_MAINPROJECT_PERSON_FIRSTNAME | 大写格式,推荐在系统环境变量中使用 |
8.6 @ConfigurationProperties 验证
只要使用 Spring 的 @Validated 注解,Spring Boot 就会尝试验证 @ConfigurationProperties 类。你可以直接在配置类上使用 JSR-303 javax.validation 约束注解:
@ConfigurationProperties("my.service")
@Validated
public class MyProperties {
@NotNull
private InetAddress remoteAddress;
@Valid
private final Security security = new Security();
// getters / setters...
public static class Security {
@NotEmpty
private String username;
// getters / setters...
}
}@ConfigurationProperties("my.service")
@Validated
class MyProperties {
@field:NotNull
var remoteAddress: InetAddress? = null
@field:Valid
val security = Security()
class Security {
@field:NotEmpty
var username: String? = null
}
}补充教学
1. 配置优先级的实战理解
在实际开发中,配置优先级的理解至关重要。以下是一个典型场景:
场景:你有一个应用程序需要在开发、测试和生产环境中运行。
- 开发环境:使用
application-dev.properties,数据库指向本地 - 测试环境:使用
application-test.properties,数据库指向测试服务器 - 生产环境:使用环境变量覆盖敏感信息(如数据库密码)
优先级链:
环境变量 > 命令行参数 > application-{profile}.properties > application.properties2. Profile 的最佳实践
推荐的 Profile 组织方式:
application.properties # 通用配置
application-dev.properties # 开发环境
application-test.properties # 测试环境
application-prod.properties # 生产环境激活 Profile:
# 方式1:命令行
java -jar app.jar --spring.profiles.active=prod
# 方式2:环境变量
export SPRING_PROFILES_ACTIVE=prod
# 方式3:application.properties
spring.profiles.active=prod3. 配置树 vs 环境变量
何时使用配置树:
- Kubernetes ConfigMap/Secret
- Docker Swarm Secrets
- 需要挂载多个配置文件
何时使用环境变量:
- 简单的键值对
- 12-Factor App 原则
- 容器化部署
4. @ConfigurationProperties vs @Value
| 特性 | @ConfigurationProperties | @Value |
|---|---|---|
| 宽松绑定 | ✅ 支持 | ❌ 不支持 |
| 元数据支持 | ✅ 支持 | ❌ 不支持 |
| SpEL 表达式 | ❌ 不支持 | ✅ 支持 |
| 复杂类型绑定 | ✅ 支持 | ⚠️ 有限支持 |
| 验证 | ✅ 支持 JSR-303 | ❌ 不支持 |
| 推荐场景 | 结构化配置 | 简单值注入 |
5. 常见陷阱与解决方案
陷阱1:环境变量命名
# ❌ 错误:环境变量不支持点号
my.service.url=xxx
# ✅ 正确:使用下划线和大写
MY_SERVICE_URL=xxx陷阱2:YAML 缩进
# ❌ 错误:缩进不一致
spring:
application:
name: myapp
# ✅ 正确:使用2个空格缩进
spring:
application:
name: myapp陷阱3:构造函数绑定忘记编译参数
<!-- Maven:确保添加 -parameters -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>6. 性能优化建议
- 减少配置文件扫描:明确指定
spring.config.location而不是依赖默认扫描 - 使用构造函数绑定:比 JavaBean 绑定更快,且线程安全
- 避免过度使用 @Value:对于复杂配置,使用
@ConfigurationProperties - 缓存配置:对于不变的配置,使用
final字段
7. 安全最佳实践
敏感信息不要提交到版本控制:
gitignoreapplication-prod.properties application-*.properties使用配置加密:
- Spring Cloud Config Server 加密
- Jasypt 加密
- HashiCorp Vault
环境变量优先:生产环境的敏感信息通过环境变量注入
8. 调试配置问题
查看所有配置源:
@Component
public class ConfigDebugger implements CommandLineRunner {
@Autowired
private Environment env;
@Override
public void run(String... args) {
MutablePropertySources sources = ((ConfigurableEnvironment) env).getPropertySources();
sources.forEach(source -> {
System.out.println("Source: " + source.getName());
});
}
}使用 Actuator 端点:
management:
endpoints:
web:
exposure:
include: env,configprops访问 /actuator/env 查看所有属性源和值。