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

文章目录
  1. I. 环境 & 方案设计
    1. 1. 环境
    2. 2. 方案设计
      1. a. 初始化
      2. b. 刷新
  2. II. 实现
    1. 1. MetaVal注解
    2. 2. MetaValHolder
    3. 3. MetaValueRegister 配置绑定与初始化
    4. 4. MetaContainer
    5. 5. Event/Listener
    6. 6. bean配置
    7. 6. 测试
  3. II. 其他
    1. 0. 项目
    2. 1. 一灰灰Blog

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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;所以提供了一个基本的类型转换器

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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修饰的属性,并初始化

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
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映射关系,提供配置的基本操作

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
@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

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
@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绑定的配置

1
2
3
4
5
6
7
8
9
10
11
12
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的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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(常规套路了)

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

6. 测试

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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服务,用于查看/更新配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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
@SpringBootApplication
public class Application {

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

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

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

II. 其他

0. 项目

工程源码

推荐博文

1. 一灰灰Blog

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

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

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到