一灰灰blog 一灰灰blog
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 高可用
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)

一灰灰blog

资深搬运工
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 高可用
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)
  • 基础系列

    • 配置

      • 【基础系列】SpringBoot基础篇配置信息之如何读取配置信息
      • 【基础系列】SpringBoot基础篇配置信息之多环境配置信息
      • 【基础系列】SpringBoot基础篇配置信息之自定义配置指定与配置内引用
      • 【基础系列】SpringBoot配置信息之配置刷新
      • 【基础系列】SpringBoot配置信息之默认配置
      • 【基础系列】实现一个自定义配置加载器(应用篇)
        • I. 环境 & 方案设计
          • 1. 环境
          • 2. 方案设计
        • II. 实现
          • 1. MetaVal注解
          • 2. MetaValHolder
          • 3. MetaValueRegister 配置绑定与初始化
          • 4. MetaContainer
          • 5. Event/Listener
          • 6. bean配置
          • 6. 测试
        • II. 其他
          • 0. 项目
          • 1. 一灰灰Blog
      • 【基础系列】SpringBoot配置篇之PropertySource加载Yaml配置文件实例演示
      • 【基础系列】ConfigurationProperties配置绑定中那些你不知道的事情
      • 【基础系列】SpringBoot基础篇@Value中哪些你不知道的知识点
      • 【基础系列】SpringBoot之自定义配置源的使用姿势
      • 【基础系列】SpringBoot @Value之字面量及SpEL知识点介绍篇
      • 【基础系列】SpringBoot应用篇@Value注解支持配置自动刷新能力扩展
      • 【基础系列】基于maven多环境配置
    • AOP

    • Bean

    • SpEL

    • 事件

    • 国际化

    • 定时器

    • 日志

  • DB系列

  • 搜索系列

  • MQ系列

  • WEB系列

  • 中间件

  • 运维

  • SpringSecurity

  • SpringCloud

  • Spring系列
  • 基础系列
  • 配置
一灰灰
2020-05-07

【基础系列】实现一个自定义配置加载器(应用篇)

Spring中提供了@Value注解,用来绑定配置,可以实现从配置文件中,读取对应的配置并赋值给成员变量;某些时候,我们的配置可能并不是在配置文件中,如存在db/redis/其他文件/第三方配置服务,本文将手把手教你实现一个自定义的配置加载器,并支持@Value的使用姿势

# I. 环境 & 方案设计

# 1. 环境

  • SpringBoot 2.2.1.RELEASE
  • IDEA + JDK8

# 2. 方案设计

自定义的配置加载,有两个核心的角色

  • 配置容器 MetaValHolder:与具体的配置打交道并提供配置
  • 配置绑定 @MetaVal:类似@Value注解,用于绑定类属性与具体的配置,并实现配置初始化与配置变更时的刷新

上面@MetaVal提到了两点,一个是初始化,一个是配置的刷新,接下来可以看一下如何支持这两点

# a. 初始化

初始化的前提是需要获取到所有修饰有这个注解的成员,然后借助MetaValHolder来获取对应的配置,并初始化

为了实现上面这一点,最好的切入点是在Bean对象创建之后,获取bean的所有属性,查看是否标有这个注解,可以借助InstantiationAwareBeanPostProcessorAdapter来实现

# b. 刷新

当配置发生变更时,我们也希望绑定的属性也会随之改变,因此我们需要保存配置与bean属性之间的绑定关系

配置变更 与 bean属性的刷新 这两个操作,我们可以借助Spring的事件机制来解耦,当配置变更时,抛出一个MetaChangeEvent事件,我们默认提供一个事件处理器,用于更新通过@MetaVal注解绑定的bean属性

使用事件除了解耦之外,另一个好处是更加灵活,如支持用户对配置使用的扩展

# II. 实现

# 1. MetaVal注解

