类型安全配置属性

使用@Value("${property}")注释注入配置属性有时会很麻烦,尤其是在使用多个属性或数据本质上是分层的情况下。Spring Boot提供了一种使用属性的替代方法,该方法允许强类型bean管理和验证应用程序的配置,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.example;

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("acme")
public class AcmeProperties {

private boolean enabled;

private InetAddress remoteAddress;

private final Security security = new Security();

public boolean isEnabled() { ... }

public void setEnabled(boolean enabled) { ... }

public InetAddress getRemoteAddress() { ... }

public void setRemoteAddress(InetAddress remoteAddress) { ... }

public Security getSecurity() { ... }

public static class Security {

private String username;

private String password;

private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

public String getUsername() { ... }

public void setUsername(String username) { ... }

public String getPassword() { ... }

public void setPassword(String password) { ... }

public List<String> getRoles() { ... }

public void setRoles(List<String> roles) { ... }

}
}

前面的POJO定义了以下属性:

  • acme.enabled,默认值为false
  • acme.remote-address,具有可以强制的类型String
  • acme.security.username,使用嵌套的“安全”对象,其名称由属性名称决定。特别是,那里根本没有使用返回类型SecurityProperties
  • acme.security.password
  • acme.security.roles,收集String

getter和setter通常是必需的,因为绑定是通过标准的Java Beans属性描述符,就像在Spring MVC中一样。在下列情况下可以省略setter:

  • 映射,只要它们被初始化,就需要一个getter但不一定是setter,因为它们可以被绑定器变异。
  • 可以通过索引(通常使用YAML)或使用单个逗号分隔值(属性)访问集合和数组。在后一种情况下,必须设置一个setter。我们建议始终为这些类型添加setter。如果初始化集合,请确保它不是不可变的(如上例所示)。
  • 如果初始化嵌套的POJO属性(如Security前面示例中的字段),则不需要setter。如果您希望绑定器使用其默认构造函数动态创建实例,则需要一个setter。

有些人使用Project Lombok自动添加getter和setter。确保Lombok不为此类型生成任何特定构造函数,因为容器会自动使用它来实例化对象。

最后,仅考虑标准Java Bean属性,并且不支持对静态属性的绑定。

您还需要列出要在@EnableConfigurationProperties注释中注册的属性类 ,如以下示例所示:

1
2
3
4
@Configuration 
@EnableConfigurationProperties(AcmeProperties.class)
public class MyConfiguration {
}

@ConfigurationPropertiesbean以这种方式注册时,bean具有常规名称:<prefix>-<fqn>,其中<prefix>@ConfigurationProperties注释中指定的环境键前缀,并且<fqn>是bean的完全限定名称。如果注释未提供任何前缀,则仅使用bean的完全限定名称。

上例中的bean名称是acme-com.example.AcmeProperties

即使前面的配置创建了常规bean AcmeProperties,我们也建议@ConfigurationProperties只处理环境,特别是不从上下文中注入其他bean。话虽如此,@EnableConfigurationProperties注释也会自动应用于您的项目,以便从中配置任何注释的现有 bean 。您可以通过确保 已经是bean 来快捷方式,如以下示例所示:@ConfigurationProperties

Environment

MyConfiguration

AcmeProperties

1
2
3
4
5
@Component 
@ConfigurationProperties(prefix =“acme”)
public class AcmeProperties {
// ...参见前面的例子
}

这种配置风格特别适用于SpringApplication外部YAML配置,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
# application.yml

acme:
remote-address: 192.168.1.1
security:
username: admin
roles:
- USER
- ADMIN

# additional configuration as required

要使用@ConfigurationPropertiesbean,可以使用与任何其他bean相同的方式注入它们,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class MyService {

private final AcmeProperties properties;

@Autowired
public MyService(AcmeProperties properties) {
this.properties = properties;
}

//...

@PostConstruct
public void openConnection() {
Server server = new Server(this.properties.getRemoteAddress());
// ...
}

}

使用@ConfigurationProperties还可以生成元数据文件,IDE可以使用这些文件为您自己的密钥提供自动完成功能。有关详细信息请参阅 附录B,配置元数据附录。

第三方配置

除了@ConfigurationProperties用于注释类之外,您还可以在公共@Bean方法上使用它。当您想要将属性绑定到控件之外的第三方组件时,这样做会特别有用。

要从Environment属性配置bean ,请添加@ConfigurationProperties到其bean注册,如以下示例所示:

1
2
3
4
5
@ConfigurationProperties(prefix =“another”)
@Bean
public AnotherComponent anotherComponent(){
...
}

使用another前缀定义的任何属性都AnotherComponent以与前面AcmeProperties示例类似的方式映射到该bean 。

松弛结合

