https://blog.csdn.net/yanluandai1985/article/details/99446060
@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;
- }