Spring Boot v2.4.4源码解析(六)属性绑定篇上 —— 类型安全配置属性

Spring Boot JavaBean 属性绑定

一、概述

在Spring Boot下开发某个模块,如果这个模块配置属性比较多而且具有层级关系,使用@Value("${property}")注解依次注入这些属性显得比较麻烦。Spring Boot提供了另一种替代方案,可以使用@ConfigurationProperties注解将这些属性绑定到类上。

例如,Mybatis将配置属性绑定到`MybatisProperties`类上:
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

	public static final String MYBATIS_PREFIX = "mybatis";
	private String configLocation;
	private String[] mapperLocations;
	private String typeAliasesPackage;
	//  省略其余字段
	private Configuration configuration;
	// 省略getter/setter方法
}
public class Configuration {
	
	protected boolean safeRowBoundsEnabled;
	protected boolean safeResultHandlerEnabled = true;
	protected boolean mapUnderscoreToCamelCase;
  	// 省略其余代码
 }

MybatisProperties 类中各字段在配置文件中分别取值为:

  • mybatis.config-location
  • mybatis.mapper-locations
  • mybatis.type-aliases-package
  • mybatis.configuration.safe-row-bounds-enabled
  • mybatis.configuration.safe-result-handler-enabled
  • mybatis.configuration.map-underscore-to-camel-case;

类似Bean注入除了可以使用@Autowired注入之外,可以使用构造器注入,属性绑定也可以使用@ConstructorBinding注解进行构造器绑定,参考官方文档Spring Boot Reference Documentation

二、激活 @ConfigurationProperties 注解类

Spring Boot提供了一下几种机制将@ConfigurationProperties注解类注册到IOC容器:

  • @Configuration类(@Component)上将需要使用到的属性绑定类注解类通过@EnableConfigurationProperties列出;
    @EnableConfigurationProperties(MybatisProperties.class)
    @Configuration
    public class MybatisAutoConfiguration implements InitializingBean {
    }
    
  • 通过包扫描方式将扫描到的@ConfigurationProperties注解类注入到容器。这种方式需要在@SpringBootApplication注解的启动主类,或者任何@Configuration注解类上注解@ConfigurationPropertiesScan,默认扫描包为@ConfigurationPropertiesScan注解类所在包,也可通过basePackages()指出;
    @SpringBootApplication
    @ConfigurationPropertiesScan({ "com.example.app", "org.acme.another" })
    public class MyApplication {
    }
    

一旦@ConfigurationProperties注解类注册到IOC容器成功后,便可以在其他Bean中将其注入。

  • 对于第三方Jar包中的类,不能修改源码,加@Component等注解,Spring Boot提供了@Configuration + @Bean 方式将其注册到IOC容器。那如果属性绑定类源自第三方Jar包呢?解决方法类似,通过在@Configuration 注解类中@Beanpublic方法中在加上@ConfigurationProperties可以将该方法返回的Bean属性绑定并注册到IOC容器中;
    @Configuration
    public class Test {
        @Bean
        @ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
        public MybatisProperties mybatisProperties(){
            return new MybatisProperties();
        }
    }
    

三、属性转换

Spring Boot 在将这些配置属性绑定到@ConfigurationProperties Bean属性时尝试转换成正确类型。如果需要自定义转换规则,可以提供ConversionService Bean,或者通过CustomEditorConfigurer Bean自定义属性编辑器,或者通过注解@ConfigurationPropertiesBinding自定义Converters

1. Duration 类型转换

如果绑定类中包含java.time.Duration字段,则配置文件中支持如下格式配置:

  • 带单位或者不带单位的数字,例如,10s表示10秒,数字范围需要在long内。如果配置项纯数字没有带单位,可以通过在绑定类java.time.Duration字段中通过@DurationUnit指定单位,如果都没指定,则取默认值默认为ChronoUnit.MILLIS,即毫秒。
  • 标准ISO-8601格式;

转换源码位于org.springframework.boot.convert.StringToDurationConverter类中,上面的两种格式跟别对应org.springframework.boot.convert.DurationStyle枚举类的SIMPLE(正则表达式^([+-]?\d+)([a-zA-Z]{0,2})$)和ISO8601(正则表达式^[+-]?P.*$),Spring Boot用各自正则表达式确定配置项属于那种格式类型。
对于SIMPLE类型格式,其正则表达式包含两部分([+-]?\d+)([a-zA-Z]{0,2}),表示纯数字的时间间隔和0-2位的单位,单位字母大小写不敏感。
SIMPLE支持的单位定义在DurationStyle内部枚举类Unit中:

  • ns,纳秒;
  • us,微妙;
  • ms,毫秒;
  • s,秒;
  • m,分钟;
  • h,小时;
  • d,天;

