【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. 项目
工程源码
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源码: - https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config - https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config-demo
推荐博文
- 【DB 系列】借助 Redis 实现排行榜功能(应用篇)
- 【DB 系列】借助 Redis 搭建一个简单站点统计服务(应用篇)
- 【WEB 系列】实现后端的接口版本支持(应用篇)
- 【WEB 系列】徒手撸一个扫码登录示例工程(应用篇)
- 【基础系列】AOP 实现一个日志插件(应用篇)
- 【基础系列】Bean 之注销与动态注册实现服务 mock(应用篇)
- 【基础系列】从0到1实现一个自定义Bean注册器(应用篇)
- 【基础系列】FactoryBean及代理实现SPI机制的实例(应用篇)
- 【基础系列-实战】如何指定bean最先加载(应用篇)
- 【基础系列】实现一个简单的分布式定时任务(应用篇)
1. 一灰灰 Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰 Blog 个人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top
【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)的更多相关文章
- Flex 4 自定义预加载器
本示例的目的是在Flash Professional里创建自定义预加载器SWC,并扩展SparkDownloadProgressBar类在Flex 4应用程序中使用. 预加载器显示加载进度百分比 ...
- RequireJS 是一个JavaScript模块加载器
RequireJS 是一个JavaScript模块加载器.它非常适合在浏览器中使用, 它非常适合在浏览器中使用,但它也可以用在其他脚本环境, 就像 Rhino and Node. 使用RequireJ ...
- Expressjs配置加载器
有些东西就是操刀开干,没什么好解释的.... 问题引入 解决问题 直接上码 env.js index.js 使用方法 初始化 使用方法 写在最后 问题引入 大家都知道在日常的研发过程中,我们的程序会有 ...
- 实现一个JavaScript模块化加载器
对任何程序,都存在一个规模的问题,起初我们使用函数来组织不同的模块,但是随着应用规模的不断变大,简单的重构函数并不能顺利的解决问题.尤其对JavaScript程序而言,模块化有助于解决我们在前端开发中 ...
- SpringBoot基础系列之自定义配置源使用姿势实例演示
[SpringBoot基础系列]自定义配置源的使用姿势介绍 前面一篇博文介绍了一个@Value的一些知识点,其中提了一个点,@Value对应的配置,除了是配置文件中之外,可以从其他的数据源中获取么,如 ...
- 【模块化编程】理解requireJS-实现一个简单的模块加载器
在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...
- Java实现配置加载机制
前言 现如今几乎大多数Java应用,例如我们耳熟能详的tomcat, struts2, netty…等等数都数不过来的软件,要满足通用性,都会提供配置文件供使用者定制功能. 甚至有一些例如Netty这 ...
- <JVM中篇:字节码与类的加载篇>04-再谈类的加载器
笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...
- KnockoutJS 3.X API 第六章 组件(5) 高级应用组件加载器
无论何时使用组件绑定或自定义元素注入组件,Knockout都将使用一个或多个组件装载器获取该组件的模板和视图模型. 组件加载器的任务是异步提供任何给定组件名称的模板/视图模型对. 本节目录 默认组件加 ...
随机推荐
- tf.train.GradientDescentOptimizer 优化器
tf.train.GradientDescentOptimizer(learning_rate, use_locking=False,name='GradientDescent') 参数: learn ...
- MySQL中group_concat函数 --- 很有用的一个用来查询出所有group by 分组后所有 同组内的 内容
本文通过实例介绍了MySQL中的group_concat函数的使用方法,比如select group_concat(name) . MySQL中group_concat函数 完整的语法如下: grou ...
- 如何用 Python 绘制玫瑰图等常见疫情图
新冠疫情已经持续好几个月了,目前,我国疫情已经基本控制住了,而欧美国家正处于爆发期,我们会看到很多网站都提供了多种疫情统计图,今天我们使用 Python 的 pyecharts 框架来绘制一些比较常见 ...
- 【Java】【常用类】Object 基类 源码学习
源码总览: 有好些都是native本地方法,背后是C++写的 没有关于构造器的描述,默认编译器提供的无参构造 https://blog.csdn.net/dmw412724/article/detai ...
- Mitmproxy教程
本文是一个较为完整的 mitmproxy教程,侧重于介绍如何开发拦截脚本,帮助读者能够快速得到一个自定义的代理工具. 本文假设读者有基本的 python 知识,且已经安装好了一个 python 3 开 ...
- R语言kohonen包主要函数介绍
最近准备写一篇关于自组织映射 (Self-organizing map)的文章.SOM的代码很多,研究了一圈之后目前使用最顺手的是R语言的kohonen包. 这个kohonen包功能很丰富,但是接口不 ...
- pickle\json,configparser,hashlib模块
python常用模块 目录 python常用模块 json模块\pickle模块 configparser模块 hashlib模块 subprocess模块 json模块\pickle模块 首先说一下 ...
- vue2.x学习笔记(七)
接着前面的内容:https://www.cnblogs.com/yanggb/p/12576797.html. 条件渲染 vue也提供了一些指令,用于条件性地渲染模板中的内容. [v-if]和[v-e ...
- 面试官:兄弟,说说Java到底是值传递还是引用传递
二哥,好久没更新面试官系列的文章了啊,真的是把我等着急了,所以特意过来催催.我最近一段时间在找工作,能从二哥的文章中学到一点就多一点信心啊! 说句实在话,离读者 trust you 发给我这段信息已经 ...
- pytorch seq2seq闲聊机器人
cut_sentence.py """ 实现句子的分词 注意点: 1. 实现单个字分词 2. 实现按照词语分词 2.1 加载词典 3. 使用停用词 "" ...