Externalized Configuration
Spring Boot 允许你将配置外部化,以便在不同环境中使用相同的应用程序代码。你可以使用多种外部配置源,包括 Java 属性文件、YAML 文件、环境变量和命令行参数。
属性值可以通过使用 @Value 注解直接注入到你的 Bean 中,也可以通过 Spring 的 Environment 抽象进行访问,或者通过 @ConfigurationProperties 绑定到结构化对象。
Spring Boot 使用了一种非常特定的 PropertySource 顺序,该顺序旨在允许对值进行合理的覆盖。后面的 PropertySource 可以覆盖前面定义的值。
Sources 按以下顺序考虑:
-
默认属性(通过调用 SpringApplication.setDefaultProperties(Map) 设置)。
-
在你的 @Configuration 类上的 @PropertySource 注解。请注意,此类属性源直到应用上下文开始刷新时才会被添加到 Environment 中。这已经太晚了,无法配置某些在刷新开始前就读取的属性,例如
logging.*和spring.main.*。 -
配置数据(如
application.properties文件)。 -
仅包含
random.*属性的 RandomValuePropertySource。 -
操作系统环境变量。
-
Java 系统属性(
System.getProperties())。 -
来自
java:comp/env的 JNDI 属性。 -
ServletContext 初始化参数。
-
ServletConfig 初始化参数。
-
来自
SPRING_APPLICATION_JSON的属性(内嵌在环境变量或系统属性中的 JSON 字符串)。 -
命令行参数。
-
测试类中的
properties属性。可用于 @SpringBootTest 以及 用于测试应用程序特定切片的测试注解。 -
测试中的 @DynamicPropertySource 注解。
-
测试类上的 @TestPropertySource 注解。
-
当 devtools 处于激活状态时,位于
$HOME/.config/spring-boot目录下的 Devtools 全局设置属性。
配置数据文件按以下顺序被考虑:
-
应用程序属性 打包在你的 jar 内部(
application.properties及其 YAML 变体)。 -
特定于 Profile 的应用程序属性 打包在你的 jar 内部(
application-{profile}.properties及其 YAML 变体)。 -
应用程序属性 位于打包的 jar 外部(
application.properties及其 YAML 变体)。 -
特定于 Profile 的应用程序属性 位于打包的 jar 外部(
application-{profile}.properties及其 YAML 变体)。
建议在整个应用程序中坚持使用一种格式。如果你在同一位置同时拥有 .properties 和 YAML 格式的配置文件,.properties 文件将具有更高的优先级。
如果你使用环境变量而非系统属性,大多数操作系统不允许使用带点号分隔的键名,但你可以改用下划线(例如,使用 SPRING_CONFIG_NAME 而不是 spring.config.name)。详情请参见 从环境变量绑定。
如果你的应用程序运行在 servlet 容器或应用服务器中,那么可以使用 JNDI 属性(位于 java:comp/env 中)或 servlet 上下文初始化参数,来替代环境变量或系统属性,或者与它们一起使用。
为了提供一个具体的示例,假设你开发了一个 @Component,它使用了一个 name 属性,如下例所示:
- Java
- Kotlin
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class MyBean {
@Value("${name}")
private String name;
// ...
}
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
@Component
class MyBean {
@Value("\${name}")
private val name: String? = null
// ...
}
在你的应用程序 classpath 中(例如,在你的 jar 文件内部),可以包含一个 application.properties 文件,为 name 提供一个合理的默认属性值。当在新环境中运行时,可以在 jar 文件外部提供一个 application.properties 文件,以覆盖 name 的值。对于一次性测试,你可以通过特定的命令行参数启动(例如,java -jar app.jar --name="Spring")。
env 和 configprops 端点可用于确定某个属性为何具有特定的值。你可以使用这两个端点来诊断属性值异常的问题。详见 Production ready features 章节。
访问命令行属性
默认情况下,SpringApplication 会将任何命令行选项参数(即以 -- 开头的参数,例如 --server.port=9000)转换为一个 property,并将其添加到 Spring Environment 中。如前所述,命令行属性始终优先于基于文件的属性源。
如果你不希望将命令行属性添加到 Environment,可以通过使用 SpringApplication.setAddCommandLineProperties(false) 来禁用它们。
JSON 应用属性
环境变量和系统属性通常存在一些限制,导致某些属性名称无法使用。为了解决这个问题,Spring Boot 允许你将一组属性编码为单个 JSON 结构。
当你的应用程序启动时,任何 spring.application.json 或 SPRING_APPLICATION_JSON 属性都会被解析并添加到 Environment 中。
例如,SPRING_APPLICATION_JSON 属性可以在 UN*X shell 的命令行中作为环境变量提供:
$ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar
在前面的示例中,最终你会在 Spring Environment 中得到 my.name=test。
相同的 JSON 也可以作为系统属性提供:
$ java -Dspring.application.json='{"my":{"name":"test"}}' -jar myapp.jar
或者,你可以通过命令行参数提供 JSON:
$ java -jar myapp.jar --spring.application.json='{"my":{"name":"test"}}'
如果你部署到一个经典的应用服务器(Application Server),也可以使用一个名为 java:comp/env/spring.application.json 的 JNDI 变量。
尽管来自 JSON 的 null 值会被添加到生成的属性源中,但 PropertySourcesPropertyResolver 会将 null 属性视为缺失值。这意味着 JSON 无法使用 null 值覆盖来自较低优先级属性源中的属性。
外部应用程序属性
Spring Boot 在应用程序启动时会自动从以下位置查找并加载 application.properties 和 application.yaml 文件:
-
从 classpath 中
-
classpath 根目录
-
classpath 中的
/config包
-
-
从当前目录中
-
当前目录
-
当前目录下的
config/子目录 -
config/子目录的直接子目录
-
该列表按优先级排序(后面项的值会覆盖前面项的值)。从加载的文件中读取的文档会被添加为 PropertySource 实例到 Spring 的 Environment 中。
如果你不喜欢使用 application 作为配置文件名,可以通过指定 spring.config.name 环境属性来切换为其他文件名。例如,若要查找 myproject.properties 和 myproject.yaml 文件,你可以按如下方式运行你的应用程序:
$ java -jar myproject.jar --spring.config.name=myproject
你也可以通过使用 spring.config.location 环境属性来指定一个明确的位置。该属性接受一个逗号分隔的列表,包含一个或多个要检查的位置。
以下示例展示了如何指定两个不同的文件:
$ java -jar myproject.jar --spring.config.location=\
optional:classpath:/default.properties,\
optional:classpath:/override.properties
如果 位置是可选的,并且你不介意它们不存在,请使用前缀 optional:。
spring.config.name、spring.config.location 和 spring.config.additional-location 会被非常早地用于确定需要加载哪些文件。它们必须被定义为环境属性(通常为操作系统环境变量、系统属性或命令行参数)。
如果 spring.config.location 包含的是目录(而非文件),则应以 / 结尾。在运行时,它们会与由 spring.config.name 生成的文件名拼接后再进行加载。而 spring.config.location 中指定的文件则会被直接导入。
目录和文件位置的值也会被扩展,以检查是否存在特定于 profile 的文件。例如,如果你设置了 spring.config.location 为 classpath:myconfig.properties,那么相应的 classpath:myconfig-<profile>.properties 文件也会被加载。
在大多数情况下,你添加的每个 spring.config.location 项都会引用单个文件或目录。位置按照定义的顺序进行处理,后面的位置可以覆盖前面位置的值。
如果你有复杂的位置设置,并且使用了特定于 profile 的配置文件,你可能需要提供额外的提示,以便 Spring Boot 知道应该如何对它们进行分组。一个位置组(location group)是一组在相同层级上被考虑的位置集合。例如,你可能希望将所有 classpath 位置归为一组,然后将所有外部位置归为另一组。位置组内的各项应使用 ; 分隔。更多详细信息,请参见 Profile Specific Files 部分中的示例。
通过 spring.config.location 配置的位置会替换默认位置。例如,如果 spring.config.location 被配置为 optional:classpath:/custom-config/,optional:file:./custom-config/,则考虑的完整位置集合为:
-
optional:classpath:custom-config/ -
optional:file:./custom-config/
如果你希望添加额外的位置,而不是替换默认位置,可以使用 spring.config.additional-location。从额外位置加载的属性可以覆盖默认位置中的属性。例如,如果 spring.config.additional-location 配置的值为 optional:classpath:/custom-config/,optional:file:./custom-config/,那么考虑的完整位置集合为:
-
optional:classpath:/;optional:classpath:/config/ -
optional:file:./;optional:file:./config/;optional:file:./config/*/ -
optional:classpath:custom-config/ -
optional:file:./custom-config/
这种搜索顺序允许你在某个配置文件中指定默认值,然后在另一个配置文件中有选择地覆盖这些值。你可以在某个默认位置的 application.properties(或使用 spring.config.name 指定的其他 basename)中为你的应用程序提供默认值。这些默认值随后可以在运行时通过位于某个自定义位置的其他文件进行覆盖。
可选位置
默认情况下,当指定的配置数据位置不存在时,Spring Boot 会抛出一个 ConfigDataLocationNotFoundException,并且你的应用程序将无法启动。
如果你想指定一个位置,但不介意该位置并非始终存在,可以使用 optional: 前缀。该前缀可用于 spring.config.location 和 spring.config.additional-location 属性,以及 spring.config.import 声明中。
例如,spring.config.import 的值为 optional:file:./myconfig.properties 时,即使 myconfig.properties 文件缺失,你的应用程序也能正常启动。
如果你想忽略所有的 ConfigDataLocationNotFoundException 错误并始终继续启动你的应用程序,可以使用 spring.config.on-not-found 属性。通过 SpringApplication.setDefaultProperties(…) 或系统 / 环境变量将该属性的值设置为 ignore。
通配符位置
如果配置文件位置的最后一个路径段包含 * 字符,则该位置被视为通配符位置。在加载配置时,通配符会被展开,以检查其直接子目录。在 Kubernetes 等环境中,当存在多个配置属性来源时,通配符位置特别有用。
例如,如果你有一些 Redis 配置和一些 MySQL 配置,你可能希望将这两部分配置分开,同时又要求它们都存在于一个 application.properties 文件中。这可能会导致两个独立的 application.properties 文件被挂载到不同的位置,例如 /config/redis/application.properties 和 /config/mysql/application.properties。在这种情况下,使用通配符路径 config/*/ 将会使得这两个文件都被处理。
默认情况下,Spring Boot 会在默认的搜索位置中包含 config/*/。这意味着 jar 包外部的 /config 目录下的所有子目录都会被搜索。
你可以使用 spring.config.location 和 spring.config.additional-location 属性自行指定通配符位置。
通配符位置必须仅包含一个 *,并且对于目录搜索位置,必须以 */ 结尾;对于文件搜索位置,则必须以 */<filename> 结尾。含有通配符的位置会根据文件名的绝对路径按字母顺序排序。
通配符位置仅适用于外部目录。你不能在 classpath: 位置中使用通配符。
Profile Specific Files
除了 application 属性文件外,Spring Boot 还会尝试使用命名约定 application-{profile} 加载特定于 profile 的文件。例如,如果你的应用激活了一个名为 prod 的 profile 并使用 YAML 文件,那么 application.yaml 和 application-prod.yaml 都会被加载。
特定于 Profile 的属性从与标准 application.properties 相同的位置加载,且特定于 Profile 的文件始终会覆盖非特定的文件。如果指定了多个 Profile,则采用“后胜”(last-wins)策略。例如,如果通过 spring.profiles.active 属性指定了 prod,live 这两个 Profile,则 application-prod.properties 中的值可以被 application-live.properties 中的值覆盖。
“后写入优先”(last-wins)策略适用于 位置组 级别。spring.config.location 设置为 classpath:/cfg/,classpath:/ext/ 时的覆盖规则与 classpath:/cfg/;classpath:/ext/ 不同。
例如,延续上面的 prod,live 示例,我们可能有以下文件:
/cfg
application-live.properties
/ext
application-live.properties
application-prod.properties
当 spring.config.location 为 classpath:/cfg/,classpath:/ext/ 时,我们会先处理所有 /cfg 目录下的文件,再处理所有 /ext 目录下的文件:
-
/cfg/application-live.properties -
/ext/application-prod.properties -
/ext/application-live.properties
而当使用 classpath:/cfg/;classpath:/ext/(以 ; 作为分隔符)时,/cfg 和 /ext 会被视为同一级别进行处理:
-
/ext/application-prod.properties -
/cfg/application-live.properties -
/ext/application-live.properties
Environment 有一组默认的 profile(默认为 [default]),在没有设置激活的 profile 时会使用这些默认 profile。换句话说,如果没有显式激活任何 profile,则会考虑 application-default 中的属性。
属性文件只会被加载一次。如果你已经直接 导入 了一个特定于配置文件的属性文件,那么它将不会被第二次导入。
导入额外数据
应用程序属性可以使用 spring.config.import 属性从其他位置导入更多的配置数据。导入操作在被发现时即进行处理,并被视为额外的文档,直接插入到声明该导入的文档下方。
例如,你的 classpath 中的 application.properties 文件可能包含以下内容:
- Properties
- YAML
spring.application.name=myapp
spring.config.import=optional:file:./dev.properties
spring:
application:
name: "myapp"
config:
import: "optional:file:./dev.properties"
这将触发导入当前目录下的 dev.properties 文件(如果该文件存在)。从导入的 dev.properties 中获取的值将优先于触发导入的文件中的值。在上述示例中,dev.properties 可以将 spring.application.name 重新定义为另一个值。
一个 import 无论被声明多少次,都只会被导入一次。
使用 “Fixed” 和 “Import Relative” 位置
导入可以指定为 fixed(固定)位置或 import relative(导入相对)位置。固定位置始终解析为相同的底层资源,无论 spring.config.import 属性在何处声明。导入相对位置则相对于声明 spring.config.import 属性的文件进行解析。
以正斜杠(/)或 URL 风格前缀(file:、classpath: 等)开头的位置被视为固定位置。所有其他位置都被视为相对于导入的位置。
optional: 前缀在判断一个位置是固定路径还是导入相对路径时不予考虑。
例如,假设我们有一个 /demo 目录,其中包含 application.jar 文件。我们可以在该目录下添加一个 /demo/application.properties 文件,内容如下:
spring.config.import=optional:core/core.properties
这是一个相对导入位置,因此如果文件 /demo/core/core.properties 存在,将会尝试加载该文件。
如果 /demo/core/core.properties 具有以下内容:
spring.config.import=optional:extra/extra.properties
它将尝试加载 /demo/core/extra/extra.properties。optional:extra/extra.properties 是相对于 /demo/core/core.properties 的,因此完整的路径为 /demo/core/ + extra/extra.properties。
属性排序
在 properties/yaml 文件的单个文档中,import 的定义顺序无关紧要。例如,下面两个示例会产生相同的结果:
- Properties
- YAML
spring.config.import=my.properties
my.property=value
spring:
config:
import: "my.properties"
my:
property: "value"
- Properties
- YAML
my.property=value
spring.config.import=my.properties
my:
property: "value"
spring:
config:
import: "my.properties"
在上述两个示例中,my.properties 文件中的值将优先于触发其导入的文件。
可以在单个 spring.config.import 键下指定多个位置。这些位置将按照定义的顺序进行处理,后导入的配置优先级更高。
在适当的情况下,也会考虑导入特定于 Profile 的变体。上述示例将同时导入 my.properties 以及任何 my-<profile>.properties 变体。
Spring Boot 提供了可插拔的 API,允许支持各种不同的位置地址。默认情况下,你可以导入 Java Properties、YAML 和 配置树(configuration trees)。
第三方 jar 包可以提供对其他技术的支持(文件无需一定是本地的)。例如,你可以想象配置数据来自 Consul、Apache ZooKeeper 或 Netflix Archaius 等外部存储。
如果你想支持自己的位置,请参阅 org.springframework.boot.context.config 包中的 ConfigDataLocationResolver 和 ConfigDataLoader 类。
导入无扩展名文件
某些云平台无法为挂载卷中的文件添加文件扩展名。要导入这些没有扩展名的文件,你需要给 Spring Boot 一个提示,以便它知道如何加载这些文件。你可以通过在方括号中放置一个扩展名提示来实现这一点。
例如,假设你有一个 /etc/config/myconfig 文件,希望将其作为 YAML 导入。你可以通过以下方式从 application.properties 中导入它:
- Properties
- YAML
spring.config.import=file:/etc/config/myconfig[.yaml]
spring:
config:
import: "file:/etc/config/myconfig[.yaml]"
使用环境变量
在云平台(例如 Kubernetes)上运行应用程序时,通常需要读取平台提供的配置值。为此,你可以使用环境变量,也可以使用配置树。
你甚至可以将完整的配置以 properties 或 yaml 格式存储在(多行)环境变量中,并使用 env: 前缀加载它们。假设有一个名为 MY_CONFIGURATION 的环境变量,其内容如下:
my.name=Service1
my.cluster=Cluster1
使用 env: 前缀,可以导入该变量中的所有属性:
- Properties
- YAML
spring.config.import=env:MY_CONFIGURATION
spring:
config:
import: "env:MY_CONFIGURATION"
此功能还支持指定扩展名。默认扩展名为 .properties。
使用配置树
将配置值存储在环境变量中存在一些缺点,特别是当该值应当保密时。
作为环境变量的替代方案,许多云平台现在允许你将配置映射到挂载的数据卷中。例如,Kubernetes 可以将 ConfigMaps 和 Secrets 作为卷挂载。
有两种常见的卷挂载模式可以使用:
-
单个文件包含一整套属性(通常以 YAML 格式编写)。
-
多个文件被写入一个目录树中,其中文件名成为 “key”,文件内容成为 “value”。
对于第一种情况,你可以直接使用 spring.config.import 导入 YAML 或 Properties 文件,如上文所述。对于第二种情况,你需要使用 configtree: 前缀,以便 Spring Boot 知道它需要将所有文件作为属性暴露出来。
举个例子,假设 Kubernetes 挂载了以下卷:
etc/
config/
myapp/
username
password
username 文件的内容将是一个配置值,而 password 文件的内容则是一个 secret。
要导入这些属性,您可以在 application.properties 或 application.yaml 文件中添加以下内容:
- Properties
- YAML
spring.config.import=optional:configtree:/etc/config/
spring:
config:
import: "optional:configtree:/etc/config/"
然后,你可以以常规方式从 Environment 中访问或注入 myapp.username 和 myapp.password 属性。
配置树(config tree)下文件夹和文件的名称构成了属性名。在上述示例中,若要以 username 和 password 的形式访问属性,可将 spring.config.import 设置为 optional:configtree:/etc/config/myapp。
使用点号表示法的文件名也会被正确映射。例如,在上述示例中,/etc/config 目录下名为 myapp.username 的文件将生成 Environment 中的 myapp.username 属性。
配置树的值可以根据预期内容绑定到字符串 String 类型或 byte[] 类型。
如果你有多个配置树需要从同一个父文件夹导入,可以使用通配符快捷方式。任何以 /*/ 结尾的 configtree: 位置都会将所有直接子目录作为配置树导入。与非通配符导入一样,每个配置树下的文件夹和文件名称将构成属性名。
例如,给定以下卷:
etc/
config/
dbconfig/
db/
username
password
mqconfig/
mq/
username
password
你可以使用 configtree:/etc/config/*/ 作为导入位置:
- Properties
- YAML
spring.config.import=optional:configtree:/etc/config/*/
spring:
config:
import: "optional:configtree:/etc/config/*/"
这将添加 db.username、db.password、mq.username 和 mq.password 属性。
使用通配符加载的目录会按字母顺序排序。如果你需要不同的顺序,则应将每个位置作为单独的 import 列出。
配置树也可用于 Docker secrets。当 Docker Swarm 服务被授予对某个 secret 的访问权限时,该 secret 会被挂载到容器中。例如,如果一个名为 db.password 的 secret 被挂载到 /run/secrets/ 位置,你可以通过以下方式将 db.password 提供给 Spring 环境:
- Properties
- YAML
spring.config.import=optional:configtree:/run/secrets/
spring:
config:
import: "optional:configtree:/run/secrets/"
属性占位符
application.properties 和 application.yaml 中的值在使用时会通过现有的 Environment 进行过滤,因此你可以引用之前定义的值(例如来自系统属性或环境变量)。标准的 ${name} 属性占位符语法可以在值中的任意位置使用。属性占位符还可以使用 : 指定默认值,将默认值与属性名分隔开,例如 ${name:default}。
以下示例展示了使用带默认值和不带默认值的占位符:
- Properties
- YAML
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}"
假设 username 属性在其他地方未被设置,app.description 的值将为 MyApp is a Spring Boot application written by Unknown。
在占位符中引用属性名称时,应始终使用其规范形式(即仅使用小写字母的 kebab-case 形式)。这样 Spring Boot 就能使用与 宽松绑定(relaxed binding) 在处理 @ConfigurationProperties 时相同的逻辑。
例如,${demo.item-price} 会从 application.properties 文件中匹配 demo.item-price 和 demo.itemPrice 形式,以及从系统环境变量中匹配 DEMO_ITEMPRICE。但如果你使用 ${demo.itemPrice},则 demo.item-price 和 DEMO_ITEMPRICE 将不会被考虑。
你也可以使用此技术为现有的 Spring Boot 属性创建“简短”变体。详情请参阅“How-to Guides”中的 使用‘简短’命令行参数 一节。
处理多文档文件
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=kubernetes
属性文件分隔符不能包含任何前导空白字符,并且必须恰好包含三个连字符(hyphen)。分隔符前后紧邻的行不能使用相同的注释前缀。
多文档属性文件通常与激活属性(如 spring.config.activate.on-profile)结合使用。详见下一节。
多文档属性文件无法通过 @PropertySource 或 @TestPropertySource 注解加载。
Activation Properties
有时,仅在满足特定条件时激活一组给定的属性会很有用。例如,你可能有一些属性仅在某个特定 profile 处于激活状态时才相关。
你可以使用 spring.config.activate.* 来有条件地激活一个属性文件。
以下激活属性可用:
表 1. 激活属性
| 属性 | 说明 |
|---|---|
on-profile | 一个 profile 表达式,文档要处于激活状态必须匹配该表达式;或者一个 profile 表达式列表,只要其中至少有一个匹配,文档就会被激活。 |
on-cloud-platform | 必须检测到的 CloudPlatform,文档才会处于激活状态。 |
例如,以下内容指定第二个文档仅在 Kubernetes 上运行时激活,并且仅当 “prod” 或 “staging” 配置文件处于激活状态时才生效:
- Properties
- YAML
myprop=always-set
#---
spring.config.activate.on-cloud-platform=kubernetes
spring.config.activate.on-profile=prod | staging
myotherprop=sometimes-set
myprop:
"always-set"
---
spring:
config:
activate:
on-cloud-platform: "kubernetes"
on-profile: "prod | staging"
myotherprop: "sometimes-set"
加密属性
Spring Boot 未提供对属性值加密的内置支持,但它提供了必要的钩子点,用于修改 Spring Environment 中包含的值。EnvironmentPostProcessor 接口允许你在应用程序启动之前操作 Environment。详见 在启动前自定义 Environment 或 ApplicationContext。
如果你需要一种安全的方式来存储凭证和密码,Spring Cloud Vault 项目提供了对将外部化配置存储在 HashiCorp Vault 中的支持。
使用 YAML
YAML 是 JSON 的超集,因此是一种用于指定层次化配置数据的便捷格式。当你在 classpath 中包含 SnakeYAML 库时,SpringApplication 类会自动支持 YAML 作为 properties 文件的替代方案。
如果你使用 starters,SnakeYAML 会由 spring-boot-starter 自动提供。
将 YAML 映射到 Properties
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 App
同样,YAML 列表也需要被扁平化。它们以带有 [index] 解引用符的属性键来表示。例如,考虑以下 YAML:
my:
servers:
- "dev.example.com"
- "another.example.com"
前面的示例将被转换为以下属性:
my.servers[0]=dev.example.com
my.servers[1]=another.example.com
使用 [index] 表示法的属性可以通过 Spring Boot 的 Binder 类绑定到 Java 的 List 或 Set 对象。更多详情请参见下面的 Type-safe Configuration Properties 小节。
YAML 文件无法通过 @PropertySource 或 @TestPropertySource 注解加载。因此,如果你需要以这种方式加载值,则必须使用 properties 文件。
直接加载 YAML
Spring Framework 提供了两个便捷的类,可用于加载 YAML 文档。YamlPropertiesFactoryBean 将 YAML 加载为 Properties,而 YamlMapFactoryBean 将 YAML 加载为 Map。
如果你想要将 YAML 加载为 Spring 的 PropertySource,也可以使用 YamlPropertySourceLoader 类。
配置随机值
RandomValuePropertySource 对于注入随机值(例如,用于密钥或测试用例)非常有用。它可以生成整数、长整型、UUID 或字符串,如下例所示:
- Properties
- YAML
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]}"
random.int* 语法为 OPEN value (,max) CLOSE,其中 OPEN、CLOSE 可以是任意字符,而 value、max 为整数。如果提供了 max,则 value 为最小值,max 为最大值(不包含)。
配置系统环境属性
Spring Boot 支持为环境属性设置前缀。当系统环境被多个具有不同配置需求的 Spring Boot 应用共享时,这一功能非常有用。可以在运行应用之前,通过调用 setEnvironmentPrefix(…) 方法直接在 SpringApplication 上设置系统环境属性的前缀。
例如,如果你将前缀设置为 input,那么像 remote.timeout 这样的属性在系统环境变量中将被解析为 INPUT_REMOTE_TIMEOUT。
该前缀 仅 适用于系统环境属性。上述示例在从其他来源读取属性时,仍会继续使用 remote.timeout。
类型安全的配置属性
使用 @Value("${property}") 注解注入配置属性有时会显得繁琐,特别是当你处理多个属性或数据具有层次结构时。Spring Boot 提供了一种替代方法来处理属性,该方法允许使用强类型的 Bean 来管理和验证应用程序的配置。
JavaBean 属性绑定
可以绑定一个声明了标准 JavaBean 属性的 bean,如下例所示:
- Java
- Kotlin
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my.service")
public class MyProperties {
private boolean enabled;
private InetAddress remoteAddress;
private final Security security = new Security();
// getters / setters...
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public InetAddress getRemoteAddress() {
return this.remoteAddress;
}
public void setRemoteAddress(InetAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
public Security getSecurity() {
return this.security;
}
public static class Security {
private String username;
private String password;
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
// getters / setters...
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public List<String> getRoles() {
return this.roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import java.net.InetAddress
@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"))
}
}
上述 POJO 定义了以下属性:
-
my.service.enabled,默认值为false。 -
my.service.remote-address,其类型可从 String 转换而来。 -
my.service.security.username,包含一个嵌套的 "security" 对象,该对象的名称由属性名决定。特别地,此处完全未使用类型,也可以是 SecurityProperties。 -
my.service.security.password。 -
my.service.security.roles,一个 String 类型的集合,默认值为USER。
若要在属性名称中使用保留关键字(例如 my.service.import),请在该属性字段上使用 @Name 注解。
映射到 Spring Boot 中可用的 @ConfigurationProperties 类的属性,这些属性通过属性文件、YAML 文件、环境变量及其他机制进行配置,属于公共 API,但该类本身的访问器(getter/setter)并不打算被直接使用。
这种安排依赖于默认的无参构造函数,并且通常必须提供 getter 和 setter 方法,因为绑定是通过标准的 Java Beans 属性描述符进行的,就像在 Spring MVC 中一样。在以下情况下可以省略 setter:
-
已预先初始化的
Map和Collection,只要它们使用的是可变实现(如前面示例中的roles字段)。 -
已预先初始化的嵌套 POJO(如前面示例中的
Security字段)。如果你希望绑定器通过其默认构造函数动态创建实例,则需要提供一个 setter。
有些人使用 Project Lombok 自动生成 getter 和 setter。请确保 Lombok 不会为此类类型生成任何特定的构造函数,因为容器会自动使用该构造函数来实例化对象。
最后,仅考虑标准的 Java Bean 属性,不支持对静态属性进行绑定。
构造函数绑定
上一节中的示例可以按照以下示例以不可变的方式重写:
- Java
- Kotlin
import java.net.InetAddress;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
@ConfigurationProperties("my.service")
public class MyProperties {
// fields...
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 boolean isEnabled() {
return this.enabled;
}
public InetAddress getRemoteAddress() {
return this.remoteAddress;
}
public Security getSecurity() {
return this.security;
}
public static class Security {
// fields...
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...
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public List<String> getRoles() {
return this.roles;
}
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.DefaultValue
import java.net.InetAddress
@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>)
}
在此设置中,存在一个带参数的构造函数意味着应使用构造函数绑定。这意味着绑定器将查找一个具有你希望被绑定的参数的构造函数。如果你的类有多个构造函数,可以使用 @ConstructorBinding 注解来指定用于构造函数绑定的构造函数。
要为某个类退出构造函数绑定,必须使用 @Autowired 注解标注其带参数的构造函数,或者将其设为 private。Kotlin 开发者可以使用一个空的主构造函数来退出构造函数绑定。
例如:
- Java
- Kotlin
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my")
public class MyProperties {
// fields...
final MyBean myBean;
private String name;
@Autowired
public MyProperties(MyBean myBean) {
this.myBean = myBean;
}
// getters / setters...
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("my")
class MyProperties() {
constructor(name: String) : this() {
this.name = name
}
// vars...
var name: String? = null
}
构造函数绑定(Constructor binding)可以与记录类(records)一起使用。除非你的记录类有多个构造函数,否则无需使用 @ConstructorBinding。
构造函数绑定类的嵌套成员(例如上面示例中的 Security)也将通过其构造函数进行绑定。
可以使用 @DefaultValue 注解在构造函数参数和 record 组件上指定默认值。转换服务(conversion service)将被用来将注解中的 String 值强制转换为缺失属性的目标类型。
参考前面的例子,如果没有属性绑定到 Security,则 MyProperties 实例中的 security 将包含一个 null 值。为了使其即使在没有属性绑定时也包含一个非 null 的 Security 实例(在使用 Kotlin 时,这将要求 Security 的 username 和 password 参数声明为可空类型,因为它们没有默认值),请使用一个空的 @DefaultValue 注解:
- Java
- Kotlin
public MyProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security) {
this.enabled = enabled;
this.remoteAddress = remoteAddress;
this.security = security;
}
class MyProperties(val enabled: Boolean, val remoteAddress: InetAddress,
@DefaultValue val security: Security) {
class Security(val username: String?, val password: String?,
@param:DefaultValue("USER") val roles: List<String>)
}
要使用构造函数绑定,必须通过 @EnableConfigurationProperties 或配置属性扫描来启用该类。不能对通过常规 Spring 机制创建的 Bean 使用构造函数绑定(例如 @Component 注解的 Bean、使用 @Bean 方法创建的 Bean,或通过 @Import 加载的 Bean)。
要使用构造函数绑定,类必须使用 -parameters 参数进行编译。如果你使用 Spring Boot 的 Gradle 插件,或者使用 Maven 并且基于 spring-boot-starter-parent,这将自动完成。
不建议将 Optional 与 @ConfigurationProperties 一起使用,因为 Optional 主要设计为返回类型。因此,它并不适合用于配置属性注入。为了与其他类型的属性保持一致,如果你声明了一个 Optional 类型的属性但未提供值,则绑定的值将是 null,而不是一个空的 Optional。
若要在属性名称中使用保留关键字(例如 my.service.import),请在构造函数参数上使用 @Name 注解。
启用带有 @ConfigurationProperties 注解的类型
Spring Boot 提供了基础设施,用于绑定 @ConfigurationProperties 类型并将其注册为 Bean。你可以逐类启用配置属性,也可以启用配置属性扫描,其工作方式类似于组件扫描。
有时,使用 @ConfigurationProperties 注解的类可能不适合被自动扫描,例如,当你正在开发自己的自动配置,或者希望有条件地启用它们时。在这些情况下,可以使用 @EnableConfigurationProperties 注解来指定要处理的类型列表。这可以在任意 @Configuration 类上完成,如下例所示:
- Java
- Kotlin
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties.class)
public class MyConfiguration {
}
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties::class)
class MyConfiguration
- Java
- Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("some.properties")
public class SomeProperties {
}
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("some.properties")
class SomeProperties
要使用配置属性扫描功能,请在你的应用程序中添加 @ConfigurationPropertiesScan 注解。通常,该注解会添加到带有 @SpringBootApplication 注解的主应用程序类上,但也可以添加到任意 @Configuration 类上。默认情况下,扫描将从声明该注解的类所在的包开始进行。如果你想指定要扫描的特定包,可以按照以下示例进行操作:
- Java
- Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {
}
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
@SpringBootApplication
@ConfigurationPropertiesScan("com.example.app", "com.example.another")
class MyApplication
当使用配置属性扫描(configuration property scanning)或通过 @EnableConfigurationProperties 注册 @ConfigurationProperties bean 时,该 bean 会有一个约定的名称:<prefix>-<fqn>,其中 <prefix> 是 @ConfigurationProperties 注解中指定的环境属性键前缀,<fqn> 是该 bean 的全限定名(fully qualified name)。如果注解中未提供任何前缀,则仅使用 bean 的全限定名。
假设 SomeProperties 位于 com.example.app 包中,则上述 SomeProperties 示例的 bean 名称为 some.properties-com.example.app.SomeProperties。
我们建议 @ConfigurationProperties 仅处理环境配置,尤其不要从上下文中注入其他 Bean。对于特殊情况,可以使用 setter 注入,或者使用框架提供的任意 *Aware 接口(例如,如果需要访问 Environment,可使用 EnvironmentAware)。如果你仍希望通过构造函数注入其他 Bean,则配置属性 Bean 必须使用 @Component 注解,并采用基于 JavaBean 的属性绑定方式。
使用 @ConfigurationProperties 注解的类型
这种配置方式与 SpringApplication 的外部 YAML 配置配合得特别好,如下例所示:
my:
service:
remote-address: 192.168.1.1
security:
username: "admin"
roles:
- "USER"
- "ADMIN"
要使用 @ConfigurationProperties Bean,你可以像注入其他任何 Bean 一样进行注入,如下例所示:
- Java
- Kotlin
import org.springframework.stereotype.Service;
@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();
// ...
}
// ...
}
import org.springframework.stereotype.Service
@Service
class MyService(val properties: MyProperties) {
fun openConnection() {
val server = Server(properties.remoteAddress)
server.start()
// ...
}
// ...
}
使用 @ConfigurationProperties 还可以生成元数据文件,供 IDE 为你的自定义属性键提供自动补全功能。详见附录。
第三方配置
除了使用 @ConfigurationProperties 注解一个类之外,你还可以将其用在 public @Bean 方法上。当你希望将属性绑定到不受你控制的第三方组件时,这样做尤其有用。
要从 Environment 属性中配置一个 bean,请在其 bean 注册上添加 @ConfigurationProperties,如下例所示:
- Java
- Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ThirdPartyConfiguration {
@Bean
@ConfigurationProperties("another")
public AnotherComponent anotherComponent() {
return new AnotherComponent();
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration(proxyBeanMethods = false)
class ThirdPartyConfiguration {
@Bean
@ConfigurationProperties("another")
fun anotherComponent(): AnotherComponent = AnotherComponent()
}
任何使用 another 前缀定义的 JavaBean 属性都会以类似于前面 SomeProperties 示例的方式映射到该 AnotherComponent bean 上。
宽松绑定
Spring Boot 在将 Environment 属性绑定到 @ConfigurationProperties Bean 时,采用了一些宽松的规则,因此 Environment 属性名与 Bean 属性名之间无需完全匹配。常见的有用场景包括:使用短横线分隔的环境属性(例如,context-path 会绑定到 contextPath),以及大写的环境属性(例如,PORT 会绑定到 port)。
例如,考虑以下 @ConfigurationProperties 类:
- Java
- Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my.main-project.person")
public class MyPersonProperties {
private String firstName;
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("my.main-project.person")
class MyPersonProperties {
var firstName: String? = null
}
使用上述代码,以下属性名称均可使用:
表 2. relaxed binding
| 属性 | 说明 |
|---|---|
my.main-project.person.first-name | Kebab case,推荐在 .properties 和 YAML 文件中使用。 |
my.main-project.person.firstName | 标准的驼峰命名法(camel case)语法。 |
my.main-project.person.first_name | 下划线命名法(underscore notation),是 .properties 和 YAML 文件中的另一种可选格式。 |
MY_MAINPROJECT_PERSON_FIRSTNAME | 大写格式,推荐在使用系统环境变量时采用。 |
注解的 prefix 值必须使用 kebab case(小写字母,并用 - 分隔,例如 my.main-project.person)。
表 3. 每个属性源的宽松绑定规则
我们建议在可能的情况下,属性以小写 kebab 格式存储,例如 my.person.first-name=Rod。
绑定 Map
在绑定到 Map 属性时,你可能需要使用一种特殊的方括号表示法,以保留原始的 key 值。如果 key 未被 [] 包围,则所有非字母数字、- 或 . 的字符都会被移除。
例如,考虑将以下属性绑定到一个 Map<String,String>:
- Properties
- YAML
my.map[/key1]=value1
my.map[/key2]=value2
my.map./key3=value3
my:
map:
"[/key1]": "value1"
"[/key2]": "value2"
"/key3": "value3"
对于 YAML 文件,括号需要被引号包围,以确保键能被正确解析。
上述属性将绑定到一个 Map,其中 /key1、/key2 和 key3 作为该 Map 中的键。key3 中的斜杠已被移除,因为它未被方括号包围。
在绑定到标量值(scalar values)时,包含 . 的键无需用 [] 包裹。标量值包括枚举类型以及 java.lang 包中的所有类型,但 Object 除外。将 a.b=c 绑定到 Map<String, String> 时,键中的 . 会被保留,并返回一个包含条目 {"a.b"="c"} 的 Map。对于其他任何类型,如果 key 中包含 .,则必须使用方括号表示法。例如,将 a.b=c 绑定到 Map<String, Object> 会返回一个包含条目 {"a"={"b"="c"}} 的 Map,而 [a.b]=c 则会返回一个包含条目 {"a.b"="c"} 的 Map。
从环境变量绑定
大多数操作系统对环境变量的命名施加了严格的规则。例如,Linux shell 变量只能包含字母(a 到 z 或 A 到 Z)、数字(0 到 9)或下划线字符(_)。按照惯例,Unix shell 变量的名称通常使用大写字母(UPPERCASE)。
Spring Boot 的宽松绑定规则尽可能地设计为与这些命名限制兼容。
要将规范形式(canonical-form)的属性名转换为环境变量名,可以遵循以下规则:
-
将点号(
.)替换为下划线(_)。 -
移除所有连字符(
-)。 -
转换为大写。
例如,配置属性 spring.main.log-startup-info 对应的环境变量名为 SPRING_MAIN_LOGSTARTUPINFO。
在绑定到对象列表时,也可以使用环境变量。要绑定到 List,变量名中应使用下划线将元素编号括起来。
例如,配置属性 my.service[0].other 会使用名为 MY_SERVICE_0_OTHER 的环境变量。
对从环境变量进行绑定的支持会应用到 systemEnvironment 属性源,以及任何名称以 -systemEnvironment 结尾的额外属性源。
从环境变量绑定 Map
当 Spring Boot 将环境变量绑定到属性类时,它会在绑定之前将环境变量名称转换为小写。大多数情况下,这一细节并不重要,除非是绑定到 Map 属性时。
Map 中的键始终为小写,如下例所示:
- Java
- Kotlin
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my.props")
public class MyMapsProperties {
private final Map<String, String> values = new HashMap<>();
public Map<String, String> getValues() {
return this.values;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("my.props")
class MyMapsProperties {
val values: Map<String, String> = HashMap()
}
当设置 MY_PROPS_VALUES_KEY=value 时,values Map 包含一个 {"key"="value"} 条目。
只有环境变量的 name 会被转换为小写,而值不会。当设置 MY_PROPS_VALUES_KEY=VALUE 时,values Map 中会包含一个 {"key"="VALUE"} 的条目。
Caching
宽松绑定(Relaxed binding)使用缓存来提升性能。默认情况下,此缓存仅应用于不可变的属性源(immutable property sources)。若要自定义此行为,例如为可变的属性源(mutable property sources)启用缓存,请使用 ConfigurationPropertyCaching。
合并复杂类型
当列表在多个位置配置时,覆盖操作会通过替换整个列表来生效。
例如,假设有一个 MyPojo 对象,其 name 和 description 属性默认为 null。以下示例展示了如何从 MyProperties 中暴露一个 MyPojo 对象列表:
- Java
- Kotlin
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my")
public class MyProperties {
private final List<MyPojo> list = new ArrayList<>();
public List<MyPojo> getList() {
return this.list;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("my")
class MyProperties {
val list: List<MyPojo> = ArrayList()
}
考虑以下配置:
- Properties
- YAML
my.list[0].name=my name
my.list[0].description=my description
#---
spring.config.activate.on-profile=dev
my.list[0].name=my another name
my:
list:
- name: "my name"
description: "my description"
---
spring:
config:
activate:
on-profile: "dev"
my:
list:
- name: "my another name"
如果 dev 配置文件未激活,MyProperties.list 包含一个之前定义的 MyPojo 条目。然而,如果启用了 dev 配置文件,该 list 仍然 只包含一个条目(其名称为 my another name,描述为 null)。此配置不会向列表中添加第二个 MyPojo 实例,也不会合并条目。
当一个 List 在多个配置文件中被指定时,仅使用优先级最高的那个(且仅此一个)。考虑以下示例:
- Properties
- YAML
my.list[0].name=my name
my.list[0].description=my description
my.list[1].name=another name
my.list[1].description=another description
#---
spring.config.activate.on-profile=dev
my.list[0].name=my another name
my:
list:
- name: "my name"
description: "my description"
- name: "another name"
description: "another description"
---
spring:
config:
activate:
on-profile: "dev"
my:
list:
- name: "my another name"
在前面的示例中,如果 dev 配置文件处于激活状态,则 MyProperties.list 包含 一个 MyPojo 条目(其 name 为 my another name,description 为 null)。对于 YAML,既可以使用逗号分隔的列表,也可以使用 YAML 列表来完全覆盖列表的内容。
对于 Map 类型的属性,你可以从多个来源绑定属性值。然而,当多个来源中存在相同的属性时,将使用优先级最高的那个。以下示例展示了如何从 MyProperties 中暴露一个 Map<String, MyPojo>:
- Java
- Kotlin
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("my")
public class MyProperties {
private final Map<String, MyPojo> map = new LinkedHashMap<>();
public Map<String, MyPojo> getMap() {
return this.map;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties("my")
class MyProperties {
val map: Map<String, MyPojo> = LinkedHashMap()
}
考虑以下配置:
- Properties
- YAML
my.map.key1.name=my name 1
my.map.key1.description=my description 1
#---
spring.config.activate.on-profile=dev
my.map.key1.name=dev name 1
my.map.key2.name=dev name 2
my.map.key2.description=dev description 2
my:
map:
key1:
name: "my name 1"
description: "my description 1"
---
spring:
config:
activate:
on-profile: "dev"
my:
map:
key1:
name: "dev name 1"
key2:
name: "dev name 2"
description: "dev description 2"
如果 dev 配置文件未激活,则 MyProperties.map 包含一个键为 key1 的条目(其名称为 my name 1,描述为 my description 1)。然而,如果启用了 dev 配置文件,则 map 将包含两个条目:键为 key1(名称为 dev name 1,描述为 my description 1)和键为 key2(名称为 dev name 2,描述为 dev description 2)。
上述合并规则适用于所有属性源(property sources)中的属性,而不仅限于文件。
属性转换
Spring Boot 在将外部应用属性绑定到 @ConfigurationProperties Bean 时,会尝试将这些属性强制转换为正确的类型。如果你需要自定义类型转换,可以提供一个 ConversionService Bean(Bean 名为 conversionService),或者自定义属性编辑器(通过 CustomEditorConfigurer Bean),或者自定义转换器(使用 @ConfigurationPropertiesBinding 注解的 Bean 定义)。
用于属性转换的 Bean 在应用程序生命周期的非常早期就会被请求,因此请确保限制你的 ConversionService 所使用的依赖项。通常,你所需的任何依赖项在创建时可能尚未完全初始化。
如果你的自定义 ConversionService 不用于配置键(configuration keys)的强制转换,而仅依赖于使用 @ConfigurationPropertiesBinding 限定的自定义转换器,那么你可能希望重命名该 ConversionService。当使用 @ConfigurationPropertiesBinding 注解限定一个 @Bean 方法时,该方法应声明为 static,以避免出现 “bean is not eligible for getting processed by all BeanPostProcessors” 警告。
转换 Durations
Spring Boot 对表达时间间隔(Duration)提供了专门的支持。如果你暴露了一个 Duration 类型的属性,那么在 application.properties 中可以使用以下格式:
-
一个常规的
long表示形式(默认使用毫秒作为单位,除非指定了 @DurationUnit) -
Duration 所使用的标准 ISO-8601 格式
-
一种更易读的格式,其中值和单位是耦合的(
10s表示 10 秒)
考虑以下示例:
- Java
- Kotlin
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
@ConfigurationProperties("my")
public class MyProperties {
@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30);
private Duration readTimeout = Duration.ofMillis(1000);
// getters / setters...
public Duration getSessionTimeout() {
return this.sessionTimeout;
}
public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public Duration getReadTimeout() {
return this.readTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.convert.DurationUnit
import java.time.Duration
import java.time.temporal.ChronoUnit
@ConfigurationProperties("my")
class MyProperties {
@DurationUnit(ChronoUnit.SECONDS)
var sessionTimeout = Duration.ofSeconds(30)
var readTimeout = Duration.ofMillis(1000)
}
要指定会话超时为 30 秒,30、PT30S 和 30s 是等效的。读取超时为 500 毫秒可以用以下任意形式指定:500、PT0.5S 和 500ms。
你也可以使用任意一种支持的单位。这些单位包括:
-
ns表示纳秒 -
us表示微秒 -
ms表示毫秒 -
s表示秒 -
m表示分钟 -
h表示小时 -
d表示天
默认单位是毫秒,可以使用 @DurationUnit 进行覆盖,如上面的示例所示。
如果你更倾向于使用构造函数绑定,也可以暴露相同的属性,如下例所示:
- Java
- Kotlin
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;
@ConfigurationProperties("my")
public class MyProperties {
// fields...
private final Duration sessionTimeout;
private final Duration readTimeout;
public MyProperties(@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("30s") Duration sessionTimeout,
@DefaultValue("1000ms") Duration readTimeout) {
this.sessionTimeout = sessionTimeout;
this.readTimeout = readTimeout;
}
// getters...
public Duration getSessionTimeout() {
return this.sessionTimeout;
}
public Duration getReadTimeout() {
return this.readTimeout;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.DefaultValue
import org.springframework.boot.convert.DurationUnit
import java.time.Duration
import java.time.temporal.ChronoUnit
@ConfigurationProperties("my")
class MyProperties(@param:DurationUnit(ChronoUnit.SECONDS) @param:DefaultValue("30s") val sessionTimeout: Duration,
@param:DefaultValue("1000ms") val readTimeout: Duration)
如果你正在升级一个 Long 类型的属性,请确保在单位不是毫秒时定义其单位(使用 @DurationUnit)。这样做可以提供透明的升级路径,同时支持更丰富的格式。
转换 Periods
除了持续时间(durations)外,Spring Boot 还可以与 Period 类型配合使用。以下格式可用于 application.properties 文件中:
-
一个常规的
int表示(默认使用天数作为单位,除非指定了 @PeriodUnit) -
Period 所使用的标准 ISO-8601 格式
-
一种更简单的格式,其中值和单位成对组合(
1y3d表示 1 年 3 天)
以下单位支持简单格式:
-
y表示年 -
m表示月 -
w表示周 -
d表示天
Period 类型实际上从不存储周数,它只是一个表示“7 天”的快捷方式。
转换数据大小
-
一个常规的
long表示形式(默认使用字节作为单位,除非指定了 @DataSizeUnit) -
一种更易读的格式,其中数值和单位是耦合的(例如
10MB表示 10 兆字节)
考虑以下示例:
- Java
- Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
@ConfigurationProperties("my")
public class MyProperties {
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize bufferSize = DataSize.ofMegabytes(2);
private DataSize sizeThreshold = DataSize.ofBytes(512);
// getters/setters...
public DataSize getBufferSize() {
return this.bufferSize;
}
public void setBufferSize(DataSize bufferSize) {
this.bufferSize = bufferSize;
}
public DataSize getSizeThreshold() {
return this.sizeThreshold;
}
public void setSizeThreshold(DataSize sizeThreshold) {
this.sizeThreshold = sizeThreshold;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.convert.DataSizeUnit
import org.springframework.util.unit.DataSize
import org.springframework.util.unit.DataUnit
@ConfigurationProperties("my")
class MyProperties {
@DataSizeUnit(DataUnit.MEGABYTES)
var bufferSize = DataSize.ofMegabytes(2)
var sizeThreshold = DataSize.ofBytes(512)
}
要指定 10 兆字节的缓冲区大小,10 和 10MB 是等效的。256 字节的大小阈值可以指定为 256 或 256B。
你也可以使用任意受支持的单位。这些单位包括:
-
B表示字节 -
KB表示千字节 -
MB表示兆字节 -
GB表示吉字节 -
TB表示太字节
默认单位是字节,可以使用 @DataSizeUnit 进行覆盖,如上面的示例所示。
如果你更倾向于使用构造函数绑定,也可以暴露相同的属性,如下例所示:
- Java
- Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
@ConfigurationProperties("my")
public class MyProperties {
// fields...
private final DataSize bufferSize;
private final DataSize sizeThreshold;
public MyProperties(@DataSizeUnit(DataUnit.MEGABYTES) @DefaultValue("2MB") DataSize bufferSize,
@DefaultValue("512B") DataSize sizeThreshold) {
this.bufferSize = bufferSize;
this.sizeThreshold = sizeThreshold;
}
// getters...
public DataSize getBufferSize() {
return this.bufferSize;
}
public DataSize getSizeThreshold() {
return this.sizeThreshold;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.DefaultValue
import org.springframework.boot.convert.DataSizeUnit
import org.springframework.util.unit.DataSize
import org.springframework.util.unit.DataUnit
@ConfigurationProperties("my")
class MyProperties(@param:DataSizeUnit(DataUnit.MEGABYTES) @param:DefaultValue("2MB") val bufferSize: DataSize,
@param:DefaultValue("512B") val sizeThreshold: DataSize)
如果你正在升级一个 Long 类型的属性,请确保在单位不是字节(bytes)时,使用 @DataSizeUnit 明确定义单位。这样做可以提供透明的升级路径,同时支持更丰富的格式。
转换 Base64 数据
Spring Boot 支持解析经过 Base64 编码的二进制数据。如果你暴露了一个 Resource 属性,可以使用带有 base64: 前缀的 Base64 编码文本作为其值,如下例所示:
- Properties
- YAML
my.property=base64:SGVsbG8gV29ybGQ=
my:
property: base64:SGVsbG8gV29ybGQ=
Resource 属性也可用于提供资源的路径,使其更加灵活。
@ConfigurationProperties 验证
Spring Boot 会在 @ConfigurationProperties 类被标注了 Spring 的 @Validated 注解时,尝试对其进行验证。你可以直接在配置类上使用 JSR-303 jakarta.validation 约束注解。为此,请确保你的 classpath 中包含一个符合 JSR-303 规范的实现,然后在字段上添加约束注解,如下例所示:
- Java
- Kotlin
import java.net.InetAddress;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@ConfigurationProperties("my.service")
@Validated
public class MyProperties {
@NotNull
private InetAddress remoteAddress;
// getters/setters...
public InetAddress getRemoteAddress() {
return this.remoteAddress;
}
public void setRemoteAddress(InetAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
}
import jakarta.validation.constraints.NotNull
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated
import java.net.InetAddress
@ConfigurationProperties("my.service")
@Validated
class MyProperties {
var remoteAddress: @NotNull InetAddress? = null
}
你也可以通过在创建配置属性的 @Bean 方法上添加 @Validated 注解来触发验证。
要将验证级联到嵌套属性,关联的字段必须使用 @Valid 进行注解。以下示例基于前面的 MyProperties 示例:
- Java
- Kotlin
import java.net.InetAddress;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@ConfigurationProperties("my.service")
@Validated
public class MyProperties {
@NotNull
private InetAddress remoteAddress;
@Valid
private final Security security = new Security();
// getters/setters...
public InetAddress getRemoteAddress() {
return this.remoteAddress;
}
public void setRemoteAddress(InetAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
public Security getSecurity() {
return this.security;
}
public static class Security {
@NotEmpty
private String username;
// getters/setters...
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
}
}
import jakarta.validation.Valid
import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.NotNull
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated
import java.net.InetAddress
@ConfigurationProperties("my.service")
@Validated
class MyProperties {
var remoteAddress: @NotNull InetAddress? = null
@Valid
val security = Security()
class Security {
@NotEmpty
var username: String? = null
}
}
你也可以通过创建一个名为 configurationPropertiesValidator 的 bean 定义来添加自定义的 Spring Validator。该 @Bean 方法应声明为 static。配置属性验证器在应用程序生命周期的非常早期就被创建,将 @Bean 方法声明为静态(static)可以避免实例化 @Configuration 类,从而防止因过早实例化可能引发的问题。
spring-boot-actuator 模块包含一个端点,用于暴露所有的 @ConfigurationProperties Bean。你可以在浏览器中访问 /actuator/configprops,或使用等效的 JMX 端点。详情请参见 生产就绪功能 章节。
@ConfigurationProperties 与 @Value
[@Value](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/beans/factory/annotation/Value.html) 注解是 Spring 核心容器的一项功能,它并不提供与类型安全的配置属性(type-safe configuration properties)相同的功能。下表总结了 [@ConfigurationProperties](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/context/properties/ConfigurationProperties.html) 和 [@Value](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/beans/factory/annotation/Value.html) 所支持的特性:
| 特性 | @ConfigurationProperties | @Value |
|---|---|---|
| 宽松绑定(Relaxed binding) | 是 | 有限(参见下方注释) |
| 元数据支持(Meta-data support) | 是 | 否 |
SpEL 表达式求值 | 否 | 是 |
如果你确实想使用 @Value,我们建议你使用属性名的规范形式(即仅使用小写字母的 kebab-case 形式)进行引用。这样 Spring Boot 就可以使用与 宽松绑定(relaxed binding) @ConfigurationProperties 相同的逻辑。
例如,@Value("${demo.item-price}") 会从 application.properties 文件中匹配 demo.item-price 和 demo.itemPrice 形式,以及从系统环境变量中匹配 DEMO_ITEMPRICE。但如果你使用的是 @Value("${demo.itemPrice}"),则 demo.item-price 和 DEMO_ITEMPRICE 将不会被考虑。
如果你为自己的组件定义了一组配置键,我们建议你将它们组织在一个使用 @ConfigurationProperties 注解的 POJO 中。这样做将为你提供一个结构化、类型安全的对象,你可以将其注入到自己的 bean 中。
来自 application property files 的 SpEL 表达式在解析这些文件并填充环境时不会被处理。然而,可以在 @Value 中编写 SpEL 表达式。如果 application property 文件中的某个属性值是一个 SpEL 表达式,那么当通过 @Value 使用该属性时,该表达式会被求值。