解析SIMPLE类型字符串到java.time.Duration源码如下:

Matcher matcher = matcher(value);
Assert.state(matcher.matches(), "Does not match simple duration pattern");
String suffix = matcher.group(2);
return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix) : Unit.fromChronoUnit(unit))
		.parse(matcher.group(1));

value表示配置项字符串,unit表示java.time.Duration字段@DurationUnit注解指定的单位,从上面源码可以看出,Spring Boot优先使用配置项中配置的单位,如果配置项只包含纯数字,才会使用java.time.Duration字段@DurationUnit注解指定的单位,都没有则默认毫秒。

2. Period 类型转换

Duration类型转换类似,如果绑定类中包含java.time.Period字段,则配置文件中支持如下格式配置:

  • 带单位或者不带单位的数字, 数字范围需要在int内。如果配置项纯数字没有带单位,可以通过在绑定类java.time.Period字段中通过@PeriodUnit指定单位,如果都没指定,则取默认值默认为ChronoUnit.DAYS,即天。
  • 标准ISO-8601格式;

转换源码位于org.springframework.boot.convert.StringToPeriodConverter类中,同样,上面两种格式类型也定义了枚举类org.springframework.boot.convert.PeriodStyle。对于SIMPLE类型格式, 也定义了其支持单位枚举类org.springframework.boot.convert.PeriodStyle.Unit,其支持的单位如下:

  • d,天;
  • w,周;
  • m,月份;
  • y,年份;

Duration不同的是,PeriodSIMPLE类型格式这些单位可以同时出现,所以SIMPLE正则表达式为"^" + "(?:([-+]?[0-9]+)Y)?" + "(?:([-+]?[0-9]+)M)?" + "(?:([-+]?[0-9]+)W)?" + "(?:([-+]?[0-9]+)D)?" + "$"
"(?:([-+]?[0-9]+)Y)?"表示匹配年, "(?:([-+]?[0-9]+)M)?"部分表示匹配月, "(?:([-+]?[0-9]+)W)?"部分表示匹配周,"(?:([-+]?[0-9]+)D)?"部分表示匹配天,?:表示匹配时单位YMWY不存储,这样可以方便抽取出各自单位的数字。
解析SIMPLE类型字符串到java.time.Period源码如下:

if (NUMERIC.matcher(value).matches()) {
	return Unit.fromChronoUnit(unit).parse(value);
}
Matcher matcher = matcher(value);
Assert.state(matcher.matches(), "Does not match simple period pattern");
Assert.isTrue(hasAtLeastOneGroupValue(matcher), "'" + value + "' is not a valid simple period");
int years = parseInt(matcher, 1);
int months = parseInt(matcher, 2);
int weeks = parseInt(matcher, 3);
int days = parseInt(matcher, 4);
return Period.of(years, months, Math.addExact(Math.multiplyExact(weeks, 7), days));

3. DataSize 类型转换

DataSize类型转换和Duration类似,由于DataSize类型是Spring自定义类org.springframework.util.unit.DataSize,所以不支持ISO-8601格式,而且单位不能重复出现,使用@DataSizeUnit指定org.springframework.util.unit.DataSize字段单位。
其支持单位有:

  • B
  • KB
  • MB
  • GB
  • TB

四、复杂类型合并处理

通过Spring Boot v2.4.4源码解析(五)配置文件加载篇一可以看出,Spring boot配置可能来自多个配置源,如果和List或者Map中各元素绑定的配置项分散在不同配置源时,Spring Boot怎么处理呢?

1. List 整体替换

List在多个地方配置时,Spring Boot会用后面加载的属性源整体覆盖前面加载的配置源,不会合并。
例如,假设MyPojo对象包含namedescription属性,且默认为null。属性绑定类AcmeProperties包含MyPojo对象列表:

@ConfigurationProperties("acme")
public class AcmeProperties {
    private final List<MyPojo> list = new ArrayList<>();
    public List<MyPojo> getList() {
        return this.list;
    }
}