Spring Boot使用一些宽松的规则来绑定bean的Environment属性 @ConfigurationProperties,因此不需要在Environment属性名和bean属性名之间进行精确匹配 。这有用的常见示例包括破折号分隔的环境属性(例如,context-path绑定到contextPath)和大写环境属性(例如,PORT绑定到 port)。

例如,请考虑以下@ConfigurationProperties类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ConfigurationProperties(prefix="acme.my-project.person")
public class OwnerProperties {

private String firstName;

public String getFirstName() {
return this.firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

}

在前面的示例中,可以使用以下属性名称:

属性 注意
acme.my-project.person.first-name 建议用于.properties.yml文件。
acme.myProject.person.firstName 标准的驼峰案例语法。
acme.my_project.person.first_name 下划线符号,这是在用另一种格式.properties.yml 文件。
ACME_MYPROJECT_PERSON_FIRSTNAME 大写格式,使用系统环境变量时建议使用。

prefix注释的值必须是kebab大小写(小写并且用-,例如acme.my-project.person)。

每个属性源放宽绑定规则:

Property Source Simple List
属性文件 骆驼案,kebab案例或下划线表示法 标准列表语法使用[ ]或逗号分隔值
YAML文件 骆驼案,kebab案例或下划线表示法 标准YAML列表语法或逗号分隔值
环境变量 大写格式,下划线作为分隔符。_不应在属性名称中使用 由下划线包围的数字值,例如 MY_ACME_1_OTHER = my.acme[1].other
系统属性 骆驼案,kebab案例或下划线表示法 标准列表语法使用[ ]或逗号分隔值

我们建议,在可能的情况下,属性以小写的kebab格式存储,例如my.property-name=acme

绑定到Map属性时,如果key包含除小写字母数字字符以外的任何内容,则-需要使用括号表示法以保留原始值。如果密钥未被包围[],则任何非字母数字或-删除的字符。例如,考虑将以下属性绑定到Map

1
2
3
4
5
acme:
map:
"[/key1]": value1
"[/key2]": value2
/key3: value3

上面的属性将绑定到Mapwith /key1/key2key3作为地图中的键。

合并复杂类型

当列表在多个位置配置时,覆盖通过替换整个列表来工作。

例如,假设一个MyPojo具有对象namedescription那些属性 null默认。以下示例公开了以下MyPojo对象 列表AcmeProperties

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties("acme")
public class AcmeProperties {

private final List<MyPojo> list = new ArrayList<>();

public List<MyPojo> getList() {
return this.list;
}

}

请考虑以下配置:

1
2
3
4
5
6
7
8
9
10
acme:
list:
- name: my name
description: my description
---
spring:
profiles: dev
acme:
list:
- name: my another name

如果dev配置文件未激活,则AcmeProperties.list包含一个MyPojo条目,如先前定义的那样。dev但是,如果启用了配置文件,则list 仍然 只包含一个条目(名称my another name和描述null)。此配置不会MyPojo向列表添加第二个实例,也不会合并项目。

List在多个配置文件中指定a时,将使用具有最高优先级(并且仅具有该优先级)的配置文件。请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
acme:
list:
- name: my name
description: my description
- name: another name
description: another description
---
spring:
profiles: dev
acme:
list:
- name: my another name

在前面的示例中,如果dev配置文件处于活动状态,则AcmeProperties.list包含 一个 MyPojo条目(名称my another name和描述null)。对于YAML,逗号分隔列表和YAML列表都可用于完全覆盖列表的内容。

对于Map属性,您可以绑定从多个源中提取的属性值。但是,对于多个源中的相同属性,使用具有最高优先级的属性。以下示例公开了一个Map<String, MyPojo>from AcmeProperties

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties("acme")
public class AcmeProperties {

private final Map<String, MyPojo> map = new HashMap<>();

public Map<String, MyPojo> getMap() {
return this.map;
}

}

请考虑以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
acme:
map:
key1:
name: my name 1
description: my description 1
---
spring:
profiles: dev
acme:
map:
key1:
name: dev name 1
key2:
name: dev name 2
description: dev description 2

如果dev配置文件未处于活动状态,则AcmeProperties.map包含一个带密钥的条目key1 (名称my name 1和描述my description 1)。dev但是,如果启用了配置文件,则map包含两个带有键的条目key1 (名称dev name 1和描述my description 1)和 key2(名称dev name 2和描述dev description 2)。

前面的合并规则适用于所有属性源的属性,而不仅仅适用于YAML文件。

属性转换

Spring Boot尝试在绑定到@ConfigurationPropertiesbean 时将外部应用程序属性强制转换为正确的类型。如果需要自定义类型转换,则可以提供ConversionServicebean(带有bean命名conversionService)或自定义属性编辑器(通过CustomEditorConfigurerbean)或自定义Converters(带有注释为bean的bean定义@ConfigurationPropertiesBinding)。

由于在应用程序生命周期中很早就请求此bean,因此请确保限制您ConversionService正在使用的依赖项。通常,您在创建时可能无法完全初始化所需的任何依赖项。ConversionService如果配置密钥强制不需要,您可能需要重命名自定义,并且只依赖于合格的自定义转换器@ConfigurationPropertiesBinding

转换持续时间

Spring Boot专门支持表达持续时间。如果公开 java.time.Duration属性,则可以使用应用程序属性中的以下格式:

