Spring Boot @ConfigurationProper
一、如何绑定到DataObject上
1.1 NO.1 @component+@ConfigurationProperties
application.properties如下
demo.email=111
ConfigurationBindingDemo类
@Component
@ConfigurationProperties(prefix = "demo")
public class ConfigurationBindingDemo {
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
1.2 NO.2 @Bean+@ConfigurationProperties
如果同一个类需要注册成多个bean(即多个DataObject实例),可以采用这种方式
application.properties如下
demo1.email=111
demo2.email=222
去掉上面的注解,改用@Bean注册bean
public class ConfigurationBindingDemo {
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
@Configuration
public class Config {
@Bean
@ConfigurationProperties(prefix = "demo1")
public ConfigurationBindingDemo demo1() {
return new ConfigurationBindingDemo();
}
@Bean
@ConfigurationProperties(prefix = "demo2")
public ConfigurationBindingDemo demo2() {
return new ConfigurationBindingDemo();
}
}
1.3 NO.3 使用@ConfigurationPropertiesScan扫描指定目录下的@ConfigurationProperties
这种方式还可以将指定目录下的@ConfigurationProperties全部扫描进来,类似于@ComponentScan的用法。
1.4 @EnableConfigurationProperties
将@EnableConfigurationProperties标在一个配置类上,指定多个标有 @ConfigurationProperties的类
@Configuration
@EnableConfigurationProperties(DemoAutoConfiguration.class)
public class Config {
}
1.5 AutoConfiguration+@EnableConfigurationProperties
三方maven依赖希望使用@ConfigurationProperties的话,需要借助AutoConfiguration机制,导入AutoConfiguration配置类,在AutoConfiguration配置类上标注@EnableConfigurationProperties,这样当AutoConfiguration配置类生效时@EnableConfigurationProperties也会生效,application.properties如下
demo.email=111
ConfigurationBindingDemo类
@ConfigurationProperties(prefix = "demo")
public class ConfigurationBindingDemo {
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
开启自动配置
@EnableConfigurationProperties(ConfigurationBindingDemo.class)
public class DemoAutoConfiguration {
}
META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.example.binder.DemoAutoConfiguration
二、@ConfigurationProperties两个属性
ignoreInvalidFields和ignoreUnknownFields是@ConfigurationProperties里的两个属性。
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {
@AliasFor("prefix")
String value() default "";
@AliasFor("value")
String prefix() default "";
boolean ignoreInvalidFields() default false;
boolean ignoreUnknownFields() default true;
}
2.1 ignoreInvalidFields
ignoreInvalidFields默认是false,当绑定时候出现错误是否要忽略,这种错误一般是类型转换错误。application.properties如下
demo.number=1.1
ConfigurationBindingDemo类
@ConfigurationProperties(prefix = "demo")
@Component
public class ConfigurationBindingDemo {
private Integer number;
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
}
1.1无法转换成Integer,启动程序后会报错。当设置ignoreInvalidFields = true后,能够正常启动,number为默认值null。
2.2 ignoreUnknownFields
ignoreUnknownFields默认是true,会忽略掉.properties文件里未绑定到DataObject里的kv,当设置ignoreUnknownFields = false后,如果.properties文件里存在kv未绑定到DataObject里,会报错。application.properties如下
demo.number=1
demo.example=222
ConfigurationBindingDemo类跟上面一样,程序启动后会报错。
三、properties中key与DataObject fieldName映射
@ConfigurationProperties将.properties里面配置的kv绑定到DataObject里面的field的流程简单来说就是:
1、递归的遍历DataObject里面的每一个属性(因为DataObject里面的属性也可能是另一个DataObject)。
2、从.properties查找能够绑定到这个属性的kv。
3、将 .properties里配置的值转换成DataObject field属性的值。
DataObject里的属性一般是驼峰命名法,其类型可能有几种情况:
1、值属性,例如Integer、int、String
2、DataObject,需要对DataObject里面的每一个属性进行绑定
3、数组属性
4、Collection属性
5、Map属性
不同类型映射到field有差异,下面分别看key与fieldName如何映射。
3.1 值属性
一般来讲,fieldName使用驼峰命名法,而.properties文件为了可读性,使用’-'分隔多个单词。例如要给ConfigurationBindingDemo的phoneNumber属性绑定值的话,要写成phone-number,如下
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private String phoneNumber;
}
demo.phone-number=123
而实际上规则没有这么死,.properties映射到field时,.properties有以下规则:
- 字母、数字、’.’、’-'是有效字符,其他无效字符会忽略
- '.'是分隔符,指明field的层级结构
- '-'增强可读性,映射到field会忽略
- 忽略大小写
根据以上规则,假定希望绑定属性ConfigurationBindingDemo类的phoneNumber属性,如下
@ConfigurationProperties(prefix = "demo")
@Component
public class ConfigurationBindingDemo {
private String phoneNumber1;
private String phoneNumber2;
private String phoneNumber3;
private String phoneNumber4;
private String phoneNumber5;
private String phoneNumber6;
private String phoneNumber7;
// getters and setters
}
那么.properties里可以有很多写法
demo.phoneNumber1=1
demo.phonenumber2=2
demo.phone-number3=3
demo.phone_number4=3
DEMO.PHONEnumber5=4
demo.--_-phone______numBer----__6___=6
demo.--_-phone&&&nu**mBer----__7___=7
3.2 DataObject
通过字符’.'增加层级结构就行
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private Apple apple;
@Data
public static class Apple {
private double weight;
}
}
demo.apple.weight=1.11
3.3 数组属性
情况一:item能直接转换
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private String[] names;
}
要将.properties绑定到names数组上,可以有三种写法
3.3.1 NO.1 将所有值用’,'分隔
demo.names=a,b,c,d,e
3.3.2 NO.2 [index]指定位置
demo.names[0]=1
demo.names[1]=2
demo.names[2]=3
demo.names[3]=4
demo.names[4]=5
3.3.3 NO.3 .index指定位置
demo.names.0=1
demo.names.1=2
demo.names.2=3
demo.names.3=4
demo.names.4=a
情况二: item不能直接转换
可以通过.index/[index]继续加上.fieldName设置值
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private Person[] persons;
@Data
private static class Person {
private String name;
private Double weight;
}
}
demo.persons[0].name=jack
demo.persons[0].weight=120
demo.persons[1].name=luna
demo.persons[1].weight=110
3.4 Collection属性
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private List<String> names;
}
绑定collection .properties的写法跟数组一样,底层代码有部分都是相同的。
3.5 Map属性
map属性的绑定,key的类型必须是能直接从string转换的,value可以通过.fieldName绑定值。
情况一:kv都能直接转换
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private Map<String, Integer> age;
}
类似上面,但是只有两种写法。
3.5.1 NO.1 [index]指定map的kv
demo.age[jack]=13
demo.age[luna\ A_-1]=14
key写在’[]‘使用’\ '转义了一下空格,map的kv是没有大小写、特殊字符限制的。
3.5.2 NO.2 .index指定map的kv
demo.age.jack=13
demo.age.luna\ A_-1=14
这种写法map的key有限制:忽略掉无效字符(字母、数字、’-'是有效字符)。
情况二:v不能直接转换
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private Map<String, Person> persons;
@Data
private static class Person {
private String name;
private Double weight;
}
}
demo.persons[jack].name=jack
demo.persons[jack].weight=120
demo.persons[luna].name=luna
demo.persons[luna].weight=110
四、properties中value到field值转换
Spring默认支持了String到一系列基础类型(Number、Date、Enum)、数组、集合、Map的转换,如何去看到底支持了哪些呢?
处理值转换的是org.springframework.boot.context.properties.bind.BindConverter
这个类,底层包装了用于处理类型转换的两个代理类
org.springframework.boot.context.properties.bind.BindConverter.TypeConverterConversionService
org.springframework.boot.convert.ApplicationConversionService
其中第一个会处理PropertyEditor逻辑,重点关注第二个。ApplicationConversionService在实例化时候默认配置了一些String到TargetType的转换,看下常用的一些转换。
4.1 基本类型
基本类型和包装类型都是支持的,Bigdecimal也是支持的。
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private int a;
private Integer a2;
private boolean b;
private Boolean b2;
private char c;
private Character c2;
private BigDecimal d;
}
demo.a=123
demo.a2=1234
demo.b=true
demo.b2=false
demo.c=a
demo.c2=A
demo.d=1.234
4.2 Enum类型
处理string到Enum转换的是LenientToEnumConverter,忽略大小写、忽略字母数字以外其他字符,不能按ordinal转换。
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private List<E> e;
public enum E {
NAME,
SPLIT_NAME,
lowercase
}
}
demo.e=NAME, NA-ME, split-name, splitname, LOWERCASE, lower&case
4.3 Date
处理String到日期的转换是org.springframework.format.support.FormattingConversionService.AnnotationParserConverter
,需要借助@DateTimeFormat指定.properties里的Date格式。
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;
}
demo.date=2022-02-02 9:00:00
4.4 数组、集合、Map
Spring在进行数据绑定时,首先会尝试从.properties里尝试获取值,如果获取到值,则通过BindConverter进行类型转换,这一步骤对于数组、集合、Map也是适应的。
- 对于数组、集合,会采用逗号分隔符分割每个元素,对每个元素进行转换。
- 对于Map,默认没有String到Map的转换,会抛异常。
如果获取不到值,对数组、集合、Map还会进行进一步处理
- 数组、集合:在fieldName后面加上[index]、.index继续获取值,对每个元素将那些转换。
- Map:.properties的kv会转换成Map的key和value。
4.5 如何扩展
BinderConverter提供的转换功能以及对数组、集合、Map的特殊处理已经能够适应绝大部分场景,仍然存在一些情况需要扩展。
- Map<Person, Person>这种key不能直接转换的类型则不能绑定
- 也没有直接从String到Map的转换
- List<List>类型
解决这些问题就需要我们自定义一些String到这些类型的Converter,而Spring在从beanFactory中获取的时候,Spring怎么知道你定义的这个Converter是为了数据绑定用的还是就是单纯的加入到beanFactory另有用处呢?所以需要@Qualifier来指明是给数据绑定用的。扩展的代码极其简单。
@Configuration
public class Config {
@Bean
@Qualifier(ConfigurationPropertiesBinding.VALUE)
public Converter<String, YourBean> converter() {
return new Converter<String, YourBean>() {
@Override
public YourBean convert(String source) {
// cvonert string to your bean
}
};
}
}
4.5.1 绑定Map<Person, Dog>属性
@ConfigurationProperties(prefix = "demo")
@Component
@Data
public class ConfigurationBindingDemo {
private Map<Person, Dog> map;
@Data
public static class Person {
private String name;
private Double weight;
}
@Data
public static class Dog {
private String name;
}
}
定义两个Converter,通过fastjson将字符串转换为DataObject
@Configuration
public class Config {
@Bean
@Qualifier(ConfigurationPropertiesBinding.VALUE)
public Converter<String, ConfigurationBindingDemo.Person> convertToPerson() {
return new Converter<String, ConfigurationBindingDemo.Person>() {
@Override
public ConfigurationBindingDemo.Person convert(String source) {
return JSON.parseObject(source, ConfigurationBindingDemo.Person.class);
}
};
}
@Bean
@Qualifier(ConfigurationPropertiesBinding.VALUE)
public Converter<String, ConfigurationBindingDemo.Dog> convertToDog() {
return new Converter<String, ConfigurationBindingDemo.Dog>() {
@Override
public ConfigurationBindingDemo.Dog convert(String source) {
return JSON.parseObject(source, ConfigurationBindingDemo.Dog.class);
}
};
}
}
.properties的key需要转义一下
demo.map[{"name"\:\ "jack",\ "weight"\:\ 111.1}]={"name": "cute"}
4.5.2 直接字符串转Map
注册一个转换到Map的Converter
@Configuration
public class Config {
@Bean
@Qualifier(ConfigurationPropertiesBinding.VALUE)
public Converter<String, Map<ConfigurationBindingDemo.Person, ConfigurationBindingDemo.Dog>> convertToPerson() {
return new Converter<String, Map<ConfigurationBindingDemo.Person, ConfigurationBindingDemo.Dog>>() {
@Override
public Map<ConfigurationBindingDemo.Person, ConfigurationBindingDemo.Dog> convert(String source) {
return JSON.parseObject(source, new TypeReference<Map<ConfigurationBindingDemo.Person, ConfigurationBindingDemo.Dog>>() {});
}
};
}
}
demo.map={{"name": "jack", "weight": 111.1}: {"name": "cute"}}
五、field值验证
对field的验证支持jsr303Validator,也可以自定义Validator
5.1 jsr303
jsr303提供了一些校验限制注解,有哪些注解可以查阅代码,并且DataObject类上有@Validated注解才会开启jsr303校验,然后将这些jsr303注解标注在DataObject内属性上即可。
@ConfigurationProperties(prefix = "demo")
@Component
@Data
@Validated
public class ConfigurationBindingDemo {
private Person person;
@Data
public static class Person {
private String name;
@Min(50)
@Max(300)
private Double weight;
}
}
demo.person.weight=49.9
5.2 自定义Validator
jsr303提供验证注解的比较有限,这时候就需要自己去定义验证规则,可以自定义一个name=configurationPropertiesValidator
的Validator。Validator接口有两个方法,supports用来判断这个validator是否能用来验证这个类,如果能,再调用validate验证这个类的target值是否合法。
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
面自定义一个注解@Odd,用于验证int或long是奇数,并且跟jsr303作用范围一致:最外层有@Validated注解才生效,主要过程如下:
- 定义一个@Odd注解。
- 定义一个Validator,递归遍历DataObject的Field和Field内的Field,对有@Odd注解的field的值拿出来,校验是否为奇数。
- 将这个validator注册到beanFactory,name=configurationPropertiesValidator
@Odd注解
@Target({FIELD})
@Retention(RUNTIME)
public @interface Odd {
}
定义Validator并注册到beanFactory
@Configuration
public class Config {
@Bean("configurationPropertiesValidator")
public Validator getValidator() {
return new OddValidator();
}
public static class OddValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAnnotationPresent(Validated.class);
}
@Override
public void validate(Object target, Errors errors) {
validate("", target, errors);
}
public void validate(String path, Object target, Errors errors) {
Class<?> targetClass = target.getClass();
for (Field field : targetClass.getDeclaredFields()) {
path = "".equals(path) ? field.getName() : path + "." + field.getName();
if (field.getAnnotation(Odd.class) != null) {
field.setAccessible(true);
Object fieldValue = ReflectionUtils.getField(field, target);
if (fieldValue != null && !validateValue(fieldValue)) {
errors.rejectValue(path, "", path + " can't be non odd, value: " + fieldValue);
}
}
Class<?> declaringClass = field.getDeclaringClass();
if (!declaringClass.isPrimitive() && !declaringClass.isArray() && !declaringClass.getName().startsWith("java")) {
field.setAccessible(true);
Object fieldValue = ReflectionUtils.getField(field, target);
if (fieldValue != null) {
validate(path, fieldValue, errors);
}
}
}
}
private boolean validateValue(Object target) {
Class<?> targetClass = target.getClass();
if (Arrays.asList(int.class, long.class).contains(targetClass)) {
long num = (long) target;
return num % 2 == 1;
}
if (Arrays.asList(Integer.class, Long.class).contains(targetClass)) {
return ((Number) target).longValue() % 2 == 1;
}
return true;
}
}
}
看一下效果,DataObject
@ConfigurationProperties(prefix = "demo")
@Component
@Data
@Validated
public class ConfigurationBindingDemo {
private Person person;
@Odd
private int num;
@Data
public static class Person {
private String name;
@Odd
private Integer weight;
}
}
demo.person.weight=50
demo.person.name=49.9
demo.num=1