分类
spring-boot

Spring Boot项目应该怎样进行项目配置

configuration properties in spring boot

1. 概述

Spring Boot提供了很多实用的功能。这其中就包括了我们可以为项目定义配置文件,并轻松的访问配置文件中定义的属性。在上文 中,我们对Spring 项目配置的几种方法进行了介绍。

本文我们一起来深入的学习@ConfigurationProperties注解的使用方法。

2. 依赖

和其它文章一样,本文依赖于Spring Boot,版本为2.3.4:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

同时引入用于属性校验的hibernate-validator

		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>6.1.6.Final</version>
		</dependency>

Hibernate Validator做为java内置验证器的补充,能够处理更多的验证场景。可点击查看官方文档来提供了更多信息。

3. 简单配置

官方文档强烈建议我们将项目的一些配置项封装到POJO中,比如我们如下定义用于发送短信的配置:

@Configuration ➊
@ConfigurationProperties(prefix = "sms") ➋
public class SmsConfigProperties {
    /**
     * 短信接口地址
     */
    private String apiUrl;

    /**
     * 请求方法
     */
    private String requestMethod;

    /**
     * 端口
     */
    private int port;

    /**
     * 接收提醒消息的邮箱
     */
    private String email;

    // 请自行补充setter/getter方法
  • @Configuration将在应用启动时在Spring应用上下文中创建一个Bean。
  • @ConfigurationProperties(prefix = "sms")注解将配置文件中以sms的属性与SmsConfigProperties中的属性进行一一关联。

所以此时我们可以配置文件application.properties中加入以下配置:

# 短信基础配置信息
sms.api-url=sms.codedemo.club
sms.requestMethod=post
sms.port=8088
sms.email=panjie@yunzhi.club

然后便可以通过SmsConfigProperties实例的getter方法来获取相应的属性值了:

    @Autowired
    SmsConfigProperties smsConfigProperties;
    ...
    this.smsConfigProperties.getApiUrl();

值得注意的是由于Spring框架对配置进行赋值时调用的是标准的setter方法,所以我们必须在配置类中为每一个属性提供相应的setter方法。

除使用@Configuration注解将配置类标识为Spring Bean以外,还可以通过在启动类上加入@EnableConfigurationProperties注解的方法:

@ConfigurationProperties(prefix = "foo")
public class FooConfigProperties {
    private String bar;
@SpringBootApplication
@EnableConfigurationProperties(FooConfigProperties.class) ➊
public class ConfigurationPropertiesInSpringBootApplication {
  • ➊ 在应用启动时创建相关的Bean。此时Spring同样将自动读取配置文件application.properties中的相关配置前缀来构造相应的配置信息。

Spring在进行配置绑定时,可以非常聪明地获取到相应的属性值,比如我们在SmsConfigProperties中定义了apiUrl,则Spring可以成功绑定以下任意格式至该字段:

sms.apiUrl=sms.codedemo.club
sms.apiurl=sms.codedemo.club
sms.api_url=sms.codedemo.club
sms.api-url=sms.codedemo.club
sms.API_URL=sms.codedemo.club

3.1 Spring Boot版本兼容性

在部分Spring Boot版本中(比如Spring 2.2.0),Spring在启动时会自动扫描启动类所在包下所有以@ConfigurationProperties注解的类。在这种情况下,我们可以删除配置类上的@Component 相关注解(比如我们在上文中应用的 @Configuration),而只需要在配置类上保留@EnableConfigurationProperties即可:

@ConfigurationProperties(prefix = "scan.foo")
public class ScanFooConfigProperties {
    private String bar;

如果你恰恰使用是具有自动扫描功能的Spring Boot版本,但却想阻止Spring这么做,则可以使用 @ConfigurationPropertiesScan 注解来自定义扫描的包:

@ConfigurationPropertiesScan("club.codedemo.configurationpropertiesinspringboot.scan")
public class ConfigurationPropertiesInSpringBootApplication {

此时Spring Boot在启动时将仅扫描指定包中以@ConfigurationProperties为注解的类。当然了,即使我们使用的是其它版本,也可以这么做来实现扫描特定包中的配置类的目的。

4. 嵌套属性

有时候需要在配置文件中定义一些特殊类型的属性,比如:List、Maps或者其它java类型,我们将其统称为嵌套属性。

比如我们创建一个用于存储发送短信时第三方平台要求的用户名、密码等信息的Credentials认证信息类:

/**
 * 认证信息
 */
public class Credentials {
    private String id;
    /**
     * 密钥
     */
    private String secret;
    /**
     * 认证令牌
     */
    private String token;

然后将其与其它类型的属性一并加入到短信配置类中:

public class SmsConfigProperties {
...
    /**
     * 签名
     */
    private List<String> signs;

