@ConfigurationProperties注解原理与实战

一、@ConfigurationProperties 基本使用

        在 SpringBoot 中,当想需要获取到配置文件数据时,除了可以用 Spring 自带的 @Value 注解外,SpringBoot 还提供了一种更加方便的方式:@ConfigurationProperties。只要在 Bean 上添加上了这个注解,指定好配置文件的前缀,那么对应的配置文件数据就会自动填充到 Bean 中。

        比如在application.properties文件中有如下配置文件

config.username=jay.zhou
config.password=3333

        那么按照如下注解配置,SpringBoot项目中使用@ConfigurationProperties的Bean,它的username与password就会被自动注入值了。就像下面展示的那样

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "config")
public class TestBean{

    private String username;
    
    private String password;
}

二、@ConfigurationProperties 源码的探索及启发

         SpringBoot为什么能够获取到它的值呢?如果让我从无到有设计,我应该怎么设计呢?

         首先通过源码发现,SpringBoot主要帮助我们做了两件事情。

         第一件事情就是获取到使用@ConfigurationProperties的类。

         第二件事就是解析配置文件,并把对应的值设置到我们的Bean中。

         按照源码提供的实现思路,其核心就是对Bean的声明周期的管理。主要涉及一个叫做 BeanPostProcessor 的接口,可以在Bean初始化的时候,我们做一些文章。下面的两个方法,很简单,大致意思就是,在Spring的Bean初始化之前与之后执行。

public interface BeanPostProcessor {

    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

    
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}

          好了,作为程序员,看了半天源码,扯了半天淡,不如自己实现一个类似的效果来的实在。

三、手撸代码

        第一步,定义自己的注解。这个注解最后实现的功能希望与@ConfigurationProperties 类似。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {

    /**
     * 配置属性 的前缀
     */
    String prefix();
}

        第二步,定义配置测试实体。用上了我们刚才定义的@Config注解,并指明其prefix属性的值为 " default "

import lombok.Data;
import org.springframework.stereotype.Component;

@Config(prefix = "default")
@Component
@Data
public class TestDataSource {

    private String username;

    private String password;

    private int maxActiveCount;
}

        第三步,编写我们的自己的配置文件。为了便于操作(实际上其它的我不会解析),使用properties这种文件类型。

        我们的目标就是把 " default." 后面的对应的字段,注入到上面的TestDataSource里面。

        config.properties

default.username = root
default.password = 3333
default.maxActiveCount = 10
default.maxActiveCount2 = 10

         第四步,编写处理逻辑。

                 (1)获取使用了我们自定义注解@Config的类

                 (2)解析配置文件,并将对应的值使用反射技术,注入到Bean中。

         下面是最主要的处理代码。实现BeanPostProcessor接口的方法,检查每一个初始化成功的Bean,如果使用了我们的自定义注解,那么就把从配置文件中解析出来的数据,使用反射技术注入进去。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;

@Component
public class ConfigPostProcess implements BeanPostProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigPostProcess.class);

    private static final String FILE_NAME = "config.properties";

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        //获取使用了我们自定义注解@Config的类
        Config configAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), Config.class);
        //如果这个对象使用了此注解
        if (configAnnotation != null) {
            LOGGER.info("当前操作的类:{}", beanName);
            //解析配置文件,并将解析结果放入Map中
            Map<String, String> configProperties = getConfigPropertiesFromFile(configAnnotation);
            //将对应的值,使用反射技术,注入到这个bean中
            bindBeanValue(bean, configProperties);
        }

        return bean;
    }
  ...
}

 四、实现效果

        SpringBoot很多都是通过注解来实现功能的。只要我们能够学习其源码实现思路,我们也可以做出很多很多类似的功能。尽管我们的代码健壮性做不到跟大佬一样,但是在特定业务场景下,比如对某个特别重要的单例Bean进行操作,或者为某一类特定的接口实现,做一些特定的处理,可以考虑这种技术。

         另外,对于反射的使用,在SpringBoot框架学习过程中也是一个非常重要的部分。

         只有经常手敲代码,才能孰能生巧,切忌纸上谈兵。只有切实自己实现了特定功能,回头查看SpringBoot替我们做的事情,才能做到心中有数,否则被新手程序员问一句 " 为什么要这么配置 " 就哑口无言,实在是很尴尬。

         点击动图后,效果可以更加清楚哦!!!

 附录私有方法

   /**
     * 将对应的值,使用反射技术,注入到这个bean中
     */
    private void bindBeanValue(Object bean, Map<String, String> configProperties) {
        if (configProperties.size() > 0) {
            configProperties.forEach((key, value) -> {
                setFieldValueByFieldName(key, bean, value);
            });
        }
    }

    /**
     *  从配置文件中读取配置好的键值对,并放入到Map中
     */
    private Map<String, String> getConfigPropertiesFromFile(Config configAnnotation) {
        //get prefix from annotation
        String prefix = configAnnotation.prefix();

        //read value from resource file
        Properties properties = getClassNameFromResource(FILE_NAME);
        Map<String, String> configProperties = new HashMap<>();
        Set<String> keys = properties.stringPropertyNames();
        List<String> keyList = new ArrayList<>(keys);
        for (String key : keyList) {
            if (key.startsWith(prefix)) {
                //default.password ==> password
                String realKey = key.substring(key.indexOf(prefix) + prefix.length() + 1);
                String value = properties.getProperty(key);
                configProperties.put(realKey, value);
            }
        }


        return configProperties;
    }

    /**
     * 读取配置文件,返回一个流对象
     */
    private Properties getClassNameFromResource(String fileName) {
        Properties properties = new Properties();
        ClassLoader classLoader = ConfigPostProcess.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream(fileName);
        try {
            properties.load(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return properties;
    }

    /**
     * 为指定字段使用反射技术设值(只支持String 与 int 类型)
     */
    private void setFieldValueByFieldName(String fieldName, Object object, String value) {
        try {
            Class c = object.getClass();
            if (checkFieldExists(fieldName, c)) {
                Field f = c.getDeclaredField(fieldName);
                f.setAccessible(true);
                //如果不是String,那么就是int。其它类型不支持
                if(f.getType().equals(String.class)){
                    f.set(object, value);
                }else{

                    int number = Integer.valueOf(value);
                    f.set(object, number);
                }

            }
        } catch (Exception e) {
            LOGGER.error("设置" + fieldName + "出错");
        }
    }

    /**
     * 检查这个Bean是否有配置文件中配置的字段
     * 没有就不设置了
     */
    private boolean checkFieldExists(String fieldName, Class c) {
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            if (field.getName().equals(fieldName)) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

你看我都这么努力的分享知识给你了,鼓励一下又何妨O(∩_∩)O

大宇期待与你们共同进步!同时也非常感谢最近兄弟们的支持!

                    

 

posted @ 2022-07-17 12:13  小大宇  阅读(253)  评论(0编辑  收藏  举报