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

编程

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

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

<!-- more -->

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基础系列】实现一个自定义配置加载器(应用篇) 的全部内容, 来源链接: utcz.com/z/516198.html

回到顶部