提供配置与bean属性的绑定关系,我们这里仅提供一个根据配置名获取配置的基础功能,有兴趣的小伙伴可以自行扩展支持SPEL

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MetaVal {

    /**
     * 获取配置的规则
     *
     * @return
     */
    String value() default "";

    /**
     * meta value转换目标对象;目前提供基本数据类型支持
     *
     * @return
     */
    MetaParser parser() default MetaParser.STRING_PARSER;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

请注意上面的实现,除了value之外,还有一个parser,因为我们的配置value可能是String,当然也可能是其他的基本类型如int,boolean;所以提供了一个基本的类型转换器

public interface IMetaParser<T> {
    T parse(String val);
}

public enum MetaParser implements IMetaParser {
    STRING_PARSER {
        @Override
        public String parse(String val) {
            return val;
        }
    },

    SHORT_PARSER {
        @Override
        public Short parse(String val) {
            return Short.valueOf(val);
        }
    },

    INT_PARSER {
        @Override
        public Integer parse(String val) {
            return Integer.valueOf(val);
        }
    },

    LONG_PARSER {
        @Override
        public Long parse(String val) {
            return Long.valueOf(val);
        }
    },

    FLOAT_PARSER {
        @Override
        public Object parse(String val) {
            return null;
        }
    },

    DOUBLE_PARSER {
        @Override
        public Object parse(String val) {
            return Double.valueOf(val);
        }
    },

    BYTE_PARSER {
        @Override
        public Byte parse(String val) {
            if (val == null) {
                return null;
            }
            return Byte.valueOf(val);
        }
    },

    CHARACTER_PARSER {
        @Override
        public Character parse(String val) {
            if (val == null) {
                return null;
            }
            return val.charAt(0);
        }
    },

    BOOLEAN_PARSER {
        @Override
        public Boolean parse(String val) {
            return Boolean.valueOf(val);
        }
    };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

# 2. MetaValHolder

提供配置的核心类,我们这里只定义了一个接口,具体的配置获取与业务需求相关

public interface MetaValHolder {
    /**
     * 获取配置
     *
     * @param key
     * @return
     */
    String getProperty(String key);
}
1
2
3
4
5
6
7
8
9

为了支持配置刷新,我们提供一个基于Spring事件通知机制的抽象类

public abstract class AbstractMetaValHolder implements MetaValHolder, ApplicationContextAware {

    protected ApplicationContext applicationContext;

    public void updateProperty(String key, String value) {
        String old = this.doUpdateProperty(key, value);
        this.applicationContext.publishEvent(new MetaChangeEvent(this, key, old, value));
    }

    /**
     * 更新配置
     *
     * @param key
     * @param value
     * @return
     */
    public abstract String doUpdateProperty(String key, String value);

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 3. MetaValueRegister 配置绑定与初始化

这个类,主要提供扫描所有的bean,并获取到@MetaVal修饰的属性,并初始化

public class MetaValueRegister extends InstantiationAwareBeanPostProcessorAdapter {

    private MetaContainer metaContainer;

    public MetaValueRegister(MetaContainer metaContainer) {
        this.metaContainer = metaContainer;
    }

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        processMetaValue(bean);
        return super.postProcessAfterInstantiation(bean, beanName);
    }

    /**
     * 扫描bean的所有属性,并获取@MetaVal修饰的属性
     * @param bean
     */
    private void processMetaValue(Object bean) {
        try {
            Class clz = bean.getClass();
            MetaVal metaVal;
            for (Field field : clz.getDeclaredFields()) {
                metaVal = field.getAnnotation(MetaVal.class);
                if (metaVal != null) {
                    // 缓存配置与Field的绑定关系,并初始化
                    metaContainer.addInvokeCell(metaVal, bean, field);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

请注意,上面核心点在metaContainer.addInvokeCell(metaVal, bean, field);这一行

# 4. MetaContainer

配置容器,保存配置与field映射关系,提供配置的基本操作

@Slf4j
public class MetaContainer {
    private MetaValHolder metaValHolder;

    // 保存配置与Field之间的绑定关系
    private Map<String, Set<InvokeCell>> metaCache = new ConcurrentHashMap<>();

    public MetaContainer(MetaValHolder metaValHolder) {
        this.metaValHolder = metaValHolder;
    }

    public String getProperty(String key) {
        return metaValHolder.getProperty(key);
    }

    // 用于新增绑定关系并初始化
    public void addInvokeCell(MetaVal metaVal, Object target, Field field) throws IllegalAccessException {
        String metaKey = metaVal.value();
        if (!metaCache.containsKey(metaKey)) {
            synchronized (this) {
                if (!metaCache.containsKey(metaKey)) {
                    metaCache.put(metaKey, new HashSet<>());
                }
            }
        }

        metaCache.get(metaKey).add(new InvokeCell(metaVal, target, field, getProperty(metaKey)));
    }

    // 配置更新
    public void updateMetaVal(String metaKey, String oldVal, String newVal) {
        Set<InvokeCell> cacheSet = metaCache.get(metaKey);
        if (CollectionUtils.isEmpty(cacheSet)) {
            return;
        }

        cacheSet.forEach(s -> {
            try {
                s.update(newVal);
                log.info("update {} from {} to {}", s.getSignature(), oldVal, newVal);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        });
    }

    @Data
    public static class InvokeCell {
        private MetaVal metaVal;

        private Object target;

        private Field field;

        private String signature;

        private Object value;

        public InvokeCell(MetaVal metaVal, Object target, Field field, String value) throws IllegalAccessException {
            this.metaVal = metaVal;
            this.target = target;
            this.field = field;
            field.setAccessible(true);
            signature = target.getClass().getName() + "." + field.getName();
            this.update(value);
        }

        public void update(String value) throws IllegalAccessException {
            this.value = this.metaVal.parser().parse(value);
            field.set(target, this.value);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

# 5. Event/Listener

接下来就是事件通知机制的支持了

MetaChangeEvent配置变更事件,提供基本的三个信息,配置key,原value,新value

@ToString
@EqualsAndHashCode
public class MetaChangeEvent extends ApplicationEvent {
    private static final long serialVersionUID = -9100039605582210577L;
    private String key;

    private String oldVal;

    private String newVal;


    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public MetaChangeEvent(Object source) {
        super(source);
    }

    public MetaChangeEvent(Object source, String key, String oldVal, String newVal) {
        super(source);
        this.key = key;
        this.oldVal = oldVal;
        this.newVal = newVal;
    }

    public String getKey() {
        return key;
    }

    public String getOldVal() {
        return oldVal;
    }

    public String getNewVal() {
        return newVal;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

MetaChangeListener事件处理器,刷新@MetaVal绑定的配置

public class MetaChangeListener implements ApplicationListener<MetaChangeEvent> {
    private MetaContainer metaContainer;

    public MetaChangeListener(MetaContainer metaContainer) {
        this.metaContainer = metaContainer;
    }

    @Override
    public void onApplicationEvent(MetaChangeEvent event) {
        metaContainer.updateMetaVal(event.getKey(), event.getOldVal(), event.getNewVal());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 6. bean配置

上面五步,一个自定义的配置加载器基本上就完成了,剩下的就是bean的声明

@Configuration
public class DynamicConfig {

    @Bean
    @ConditionalOnMissingBean(MetaValHolder.class)
    public MetaValHolder metaValHolder() {
        return key -> null;
    }

    @Bean
    public MetaContainer metaContainer(MetaValHolder metaValHolder) {
        return new MetaContainer(metaValHolder);
    }

    @Bean
    public MetaValueRegister metaValueRegister(MetaContainer metaContainer) {
        return new MetaValueRegister(metaContainer);
    }

    @Bean
    public MetaChangeListener metaChangeListener(MetaContainer metaContainer) {
        return new MetaChangeListener(metaContainer);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

以二方工具包方式提供外部使用,所以需要在资源目录下,新建文件META-INF/spring.factories(常规套路了)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.git.hui.boot.dynamic.config.DynamicConfig
1

# 6. 测试

上面完成基本功能,接下来进入测试环节,自定义一个配置加载

@Component
public class MetaPropertyHolder extends AbstractMetaValHolder {
    public Map<String, String> metas = new HashMap<>(8);

    {
        metas.put("name", "一灰灰");
        metas.put("blog", "https://blog.hhui.top");
        metas.put("age", "18");
    }

    @Override
    public String getProperty(String key) {
        return metas.getOrDefault(key, "");
    }

    @Override
    public String doUpdateProperty(String key, String value) {
        return metas.put(key, value);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

一个使用MetaVal的demoBean

@Component
public class DemoBean {

    @MetaVal("name")
    private String name;

    @MetaVal("blog")
    private String blog;

    @MetaVal(value = "age", parser = MetaParser.INT_PARSER)
    private Integer age;

    public String sayHello() {
        return "欢迎关注 [" + name + "] 博客:" + blog + " | " + age;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

一个简单的REST服务,用于查看/更新配置

@RestController
public class DemoAction {

    @Autowired
    private DemoBean demoBean;

    @Autowired
    private MetaPropertyHolder metaPropertyHolder;

    @GetMapping(path = "hello")
    public String hello() {
        return demoBean.sayHello();
    }

    @GetMapping(path = "update")
    public String updateBlog(@RequestParam(name = "key") String key, @RequestParam(name = "val") String val,
            HttpServletResponse response) throws IOException {
        metaPropertyHolder.updateProperty(key, val);
        response.sendRedirect("/hello");
        return "over!";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

启动类

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}
1
2
3
4
5
6
7

动图演示配置获取和刷新过程

配置刷新时,会有日志输出,如下

# II. 其他

# 0. 项目

工程源码

  • 工程:https://github.com/liuyueyi/spring-boot-demo (opens new window)
  • 源码:
    • https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config (opens new window)
    • https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config-demo (opens new window)

推荐博文

  • 【DB系列】借助Redis实现排行榜功能(应用篇) (opens new window)
  • 【DB系列】借助Redis搭建一个简单站点统计服务(应用篇) (opens new window)
  • 【WEB系列】实现后端的接口版本支持(应用篇) (opens new window)
  • 【WEB系列】徒手撸一个扫码登录示例工程(应用篇) (opens new window)
  • 【基础系列】AOP实现一个日志插件(应用篇) (opens new window)
  • 【基础系列】Bean之注销与动态注册实现服务mock(应用篇) (opens new window)
  • 【基础系列】Bean之注销与动态注册实现服务mock(应用篇) (opens new window)
  • 【基础系列】Bean之注销与动态注册实现服务mock(应用篇) (opens new window)
  • 【基础系列】Bean之注销与动态注册实现服务mock(应用篇) (opens new window)
  • 【基础系列】实现一个简单的分布式定时任务(应用篇) (opens new window)

# 1. 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 https://blog.hhui.top (opens new window)
  • 一灰灰Blog-Spring专题博客 http://spring.hhui.top (opens new window)

一灰灰blog

编辑 (opens new window)
#Value#Listener
上次更新: 2021/10/15, 19:56:22
【基础系列】SpringBoot配置信息之默认配置
【基础系列】SpringBoot配置篇之PropertySource加载Yaml配置文件实例演示

← 【基础系列】SpringBoot配置信息之默认配置 【基础系列】SpringBoot配置篇之PropertySource加载Yaml配置文件实例演示→

最近更新
01
【基础系列】基于maven多环境配置
04-25
02
【WEB系列】内嵌Tomcat配置Accesslog日志文件生成位置源码探索
04-24
03
【搜索系列】ES查询常用实例演示
04-18
更多文章>
Theme by Vdoing | Copyright © 2017-2022 一灰灰Blog
MIT License | 鄂ICP备18017282号 |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×