    /**
     * 附加请求头
     */
    private Map<String, String> additionalHeaders;

    /**
     * 认证信息
     */
    private Credentials credentials;

    // 请自行补充setter/getter方法

则在配置文件application.properties中我们可以如下定义:

# 签名信息
sms.signs[0]=codedemo
sms.signs[1]=yunzhi

# 获取使用以下签名信息,与上述效果等同
# sms.signs=codedemo,yunzhi

# 附加头信息
sms.additionalHeaders.secure=true
sms.additionalHeaders.senduser=panjie

# 认证信息
sms.credentials.id=yourId
sms.credentials.secret=yourSecret
sms.credentials.token=yourToken

5. 在使用@Bean注解的方法上同时使用@ConfigurationProperties注解

我们同样可以在@Bean注解的方法上添加 @ConfigurationProperties 注解。这往往适用于把一些属性绑定到第三方的组件时。

比如存在以下的简单类:

public class BeanMethod {
    private String foo;

则我们可以将@Bean@ConfigurationProperties 综合添加到相关方法上来构造一个BeanMethod实例:

@Configuration
public class ConfigProperties {
    @Bean
    @ConfigurationProperties(prefix = "bean.method")
    public BeanMethod beanMethodFoo() {
        return new BeanMethod();
    }
}

此时配置文件中定义的以bean.method打头的属性便会自动的关联到BeanMethod实例上。

bean.method.foo=beanMethodBar

6. 属性验证

@ConfigurationProperties 遵从了JSR-303格式的验证规范,我们可以通过在类上添加@Validated注解的方法来启用属性验证功能:

@Configuration
@Validated ➊
@ConfigurationProperties(prefix = "sms")
public class SmsConfigProperties {
  • ➊ 启动属性验证功能。在某些版本的Spring Boot中,可省略该注解。

此时,我们便可以在配置类的相关属性上加入必要的验证信息了,比如我们要求必须提供apiUrl字段的值:

    /**
     * 短信接口地址
     * 此字段不为空
     * 请修改配置文件中的sms.api-url的值为空来测试@NotBlank注解
     */
    @NotBlank
    private String apiUrl;

规定字段的长度:

    /**
     * 请求方法
     * 长度介于3-8位之间
     */
    @Length(max = 8, min = 3)
    private String requestMethod;

规定字段的大小:

    /**
     * 端口
     * 介于1-65535之间
     */
    @Min(1)
    @Max(65535)
    private int port;

用正则表达式处理复杂规范:

    /**
     * 接收提醒消息的邮箱
     * 使用正则表达式校验邮箱格式
     */
    @Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
    private String email;

Spring Boot在启动项目时,将对添加了验证器的字段进行验证,当任意字段不符合验证规范时将抛出IllegalStateException异常并终止应用。比如我们将配置文件中sms.requestMethod的值设置为长度为2的字符串,则应用启动时将报如下错误信息:

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'sms' to club.codedemo.configurationpropertiesinspringboot.SmsConfigProperties$$EnhancerBySpringCGLIB$$6681a0ab failed:

