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.ymlacme:remote-address: 192.168.1.1security:username: adminroles:- USER- ADMIN# additional configuration as required
为配合使用@ConfigurationProperties Bean实例,您可以与处理其他的Bean方式注入配置Bean实例,如以下示例所示:
@Servicepublic class MyService {private final AcmeProperties properties;@Autowiredpublic MyService(AcmeProperties properties) {this.properties = properties;}@PostConstructpublic 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")@Beanpublic 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 namedescription: my description---spring:profiles: devacme: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 namedescription: my description- name: another namedescription: another description---spring:profiles: devacme: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 1description: my description 1---spring:profiles: devacme:map:key1:name: dev name 1key2:name: dev name 2description: 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")@Validatedpublic class AcmeProperties {@NotNullprivate InetAddress remoteAddress;// ... getters and setters}
Tip
你也可以在创建配置属性的@Bean方法上标准
@Validated注解来触发验证.
尽管嵌套的属性也将在绑定时验证,但在关联的字段上标准@Valid注解将是良好的实践.这将在即使没有嵌套属性时也会确保触发验证.以下示例基于前面的AcmeProperties示例:
@ConfigurationProperties(prefix="acme")@Validatedpublic class AcmeProperties {@NotNullprivate InetAddress remoteAddress;@Validprivate final Security security = new Security();// ... getters and setterspublic static class Security {@NotEmptypublic 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表达式,应用属性文件将不会处理这些表达式.
