Spring Boot @ConfigurationProper

2023-01-30  本文已影响0人  AC编程

一、如何绑定到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有以下规则:

根据以上规则,假定希望绑定属性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还会进行进一步处理

4.5 如何扩展

BinderConverter提供的转换功能以及对数组、集合、Map的特殊处理已经能够适应绝大部分场景,仍然存在一些情况需要扩展。

解决这些问题就需要我们自定义一些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注解

@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

转载自:Spring Boot @ConfigurationProperties(用法)

上一篇下一篇

猜你喜欢

热点阅读