    Property: sms.requestMethod
    Value: po
    Origin: class path resource [application.properties]:3:19
    Reason: length must be between 3 and 8


Action:

Update your application's configuration

在应用启动时发生异常并中断应用虽然对单元测试不够友好(水平问题,尚未掌握Spring Boot项目启动失败的测试方法),但对保证项目的健壮性却是非常非常实用的。

我们在上述代码了使用了部分来自于Hibernate的验证器,此部分验证器工作时依赖于Java Bean的getter和setter方法,所以在使用时应注意为每个字段均提供getter/setter方法。

7. 属性转换

Spring Boot提供的@ConfigurationProperties 支持多种属性转换。

7.1 Duration

比如我们在配置文件中定义两个类型为Duration的字段:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {
    /**
     * 默认的时间单位为ms
     */
    private Duration timeInDefaultUnit;
    /**
     * 在赋值时传入其它单位后缀,比如:ns
     */
    private Duration timeInNano;

然后在配置文件中如下定义:

# 时间单位自动转换
conversion.timeInDefaultUnit=30
conversion.timeInNano=50ns

则最终在配置实例中得到的timeInDefaultUnit的值为30ms,timeInNano的值为50ns。

Spring支持的时间单位有:ns、us、ms、s、m、h、d分别代表:纳秒、微秒、毫秒、秒、分钟、小时、天

默认的时间单位为毫秒ms,这也意味着当我们指定的时间单位为ms时,可以省略ms后缀。

我们还可以通过加入 @DurationUnit 注解来变更字段的默认时间单位,比如我们将某个字段的时间单位指定为天:

    /**
     * 指定默认时间单位为天
     */
    @DurationUnit(ChronoUnit.DAYS)
    private Duration timeInDays;

则如下配置后,timeInDays属性的值为10天:

conversion.timeInDays=10

7.2 DataSize

@ConfigurationProperties 还支持DataSize类型的自动转换,转换与设置的方法与Duration完全相同,比如我们在配置类中添加如下几个DataSize类型字段:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {

    ...

    /**
     * 默认数据单元为 byte
     */
    private DataSize sizeInDefaultUnit;

    /**
     * 支持传入带后缀的数据单位,比如GB
     */
    private DataSize sizeInGB;

    /**
     * 自定义数据单位为TB
     */
    @DataSizeUnit(DataUnit.TERABYTES)
    private DataSize sizeInTB;

然后加入如下配置:

# 数据大小单位自动转换
conversion.sizeInDefaultUnit=30
conversion.sizeInGB=50GB
conversion.sizeInTB=10

则配置实例中sizeInDefaultUnit的值为30字节,sizeInGB的值为50G,sizeInTB的值为10TB。

Spring支持的数据单位有:B、KB、MB、GB以及TB,同样的可以使用@DataSizeUnit来指定某个字段的默认单位。

7.3 自定义转换器

此外,Spring还支持自定义转换器。比如我们有如下员工Employee类:

public class Employee {
    private String name;
    /**
     * 薪水
     */
    private double salary;
    // 此处省略了setter/getter

同时在配置文件中加入以下配置信息:

conversion.employee=john,2000

则预实现将配置文件中的配置信息映射到Employee类中的目标,可以定义如下转换器:

@Component ➊
@ConfigurationPropertiesBinding ➋
public class EmployeeConverter implements Converter<String, Employee>➌ {

    @Override
    public Employee convert(String s) {
        String[] data = s.split(",");
        Employee employee = new Employee();
        employee.setName(data[0]);
        employee.setSalary(Double.parseDouble(data[1]));
        return employee;
    }
}
  • ➊ 声明为组件,以便Spring在启动时扫描到
  • ➋ 使用@ConfigurationPropertiesBinding注解标识该类为自定义转换类
  • ➌ 自定义的转换类需要实现Converter接口

此时当我们在配置文件中使用Employee类时,则会自动调用上述自定义转换器以达到数据转换的目的:

@Configuration
@ConfigurationProperties(prefix = "conversion")
public class ConversionProperties {
    ...
    private Employee employee;
    ...

8. @ConfigurationProperties绑定不可变类

自Spring 2.2开始,我们可以使用@ConstructorBinding 注解来绑定配置文件。该方法用于绑定不可变Immutable配置:

// @Configuration ➍
@ConfigurationProperties(prefix = "sms.credentials")
@ConstructorBinding ➌
public class ImmutableCredentialsProperties {
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final➊ String id;
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final String secret;
    /**
     * 字段被声明为final,初始化后不可变
     */
    private final String token;

    public ImmutableCredentialsProperties➋(String id, String secret, String token) {
        this.id = id;
        this.secret = secret;
        this.token = token;
    }

    public String getId() {
        return id;
    }

    public String getSecret() {
        return secret;
    }

    public String getToken() {
        return token;
    }

    // 注意,此类无setter方法 ➎
  • ➊ 字段类型为final
  • ➋ 于构造函数中对属性赋初值
  • 标明此实例在初始化不可改变
  • 一定不能有@Configuration注解!
  • 一定不能有setter方法!

最后我们需要注意的是:由于➍不能加@Configuration注解,所以若想使该配置类生效,则需要在系统启动类上加入@EnableConfigurationProperties@ConfigurationPropertiesScan注解。

@EnableConfigurationProperties({FooConfigProperties.class, ImmutableCredentialsProperties.class★})
@ConfigurationPropertiesScan("club.codedemo.configurationpropertiesinspringboot.scan")
public class ConfigurationPropertiesInSpringBootApplication {

9. 总结

本文中,我们介绍了Spring Boot项目中进行配置的几种方法。在Spring Boot中我们可以通过加入前缀的方式来快速的将配置信息绑定到配置类中。即可以绑定基本属性,又可以绑定特殊类型,在绑定的过程中还可以加入相应的验证器。支持对特殊类型的绑定,也支持自定义的转换器。此外,我们还提供了带有相对完整测试用例的demo以期降低本文的学习难度。