配置文件如下:

acme.list[0].name=my name
acme.list[0].description=my description
#---
spring.config.activate.on-profile=dev
acme.list[0].name=my another name

以上写法时Spring Boot 2.4 新增特性,可用在一个Java properties配置文件中用#---隔开多个逻辑配置文件。
如果当前激活profiledevAcmeProperties.list只包含一个MyPojo对象,由第一个逻辑配置文件中定义。然而, 当dev profile 处于激活状态时,AcmeProperties.list也只包含一个元素(name属性为my another namedescription属性为null),源自第二个逻辑配置文件。
Spring Boot v2.4.4源码解析(五)配置文件加载篇一已经说明,Spring Boot 2.4 后加载的属性源优先级比先加载属性源优先级高,这里可以理解为Spring Boot整体区分List,而不会以List每一项作为区分,这样相当于不同配置源包含相同属性时,后加载属性覆盖先加载属性。
再例如,如下配置:

acme.list[0].name=my name
acme.list[0].description=my description
acme.list[1].name=another name
acme.list[1].description=another description
#---
spring.config.activate.on-profile=dev
acme.list[0].name=my another name

dev profile 处于激活状态时,AcmeProperties.list也只包含一个元素(name属性为my another namedescription属性为null),源自第二个逻辑配置文件。

2. Map 合并

对于绑定类为Map, Spring Boot 会将所有配置源中相关属性合并到Map中,如果多配置源中包含相同属性,则使用高优先级(后加载)。
例如,重新定义AcmeProperties如下:

@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
acme.map.key1.description=my description 1
#---
spring.config.activate.on-profile=dev
acme.map.key1.name=dev name 1
acme.map.key2.name=dev name 2
acme.map.key2.description=dev description 2

如果当前激活profiledevAcmeProperties.map包含一个键key1(值对象name属性为my name 1description属性为my description 1)。
dev profile 处于激活状态时,AcmeProperties.map包含两个个键key1(值对象name属性为dev name 1description属性为my description 1)和key2(值对象name属性为dev name 2description属性为dev description 2)。这里可以理解为当配置为Map,Spring Boot按每一项区分,而不是整体区分。

五、@ConfigurationProperties 校验

Controller层参数检验一样,Spring Boot支持在@ConfigurationProperties注解类上使用@Validated对绑定属性开启校验,并可以直接将JSR-303 javax.validation 约束注解加到@ConfigurationProperties注解类属性上进行校验。JSR-303 javax.validation 约束注解主要包括@NotNull(非空),@NotBlank(字符串非空),@Email(邮件格式)等等。
例如:

@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
    @NotNull
    private InetAddress remoteAddress;
    // ... getters and setters
}

@Validated也可用于通过@Configuration+@Bean创建的属性绑定类;
另外需要注意的是,@ConfigurationProperties注解类嵌套属性也开启校验的话,需要在相关字段值标注@Valid注解。
例如:

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

六、松散绑定

概述可以看出Java Bean属性和配置属性并不是精确匹配,Java Bean属性采用骆峰式,而配置属性使用短横线命名,也能绑定成功,这是由于Spring Boot采用了松散规则将配置项和@ConfigurationProperties注解类属性进行绑定。
例如,有如下@ConfigurationProperties类:

@ConfigurationProperties(prefix="acme.my-project.person")
public class OwnerProperties {
    private String firstName;
     // ... getters and setters
}

对于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,大写格式,只有环境变量数据源systemEnvironment才能使用;

其实使用分数字字母分隔符分割也能匹配成功,比如acme.my#project.person.first#nameacme.my&project.person.first&name等等都能和OwnerPropertiesfirstName绑定成功;

综上,所有数据源简单属性短横线命名,标准骆峰式,下划线分割都能绑定成功,并且环境变量systemEnvironment还支持使用下划线分割的大写方式。
对于List, 除了.properties[])和.yml-)各自配置语法外,还可以使用逗号分割配置。
另外,环境变量也可以配置List,但数字需要用下划线包围, 即.properties中的[]需要变成_,例如在.properties中的my.acme[0].other配置项,在环境变量中需要写成MY_ACME_0_OTHER

七、@ConfigurationProperties 和 @Value对比

特性@ConfigurationPropertie@Value
松散绑定受限
支持元数据×
SpEL计算×

参考

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值