  • 常规long表示(除非@DurationUnit已指定,否则使用毫秒作为默认单位 )
  • 标准的ISO-8601格式 java.util.Duration的使用
  • 一种更易读的格式,其中值和单位耦合(例如10s10秒)

请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@ConfigurationProperties("app.system")
public class AppSystemProperties {

@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30);

private Duration readTimeout = Duration.ofMillis(1000);
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;
}

}

要指定30秒的会话超时30PT30S并且30s都是等效的。的500ms的读超时可以以任何形式如下指定:500PT0.5S500ms

您也可以使用任何支持的单位。这些是:

  • ns 为纳秒
  • us 微秒
  • ms 毫秒
  • s 几秒钟
  • m 几分钟
  • h 用了几个小时
  • d 持续数天

默认单位是毫秒,可以使用@DurationUnit上面的示例中所示进行覆盖。

如果要从仅Long用于表示持续时间的先前版本升级,请确保定义单位(使用@DurationUnit),如果它不是交换机旁边的毫秒数Duration。这样做可以提供透明的升级路径,同时支持更丰富的格式。

转换数据大小

Spring Framework有一个DataSize值类型,允许以字节为单位表示大小。如果公开DataSize属性,则可以使用应用程序属性中的以下格式:

  • 常规long表示(使用字节作为默认单位,除非 @DataSizeUnit已指定)
  • 更可读的格式,其中值和单元耦合(例如,10MB意味着10兆字节)

请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ConfigurationProperties("app.io")
public class AppIoProperties {

@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize bufferSize = DataSize.ofMegabytes(2);

private DataSize sizeThreshold = DataSize.ofBytes(512);

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;
}

}

指定10兆字节的缓冲区大小,10并且10MB是等效的。可以将大小阈值256个字节指定为256256B

您也可以使用任何支持的单位。这些是:

  • B 用于字节
  • KB 为千字节
  • MB 对于兆字节
  • GB 为千兆字节
  • TB 对于太字节

默认单位是字节,可以使用@DataSizeUnit上面的示例中所示进行覆盖。

如果要从仅Long用于表示大小的先前版本升级,请确保定义单位(使用@DataSizeUnit),如果它不是交换机旁边的字节DataSize。这样做可以提供透明的升级路径,同时支持更丰富的格式。

@ConfigurationProperties验证

Spring Boot尝试在@ConfigurationProperties使用Spring的@Validated注释注释时验证类。您可以javax.validation 直接在配置类上使用JSR-303 约束注释。为此,请确保符合条件的JSR-303实现位于类路径上,然后将约束注释添加到字段中,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {

@NotNull
private InetAddress remoteAddress;

// ... getters and setters

}

您还可以通过注释@Bean创建配置属性的方法来触发验证@Validated

虽然嵌套属性也会在绑定时进行验证,但最好还是将关联字段注释为@Valid。这确保即使没有找到嵌套属性也会触发验证。以下示例基于前面的AcmeProperties示例构建 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {

@NotNull
private InetAddress remoteAddress;

@Valid
private final Security security = new Security();

// ... getters and setters

public static class Security {

@NotEmpty
public String username;

// ... getters and setters

}

}

您还可以Validator通过创建名为的bean定义 来添加自定义Spring configurationPropertiesValidator@Bean应声明该方法static。配置属性验证器是在应用程序生命周期的早期创建的,并且将该@Bean方法声明为static可以创建bean而无需实例化@Configuration该类。这样做可以避免早期实例化可能导致的任何问题。有一个 属性验证示例,显示如何设置。

spring-boot-actuator模块包括一个暴露所有@ConfigurationPropertiesbean 的端点 。将Web浏览器指向 /actuator/configprops或使用等效的JMX端点。有关详细信息,请参阅“ 生产就绪功能 ”部分。

@ConfigurationProperties与@Value

@Value的注释是核心容器的功能,和它不提供相同的功能,类型安全配置属性。下表汇总了支持的功能@ConfigurationProperties@Value

特征 @ConfigurationProperties @Value
轻松绑定 没有
元数据支持 没有
SpEL 评测 没有

如果为自己的组件定义一组配置键,我们建议您将它们分组到带注释的POJO中@ConfigurationProperties。您还应该知道,因为@Value不支持宽松绑定,所以如果您需要使用环境变量来提供值,则它不是一个好的候选者。

最后,虽然您可以编写SpEL表达式@Value,但不会从应用程序属性文件中处理此类表达式。