内容简介:【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)Spring 中提供了自定义的配置加载,有两个核心的角色
【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
推荐博文
1. 一灰灰 Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
-
一灰灰 Blog 个人博客 https://blog.hhui.top
-
一灰灰 Blog-Spring 专题博客 http://spring.hhui.top
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Android进阶:十三、自定义类加载器加载加密类文件
- 自定义MVC框架-自动加载类
- Android 通过DrawableInflater加载自定义Drawable 原 荐
- SpringBoot 启动分析(四) — 注解驱动的 Bean 定义加载
- Android将html数据加载到chrome自定义标签中
- Spring Boot 基础系列:实现一个自定义配置加载器(应用篇)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。