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

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;
}

请注意上面的实现,除了 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);
}
};
}

2. MetaValHolder

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

public interface MetaValHolder {
/**
* 获取配置
*
* @param key
* @return
*/
String getProperty(String key);
}

为了支持配置刷新,我们提供一个基于 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;
}
}

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);
}
}
}

请注意,上面核心点在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);
}
} }

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;
}
}

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());
}
}

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);
}
}

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

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

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);
}
}

一个使用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;
} }

一个简单的 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!";
}
}

启动类

@SpringBootApplication
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

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

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

II. 其他

0. 项目

工程源码

推荐博文

1. 一灰灰 Blog

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

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

【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)的更多相关文章

  1. Flex 4 自定义预加载器

    本示例的目的是在Flash Professional里创建自定义预加载器SWC,并扩展SparkDownloadProgressBar类在Flex 4应用程序中使用.    预加载器显示加载进度百分比 ...

  2. RequireJS 是一个JavaScript模块加载器

    RequireJS 是一个JavaScript模块加载器.它非常适合在浏览器中使用, 它非常适合在浏览器中使用,但它也可以用在其他脚本环境, 就像 Rhino and Node. 使用RequireJ ...

  3. Expressjs配置加载器

    有些东西就是操刀开干,没什么好解释的.... 问题引入 解决问题 直接上码 env.js index.js 使用方法 初始化 使用方法 写在最后 问题引入 大家都知道在日常的研发过程中,我们的程序会有 ...

  4. 实现一个JavaScript模块化加载器

    对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题.尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中 ...

  5. SpringBoot基础系列之自定义配置源使用姿势实例演示

    [SpringBoot基础系列]自定义配置源的使用姿势介绍 前面一篇博文介绍了一个@Value的一些知识点,其中提了一个点,@Value对应的配置,除了是配置文件中之外,可以从其他的数据源中获取么,如 ...

  6. 【模块化编程】理解requireJS-实现一个简单的模块加载器

    在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...

  7. Java实现配置加载机制

    前言 现如今几乎大多数Java应用,例如我们耳熟能详的tomcat, struts2, netty…等等数都数不过来的软件,要满足通用性,都会提供配置文件供使用者定制功能. 甚至有一些例如Netty这 ...

  8. <JVM中篇:字节码与类的加载篇>04-再谈类的加载器

    笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...

  9. KnockoutJS 3.X API 第六章 组件(5) 高级应用组件加载器

    无论何时使用组件绑定或自定义元素注入组件,Knockout都将使用一个或多个组件装载器获取该组件的模板和视图模型. 组件加载器的任务是异步提供任何给定组件名称的模板/视图模型对. 本节目录 默认组件加 ...

随机推荐

  1. "html富文本"组件:<richtext> —— 快应用原生组件

        <template> <div class="container-full"> <richtext type="html&q ...

  2. Linux C++ 网络编程学习系列(5)——多路IO之epoll边沿触发

    多路IO之epoll边沿触发+非阻塞 源码地址:https://github.com/whuwzp/linuxc/tree/master/epoll_ET_LT_NOBLOCK_example 源码说 ...

  3. 使用spring连接mysql数据库出错

    最近在学习spring框架,但是在学到JdbcTemplate时连接数据库一直报错,百度谷歌各种查找都能没有解决问题,简直要癫狂,报错信息如下: org.springframework.jdbc.Ca ...

  4. 天天在用Redis,持久化方案你又知道哪些?

    前言 文章首发于微信公众号[码猿技术专栏]:天天用Redis,持久化方案有哪些你知道吗? Redis目前已经成为主流的内存数据库了,但是大部分人仅仅是停留在会用的阶段,你真的了解Redis内部的工作原 ...

  5. substr和substring之间的区别

    substr 和 substring都是JS 截取字符串函数,两者用法很相近,下面是两者的语法很示例: substr 方法 返回一个从指定位置开始的指定长度的子字符串.stringvar.substr ...

  6. 浅析CopyOnWriteArrayList

    CopyOnWriteArrayList引入 模拟传统的ArrayList出现线程不安全的现象 public class Demo1 { public static void main(String[ ...

  7. mongodb权限篇

    1. 权限详解 内建角色: 数据库用户角色: read.readWrite: 数据库管理角色: dbAdmin.dbOwner.userAdmin: 集群管理角色: clusterAdmin.clus ...

  8. 彻底解决Python编码问题

    1. 基本概念 字符集(Character set) 解释:文字和符合的总称 常见字符集: Unicode字符集 ASCII字符集(Unicode子集) GB2312字符集 编码方法(Encoding ...

  9. 异常体系结构 throwable

    package com.yhqtv.demo01Exception; /* * 一.异常体系结构 *java.lang.Throwable * ------java.lang.Error:一般不编写针 ...

  10. 什么是CDN内容分发网络?【刘新宇】

    CDN 使用第三方OSS服务的好处是集成了CDN服务,下面来了解一下什么是CDN. CDN 全称:Content Delivery Network或Content Distribute Network ...