sunny123456

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

https://blog.csdn.net/yanluandai1985/article/details/99446060
@ConfigurationProperties注解原理与实战

一、@ConfigurationProperties 基本使用

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

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

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

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

  1. import org.springframework.boot.context.properties.ConfigurationProperties;
  2. import org.springframework.stereotype.Component;
  3. @Component
  4. @ConfigurationProperties(prefix = "config")
  5. public class TestBean{
  6. private String username;
  7. private String password;
  8. }

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

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

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

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

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

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

  1. public interface BeanPostProcessor {
  2. Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
  3. Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
  4. }

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

三、手撸代码

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

  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5. @Target({ElementType.TYPE})
  6. @Retention(RetentionPolicy.RUNTIME)
  7. public @interface Config {
  8. /**
  9. * 配置属性 的前缀
  10. */
  11. String prefix();
  12. }

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

  1. import lombok.Data;
  2. import org.springframework.stereotype.Component;
  3. @Config(prefix = "default")
  4. @Component
  5. @Data
  6. public class TestDataSource {
  7. private String username;
  8. private String password;
  9. private int maxActiveCount;
  10. }

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

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

        config.properties

  1. default.username = root
  2. default.password = 3333
  3. default.maxActiveCount = 10
  4. default.maxActiveCount2 = 10

         第四步,编写处理逻辑。

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

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

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

  1. import org.slf4j.Logger;
  2. import org.slf4j.LoggerFactory;
  3. import org.springframework.beans.BeansException;
  4. import org.springframework.beans.factory.config.BeanPostProcessor;
  5. import org.springframework.core.annotation.AnnotationUtils;
  6. import org.springframework.stereotype.Component;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.lang.reflect.Field;
  10. import java.util.*;
  11. @Component
  12. public class ConfigPostProcess implements BeanPostProcessor {
  13. private static final Logger LOGGER = LoggerFactory.getLogger(ConfigPostProcess.class);
  14. private static final String FILE_NAME = "config.properties";
  15. @Override
  16. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  17. //获取使用了我们自定义注解@Config的类
  18. Config configAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), Config.class);
  19. //如果这个对象使用了此注解
  20. if (configAnnotation != null) {
  21. LOGGER.info("当前操作的类:{}", beanName);
  22. //解析配置文件,并将解析结果放入Map中
  23. Map<String, String> configProperties = getConfigPropertiesFromFile(configAnnotation);
  24. //将对应的值,使用反射技术,注入到这个bean中
  25. bindBeanValue(bean, configProperties);
  26. }
  27. return bean;
  28. }
  29. ...
  30. }

 四、实现效果

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

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

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

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

 附录私有方法

  1. /**
  2. * 将对应的值,使用反射技术,注入到这个bean中
  3. */
  4. private void bindBeanValue(Object bean, Map<String, String> configProperties) {
  5. if (configProperties.size() > 0) {
  6. configProperties.forEach((key, value) -> {
  7. setFieldValueByFieldName(key, bean, value);
  8. });
  9. }
  10. }
  11. /**
  12. * 从配置文件中读取配置好的键值对,并放入到Map中
  13. */
  14. private Map<String, String> getConfigPropertiesFromFile(Config configAnnotation) {
  15. //get prefix from annotation
  16. String prefix = configAnnotation.prefix();
  17. //read value from resource file
  18. Properties properties = getClassNameFromResource(FILE_NAME);
  19. Map<String, String> configProperties = new HashMap<>();
  20. Set<String> keys = properties.stringPropertyNames();
  21. List<String> keyList = new ArrayList<>(keys);
  22. for (String key : keyList) {
  23. if (key.startsWith(prefix)) {
  24. //default.password ==> password
  25. String realKey = key.substring(key.indexOf(prefix) + prefix.length() + 1);
  26. String value = properties.getProperty(key);
  27. configProperties.put(realKey, value);
  28. }
  29. }
  30. return configProperties;
  31. }
  32. /**
  33. * 读取配置文件,返回一个流对象
  34. */
  35. private Properties getClassNameFromResource(String fileName) {
  36. Properties properties = new Properties();
  37. ClassLoader classLoader = ConfigPostProcess.class.getClassLoader();
  38. InputStream inputStream = classLoader.getResourceAsStream(fileName);
  39. try {
  40. properties.load(inputStream);
  41. } catch (IOException e) {
  42. e.printStackTrace();
  43. }
  44. return properties;
  45. }
  46. /**
  47. * 为指定字段使用反射技术设值(只支持String 与 int 类型)
  48. */
  49. private void setFieldValueByFieldName(String fieldName, Object object, String value) {
  50. try {
  51. Class c = object.getClass();
  52. if (checkFieldExists(fieldName, c)) {
  53. Field f = c.getDeclaredField(fieldName);
  54. f.setAccessible(true);
  55. //如果不是String,那么就是int。其它类型不支持
  56. if(f.getType().equals(String.class)){
  57. f.set(object, value);
  58. }else{
  59. int number = Integer.valueOf(value);
  60. f.set(object, number);
  61. }
  62. }
  63. } catch (Exception e) {
  64. LOGGER.error("设置" + fieldName + "出错");
  65. }
  66. }
  67. /**
  68. * 检查这个Bean是否有配置文件中配置的字段
  69. * 没有就不设置了
  70. */
  71. private boolean checkFieldExists(String fieldName, Class c) {
  72. Field[] fields = c.getDeclaredFields();
  73. for (Field field : fields) {
  74. if (field.getName().equals(fieldName)) {
  75. return Boolean.TRUE;
  76. }
  77. }
  78. return Boolean.FALSE;
  79. }

 

posted on 2022-08-10 22:13  sunny123456  阅读(320)  评论(0编辑  收藏  举报