24.7 Type-safe Configuration Properties
使用@Value("${property}")
注解注入配置属性有时会很麻烦,特别是如果你使用多个属性或数据本质上是分层的.Spring Boot为使用属性提供了替换方法,让属性能让强类型的实例配置管理及检验应用程序配置,如以下示例所示:
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
,内部类security属性,值由属性名指定.在这里根本没使用返回类型,可能是SecurityProperties
.acme.security.password
.acme.security.roles
,字符串集合.
Note
getter和setter通常是强制性的,因为绑定是通过标准的Java Beans属性描述符,就像在Spring MVC一样.在以下情况下可以省略setter:
- Maps只要初始化,需要一个getter但不一定需要setter,因为他们可被binder自动注入.
- 集合和数组可以通过索引访问(通常使用YAML)或通过使用一个逗号分隔值(属性).在后一种情况下,setter是必需的.我们建议总是为这种类型添加setter.如果你初始化一个集合,确保它是可变的(如前面例子).
- 如果嵌套POJO属性初始化(如前面例子中的
Security
字段),setter不是必需的.如果你想要通过使用其默认构造函数动态绑定创建实例,你需要一个setter.有些人使用Lombok项目自动添加getter和setter.确保Lombok对于此类型不产生任何特定的构造函数,因为它是使用容器自动实例化对象.
最后,只有标准的Java Bean属性会被考虑并且不支持静态属性绑定.Tip
请一并查看
@Value
和@ConfigurationProperties
注解的区别.
你还需要通过@EnableConfigurationProperties
注解注册导入属性类,如以下示例所示:
@Configuration
@EnableConfigurationProperties(AcmeProperties.class)
public class MyConfiguration {
}
Note
当使用上述方法注册
@ConfigurationProperties
实例时,通常实例有一个约定的名称:<prefix>-<fqn>
,<prefix>
是在@ConfigurationProperties
注解中指定的环境键值前缀且<fqn>
是实例的完全限定名称. 如果注解不提供任何前缀,那么只有完全限定名的实例被使用.
在上面的示例中实例名为acme-com.example.AcmeProperties
.
即使前面的配置为AcmeProperties
创建一个常规的Bean实例,我们还是建议@ConfigurationProperties
只处理环境属性,特别是别注入其他上下文的Bean实例.
前面已经说过,@EnableConfigurationProperties
注解会自动应用到您的项目,以便于任何标注@ConfigurationProperties注解的Bean实例都会根据环境属性配置.
你可以通过如下代码精简MyConfiguration
确保AcmeProperties
已经是一个Bean实例:
@Component
@ConfigurationProperties(prefix="acme")
public class AcmeProperties {
// ... see the preceding example
}
这种风格的配置在SpringApplication使用外部YAML配置时能很好的工作,如以下示例所示:
# application.yml
acme:
remote-address: 192.168.1.1
security:
username: admin
roles:
- USER
- ADMIN
# additional configuration as required
为配合使用@ConfigurationProperties Bean实例,您可以与处理其他的Bean方式注入配置Bean实例,如以下示例所示:
@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());
// ...
}
}
Tip
使用
@ConfigurationProperties
还允许您生成元数据文件,可以用来在IDE中为你自己的键提供自动完成提供自动完成.参见Appendix B Configuration Metadata获取详细信息. .Thir-party Configuration
除了使用@ConfigurationProperties注解标注类外,您还可以在公共的@Bean方法使用.这样做将特别有用,当你想绑定属性至你无法控制的第三方组件.
从环境属性中配置一个Bean实例,只需在他的Bean注册方法上添加@ConfigurationProperties
注解,如以下示例所示:
@ConfigurationProperties(prefix = "another")
@Bean
public AnotherComponent anotherComponent() {
...
}
任何以前缀another
开头的属性都将被映射到AnotherComponent
Bean实例,就像前面AcmeProperties
例子的方式.
Relaxed Binding
Spring Boot使用一些宽松的规则绑定环境中的属性至@ConfigurationProperties
Bean实例,因此在绑定时不需要精确匹配环境属性名和Bean实例的属性名.
这里描述了几个非常有用的常见示例包.分割的环境属性(例如,context-path
绑定到contextPath
)和大小写配置环境属性(例如,PORT
绑定到port
)
例如,考虑下面的@ConfigurationProperties
类:
@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;
}
}
在前面的示例中,可以使用以下属性名:
Table 24.1 relaxed binding
Property | Note |
---|---|
acme.my-project.person.first-name | 短横线隔开,强烈要求配置在.properties和.yml文件中 |
acme.myProject.person.firstName | 标准驼峰写法. |
acme.my_project.person.first_name | .properties文件中的另一种写法 |
ACME_MYPROJECT_PERSON_FIRSTNAME | 建议在系统环境变量中使用. |
Note
注解的prefix前缀值必须短横线隔开(需小写和使用-隔开,如a
cme.my-project.person
)
Table 24.2 relaxed binding rules per property source
| Property Source | Simple | List | | —————————— | ——————————————————— | 标准列表语法使用[]或者逗号分割值 | | Properties Files | 驼峰式大小写,短横线隔开,下划线标注 | 标准YAML列表语法使用[]或者逗号分割值 | | YAML Files | 驼峰式大小写,短横线隔开,下划线标注 | 数字用下划线包裹 例如MY_ACME_1_OTHER = my.acem[1].other | | Environment Variables | 大写格式使用下划线分割,不使用做为属性名 | 标准列表语法使用[]或者逗号分割值 | | System properties | 驼峰式大小写,短横线隔开,下划线标注
Tip
我们建议,在可能的情况下,属性值存储在小写横短线隔开格式,如
my.property-name=acme
.
Merging Complex Types
当在多个地方配置列表时,通过替换整个列表来覆写.
例如,假设MyPojo
对象存在默认为null
的name
和description
属性.下面的例子在AcmeProperties
类中配置MyPojo
对象列表:
@ConfigurationProperties("acme")
public class AcmeProperties {
private final List<MyPojo> list = new ArrayList<>();
public List<MyPojo> getList() {
return this.list;
}
}
考虑如下配置:
acme:
list:
- name: my name
description: my description
---
spring:
profiles: dev
acme:
list:
- name: my another name
如果dev
特定配置未激活,则AcmeProperties.list
包含一个MyPojo
对象,如之前定义.如果dev
特定配置激活,然而,list
列表仍然只包含一个MyPojo
对象(name
属性值为my another name
且description
属性为null
).
这个配置未添加第二个MyPojo实例至列表,即它并没有合并项目.
当list配置在多个profile文件时,优先级最高的(只有一个)将被使用.考虑下面的例子:
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
对象(name
属性值为my another name
且description
属性为null
).
YAML的逗号分隔列表和YAML列表可以用来完全覆盖列表内容.
对于Map
属性,您可以绑定来自多个源的属性值.然而,对于多个源中的同一属性,使用优先级最高的.下面的例子在AcmeProperties
类中配置Map<String,MyPojo>
:
@ConfigurationProperties("acme")
public class AcmeProperties {
private final Map<String, MyPojo> map = new HashMap<>();
public Map<String, MyPojo> getMap() {
return this.map;
}
}
考虑以下配置:
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
对象(name属性为my name 1
且description属性为my description 1
).
如果dev
特定配置为激活,map
属性将包含两个条目,一个键值为key1
(name属性为my name 1
且description属性为my description 1
)和键值为key2
(name属性为my name 2
且description属性为my description 2
).
Note
前面所述的合并规则适用于来自所有属性源的属性,而不仅仅是YAML文件.
Properties Conversion
当绑定到@ConfigurationProperties
实例属性时,Spring Boot试图强制转换外部应用程序属性为正确类型.如果你需要自定义类型转换,您可以配置一个ConversionService
(Bean名为ConversionService
)或自定义属性编辑器(通过CustomEditorConfigurer
Bean)或自定义Converters
(标注@ConfigurationPropertiesBinding
注解的Bean定义).
Note
这个实例在应用程序生命周期的使用,一定要限制
ConversionService
使用的依赖关系.通常,任何您需要的依赖可能不是完全在创建时初始化. 如果不是必需的强制配置键,你可能需要重命名你的ConversionService
且只有在通过限定@ConfigurationPropertiesBinding注解使用自定义转换器时需要.
Converting durations
Spring Boot对持续时间表示提供了很好的支持.如果您正在使用java.time.Duration属性,以下时间格式在应用程序属性文件中可用:
- 特定的
long
长整型表示(使用毫秒作为默认单位,除非指定了@DurationUnit
). java.util.Duration
所使用的标准的ISO-8601格式.- 可读格式,值和单位表示(例如:
10s
意味着10秒).
考虑下面例子:
@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秒,30
、PT30S
和30s
配置都是等效的.读超时500ms可以使用以下格式:500
,PT0.5S
和500ms
.
您还可以以下支持的单位:
- ns表示为纳秒.
- ms表示为毫秒.
- s表示为秒.
- m表示为分.
- h表示为小时.
- d表示为日.
程序默认的单位为毫秒,可通过@DurationUnit注解覆盖默认值,具体使用说明请参照上面的示例.
Tip
如果你的Spring Boot是由低版本升级而来,程序简单使用Long来表述时间.当切换到Duration时可能默认单位不是毫秒,你需要确保定义单元(使用@DurationUnit注解). 这样做可以提供透明的升级路径便于同时支持更丰富的格式.
@ConfigurationProperties Validation
当配置类标注Spring’s的@Valicated
注解时,Spring Boot会试图验证@ConfigurationProperties
类.您可以直接使用JSR-303 javax.validation
约束注解在您的配置类.
如果这么做,确保符合JSR-303规范实现在您的类路径中,然后在字段上添加约束注解,如以下示例所示:
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
@NotNull
private InetAddress remoteAddress;
// ... getters and setters
}
Tip
你也可以在创建配置属性的@Bean方法上标准
@Validated
注解来触发验证.
尽管嵌套的属性也将在绑定时验证,但在关联的字段上标准@Valid
注解将是良好的实践.这将在即使没有嵌套属性时也会确保触发验证.以下示例基于前面的AcmeProperties
示例:
@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
}
}
你还可以通过创建名为configurationPropertiesValidator的Bean示例来增加自定义的Spring Validator.@Bean方法必须声明为静态的. 配置属性验证器将在应用程序的生命周期早期被创建,且声明@Bean方法为静态能让bean创建无需实例化@Configuration类.这样做可以避免早期实例化可能导致的任何问题.关于如何配置此相关验证请参考此.
Tip
Spring-boot-actuator
模块包含了端点用来显示@ConfigurationProperties
实例.打开你的Web浏览器指向/actuator/configprops
或者使用一致的JMX端点.请参考Production ready feature获取详细信息.
@ConfigurationProperties vs. @Value
@Value注解是核心容器的特性,它不提供类似安全配置属性的功能.下表总结了@ConfigurationProperties和@Value支持的功能特性:
Feature | @ConfigurationProperties | @Value |
---|---|---|
Relaxed binding | Yes | No |
Meta-data support | Yes | No |
SpEL evaluation | No | Yes |
如果你为自己的组件定义一组配置主键,我们建议在你的POJO中将它们分组并标注@ConfigurationProperties
注解.你也应该知道,因为@Value
不支持复杂绑定,如果你需要提供使用环境变量的值,它不是一个好的候选.
最后,您可以在@Value注解中编写SpEL表达式,应用属性文件将不会处理这些表达式.