美文网首页
Spring Boot @ConfigurationProper

Spring Boot @ConfigurationProper

作者: AC编程 | 来源:发表于2023-01-30 11:22 被阅读0次

    一、如何绑定到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
    

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

    相关文章

      网友评论

          本文标题:Spring Boot @ConfigurationProper

          本文链接:https://www.haomeiwen.com/subject/phlphdtx.html