baeldung 2017-04-24
1. 简介
Spring Boot 提供了许多实用特性,包括外部化配置(externalized configuration)以及轻松访问属性文件中定义的属性。此前的一篇教程已经介绍了实现这一功能的多种方式。
本文将深入探讨 @ConfigurationProperties 注解的使用细节。
2. 环境准备
本教程采用一个相当标准的设置。首先,在 pom.xml 中添加 spring-boot-starter-parent 作为父项目:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.7</version>
<relativePath/>
</parent>
为了能够对属性文件中定义的属性进行校验,我们还需要一个 JSR-380 的实现,而 hibernate-validator 是其中一种,并由 spring-boot-starter-validation 依赖提供。
因此,我们也将其加入 pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.5.7</version>
</dependency>
更多细节可参考 “Hibernate Validator 入门指南” 页面。
3. 简单属性绑定
官方文档建议我们将配置属性隔离到独立的 POJO 类中。
因此,我们先创建这样一个类:
@Configuration
@ConfigurationProperties(prefix = "mail")
public class ConfigProperties {
private String hostName;
private int port;
private String from;
// 标准的 getter 和 setter 方法
}
这里使用了 @Configuration 注解,以便 Spring 在应用上下文中创建该类的 Bean。
@ConfigurationProperties 最适合用于具有相同前缀的层级化属性,因此我们为它设置了前缀 mail。
注意:Spring 框架使用标准的 Java Bean setter 方法,因此我们必须为每个属性声明 setter 方法。
提示:如果我们不在 POJO 上使用
@Configuration,则需要在主 Spring 应用类上添加@EnableConfigurationProperties(ConfigProperties.class)来将属性绑定到 POJO:
@SpringBootApplication
@EnableConfigurationProperties(ConfigProperties.class)
public class EnableConfigurationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(EnableConfigurationDemoApplication.class, args);
}
}
就是这样!Spring 会自动将属性文件中所有以 mail 为前缀、且字段名与 ConfigProperties 类中字段匹配的属性进行绑定。
Spring 在属性绑定时使用了一些宽松的规则(relaxed binding)。因此,以下这些写法都会被绑定到 hostName 属性:
mail.hostNamemail.hostnamemail.host_namemail.host-namemail.HOST_NAME
因此,我们可以使用如下属性文件来设置所有字段:
# 简单属性
mail.hostname=host@mail.com
mail.port=9000
mail.from=mailer@mail.com
3.1 Spring Boot 2.2 及以后版本
从 Spring Boot 2.2 开始,Spring 通过类路径扫描(classpath scanning)自动发现并注册 @ConfigurationProperties 类。但需要显式启用该功能,即添加 @ConfigurationPropertiesScan 注解。
因此,我们不再需要在这些类上使用 @Component(或其元注解如 @Configuration),甚至也不再需要使用 @EnableConfigurationProperties:
@ConfigurationProperties(prefix = "mail")
@ConfigurationPropertiesScan
public class ConfigProperties {
private String hostName;
private int port;
private String from;
// 标准的 getter 和 setter 方法
}
即使没有使用 @Component 注解,由 @SpringBootApplication 启用的类路径扫描器也能找到 ConfigProperties 类。
此外,我们还可以使用 @ConfigurationPropertiesScan 注解指定自定义包路径来扫描配置属性类:
@SpringBootApplication
@ConfigurationPropertiesScan("com.baeldung.configurationproperties")
public class EnableConfigurationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(EnableConfigurationDemoApplication.class, args);
}
}
这样,Spring 就只会扫描 com.baeldung.configurationproperties 包中的配置属性类。
4. 嵌套属性
我们可以在 List、Map 和自定义类中使用嵌套属性。
首先,创建一个 Credentials 类用于嵌套属性:
public class Credentials {
private String authMethod;
private String username;
private String password;
// 标准的 getter 和 setter 方法
}
然后更新 ConfigProperties 类,使其包含 List、Map 和 Credentials 对象:
public class ConfigProperties {
private String hostname;
private int port;
private String from;
private List<String> defaultRecipients;
private Map<String, String> additionalHeaders;
private Credentials credentials;
// 标准的 getter 和 setter 方法
}
对应的属性文件如下:
# 简单属性
mail.hostname=mailer@mail.com
mail.port=9000
mail.from=mailer@mail.com
# List 属性
mail.defaultRecipients[0]=admin@mail.com
mail.defaultRecipients[1]=owner@mail.com
# Map 属性
mail.additionalHeaders.redelivery=true
mail.additionalHeaders.secure=true
# 对象属性
mail.credentials.username=john
mail.credentials.password=password
mail.credentials.authMethod=SHA1
5. 在 @Bean 方法上使用 @ConfigurationProperties
我们也可以将 @ConfigurationProperties 注解应用于带有 @Bean 注解的方法上。
这种方式在我们需要将属性绑定到不受我们控制的第三方组件时特别有用。
首先创建一个简单的 Item 类:
public class Item {
private String name;
private int size;
// 标准的 getter 和 setter 方法
}
然后看看如何在 @Bean 方法上使用 @ConfigurationProperties,将外部属性绑定到 Item 实例:
@Configuration
public class ConfigProperties {
@Bean
@ConfigurationProperties(prefix = "item")
public Item item() {
return new Item();
}
}
这样,所有以 item 为前缀的属性都会被映射到由 Spring 容器管理的 Item 实例上。
6. 属性校验
@ConfigurationProperties 支持使用 JSR-380(Bean Validation 2.0)格式对属性进行校验,这带来了许多便利。
例如,我们可以将 hostName 属性设为必填项:
@NotBlank
private String hostName;
再将 authMethod 属性长度限制为 1 到 4 个字符:
@Length(max = 4, min = 1)
private String authMethod;
将 port 属性值限制在 1025 到 65536 之间:
@Min(1025)
@Max(65536)
private int port;
最后,要求 from 属性必须符合电子邮件格式:
@Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
private String from;
这些校验可以显著减少代码中的 if-else 条件判断,使代码更加简洁清晰。
如果任何一项校验失败,应用程序启动时将抛出 IllegalStateException 并终止。
注意:Hibernate Validator 框架依赖标准的 Java Bean getter 和 setter 方法,因此务必为每个属性声明 getter 和 setter。
7. 属性类型转换
@ConfigurationProperties 支持将属性值转换为多种类型的 Java 对象。
7.1 Duration(持续时间)
首先看如何将属性转换为 Duration 对象。
假设有两个 Duration 类型的字段:
@ConfigurationProperties(prefix = "conversion")
public class PropertyConversion {
private Duration timeInDefaultUnit;
private Duration timeInNano;
// ...
}
对应的属性文件:
conversion.timeInDefaultUnit=10
conversion.timeInNano=9ns
结果:
timeInDefaultUnit的值为 10 毫秒(默认单位是毫秒)timeInNano的值为 9 纳秒
支持的单位有:ns(纳秒)、us(微秒)、ms(毫秒)、s(秒)、m(分钟)、h(小时)和 d(天)。
如果不指定单位,数值将默认按毫秒处理。
也可以使用 @DurationUnit 覆盖默认单位:
@DurationUnit(ChronoUnit.DAYS)
private Duration timeInDays;
对应属性:
conversion.timeInDays=2
7.2 DataSize(数据大小)
类似地,Spring Boot 的 @ConfigurationProperties 也支持 DataSize 类型转换。
添加三个 DataSize 字段:
private DataSize sizeInDefaultUnit;
private DataSize sizeInGB;
@DataSizeUnit(DataUnit.TERABYTES)
private DataSize sizeInTB;
对应属性:
conversion.sizeInDefaultUnit=300
conversion.sizeInGB=2GB
conversion.sizeInTB=4
结果:
sizeInDefaultUnit为 300 字节(默认单位是字节)sizeInGB为 2 GBsizeInTB为 4 TB(因为指定了单位为 TB)
支持的单位有:B、KB、MB、GB 和 TB。同样可通过 @DataSizeUnit 覆盖默认单位。
7.3 自定义转换器(Custom Converter)
我们也可以编写自定义 Converter,将属性值转换为特定的类类型。
例如,定义一个简单的 Employee 类:
public class Employee {
private String name;
private double salary;
// 构造函数、getter/setter 略
}
假设属性文件中有如下内容:
conversion.employee=john,2000
我们希望将其转换为 Employee 对象:
private Employee employee;
为此,需实现 Converter<String, Employee> 接口,并使用 @ConfigurationPropertiesBinding 注解注册该转换器:
@Component
@ConfigurationPropertiesBinding
public class EmployeeConverter implements Converter<String, Employee> {
@Override
public Employee convert(String from) {
String[] data = from.split(",");
return new Employee(data[0], Double.parseDouble(data[1]));
}
}
8. 不可变的 @ConfigurationProperties 绑定
从 Spring Boot 2.2 开始,我们可以使用 @ConstructorBinding 注解通过构造函数注入来绑定配置属性,从而实现不可变对象。
这意味着 @ConfigurationProperties 注解的类现在可以是不可变的。
注意:在 Spring Boot 3 中,如果类只有一个带参数的构造函数,则构造函数绑定是隐式的,无需显式添加
@ConstructorBinding。但如果存在多个构造函数,则必须使用该注解标记首选的构造函数。
示例:
@ConfigurationProperties(prefix = "mail.credentials")
public class ImmutableCredentials {
private final String authMethod;
private final String username;
private final String password;
@ConstructorBinding
public ImmutableCredentials(String authMethod, String username, String password) {
this.authMethod = authMethod;
this.username = username;
this.password = password;
}
// 另一个构造函数(非绑定用)
public ImmutableCredentials(String username, String password) {
this.username = username;
this.password = password;
this.authMethod = "Default";
}
// 只读 getter
public String getAuthMethod() { return authMethod; }
public String getUsername() { return username; }
public String getPassword() { return password; }
}
使用 @ConstructorBinding 时,必须提供包含所有待绑定参数的构造函数。
注意:所有字段都是 final 的,且没有 setter 方法。
重要:要使用构造函数绑定,必须通过
@EnableConfigurationProperties或@ConfigurationPropertiesScan显式启用配置类。
9. Java 16 Records
Java 16 引入了记录类(record),作为 JEP 395 的一部分。记录类是用于承载不可变数据的透明载体,非常适合作为配置持有类或 DTO。
事实上,我们完全可以将 Java record 定义为 Spring Boot 的配置属性。
例如,前面的例子可以用 record 重写为:
@ConstructorBinding
@ConfigurationProperties(prefix = "mail.credentials")
public record ImmutableCredentials(String authMethod, String username, String password) {
}
显然,这比冗长的 getter/setter 代码简洁得多。
补充:从 Spring Boot 2.6 开始,对于只有一个构造函数的 record,可以省略
@ConstructorBinding注解。但如果 record 有多个构造函数,则仍需使用该注解来指定用于属性绑定的构造函数。
10. 总结
本文深入探讨了 Spring Boot 中的 @ConfigurationProperties 注解,并重点介绍了其提供的多项实用功能,包括宽松绑定(relaxed binding)和 Bean Validation(JSR-380)校验等。通过合理使用该注解,我们可以构建更清晰、更安全、更易维护的配置管理代码。