本文主要是介绍Spring Boot v2.4.4源码解析(六)属性绑定篇上 —— 类型安全配置属性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Spring Boot JavaBean 属性绑定
一、概述
在Spring Boot下开发某个模块,如果这个模块配置属性比较多而且具有层级关系,使用@Value("${property}")
注解依次注入这些属性显得比较麻烦。Spring Boot提供了另一种替代方案,可以使用@ConfigurationProperties
注解将这些属性绑定到类上。
@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
注解类中@Bean
的public
方法中在加上@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
不同的是,Period
的SIMPLE
类型格式这些单位可以同时出现,所以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)?"
部分表示匹配天,?:
表示匹配时单位Y
,M
,W
,Y
不存储,这样可以方便抽取出各自单位的数字。
解析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
对象包含name
和description
属性,且默认为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
配置文件中用#---
隔开多个逻辑配置文件。
如果当前激活profile
非dev
,AcmeProperties.list
只包含一个MyPojo
对象,由第一个逻辑配置文件中定义。然而, 当dev
profile
处于激活状态时,AcmeProperties.list
也只包含一个元素(name
属性为my another name
,description
属性为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 name
,description
属性为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
如果当前激活profile
非dev
,AcmeProperties.map
包含一个键key1
(值对象name
属性为my name 1
,description
属性为my description 1
)。
当dev
profile
处于激活状态时,AcmeProperties.map
包含两个个键key1
(值对象name
属性为dev name 1
,description
属性为my description 1
)和key2
(值对象name
属性为dev name 2
,description
属性为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 {@NotNullprivate InetAddress remoteAddress;// ... getters and setters
}
@Validated
也可用于通过@Configuration
+@Bean
创建的属性绑定类;
另外需要注意的是,@ConfigurationProperties
注解类嵌套属性也开启校验的话,需要在相关字段值标注@Valid
注解。
例如:
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {@NotNullprivate InetAddress remoteAddress;@Validprivate final Security security = new Security();// ... getters and setterspublic static class Security {@NotEmptypublic 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#name
,acme.my&project.person.first&name
等等都能和OwnerProperties
的firstName
绑定成功;
综上,所有数据源简单属性短横线命名,标准骆峰式,下划线分割都能绑定成功,并且环境变量systemEnvironment
还支持使用下划线分割的大写方式。
对于List
, 除了.properties
([]
)和.yml
(-
)各自配置语法外,还可以使用逗号分割配置。
另外,环境变量也可以配置List
,但数字需要用下划线包围, 即.properties
中的[]
需要变成_
,例如在.properties
中的my.acme[0].other
配置项,在环境变量中需要写成MY_ACME_0_OTHER
。
七、@ConfigurationProperties 和 @Value对比
特性 | @ConfigurationPropertie | @Value |
---|---|---|
松散绑定 | √ | 受限 |
支持元数据 | √ | × |
SpEL 计算 | × | √ |
参考
- [1] Spring Boot Reference Documentation 2.4.1
- [2] Property Binding in Spring Boot 2.0
这篇关于Spring Boot v2.4.4源码解析(六)属性绑定篇上 —— 类型安全配置属性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!