【基础系列】SpringBoot应用篇@Value注解支持配置自动刷新能力扩展

文章目录
  1. I. 项目环境
    1. 1. 项目依赖
  2. II. 配置动态刷新支持
    1. 1. 思路介绍
    2. 2. 修改配置
    3. 3. 配置同步
      1. 3.1 找出需要刷新的配置变量
      2. 3.2 修改事件同步
    4. 4. 实例演示
    5. 5.小结
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 一灰灰Blog

在我们的日常开发中,使用@Value来绑定配置属于非常常见的基础操作,但是这个配置注入是一次性的,简单来说就是配置一旦赋值,则不会再修改;
通常来讲,这个并没有什么问题,基础的SpringBoot项目的配置也基本不存在配置变更,如果有使用过SpringCloudConfig的小伙伴,会知道@Value可以绑定远程配置,并支持动态刷新

接下来本文将通过一个实例来演示下,如何让@Value注解支持配置刷新;本文将涉及到以下知识点

  • BeanPostProcessorAdapter + 自定义注解:获取支持自动刷新的配置类
  • MapPropertySource:实现配置动态变更

I. 项目环境

1. 项目依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

II. 配置动态刷新支持

1. 思路介绍

要支持配合的动态刷新,重点在于下面两点

  • 如何修改Environment中的配置源
  • 配置变更之后,如何通知到相关的类同步更新

2. 修改配置

相信很多小伙伴都不会去修改Environment中的数据源,突然冒出一个让我来修改配置源的数据,还是有点懵的,这里推荐之前分享过一篇博文 SpringBoot基础篇之自定义配置源的使用姿势

当我们知道如何去自定义配置源之后,再来修改数据源,就会有一点思路了

定义一个配置文件application-dynamic.yml

1
2
3
xhh:
dynamic:
name: 一灰灰blog

然后在主配置文件中使用它

1
2
3
spring:
profiles:
active: dynamic

使用配置的java config

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@Component
public class RefreshConfigProperties {

@Value("${xhh.dynamic.name}")
private String name;

@Value("${xhh.dynamic.age:18}")
private Integer age;

@Value("hello ${xhh.dynamic.other:test}")
private String other;
}

接下来进入修改配置的正题

1
2
3
4
5
6
7
8
9
10
11
@Autowired
ConfigurableEnvironment environment;

// --- 配置修改
String name = "applicationConfig: [classpath:/application-dynamic.yml]";
MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
Map<String, Object> source = propertySource.getSource();
Map<String, Object> map = new HashMap<>(source.size());
map.putAll(source);
map.put(key, value);
environment.getPropertySources().replace(name, new MapPropertySource(name, map));

上面的实现中,有几个疑问点

  • name如何找到的?
    • debug…
  • 配置变更
    • 注意修改配置是新建了一个Map,然后将旧的配置拷贝到新的Map,然后再执行替换;并不能直接进行修改,有兴趣的小伙伴可以实测一下为什么

3. 配置同步

上面虽然是实现了配置的修改,但是对于使用@Value注解修饰的变量,已经被赋值了,如何能感知到配置的变更,并同步刷新呢?

这里就又可以拆分两块

  • 找到需要修改的配置
  • 修改事件同步

3.1 找出需要刷新的配置变量

我们这里额外增加了一个注解,用来修饰需要支持动态刷新的场景

1
2
3
4
5
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshValue {
}

接下来我们就是找出有上面这个注解的类,然后支持这些类中@Value注解绑定的变量动态刷新

关于这个就有很多实现方式了,我们这里选择BeanPostProcessor,bean创建完毕之后,借助反射来获取@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
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
@Component
public class AnoValueRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware {
private Map<String, List<FieldPair>> mapper = new HashMap<>();
private Environment environment;

@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
processMetaValue(bean);
return super.postProcessAfterInstantiation(bean, beanName);
}

/**
* 这里主要的目的就是获取支持动态刷新的配置属性,然后缓存起来
*
* @param bean
*/
private void processMetaValue(Object bean) {
Class clz = bean.getClass();
if (!clz.isAnnotationPresent(RefreshValue.class)) {
return;
}

try {
for (Field field : clz.getDeclaredFields()) {
if (field.isAnnotationPresent(Value.class)) {
Value val = field.getAnnotation(Value.class);
List<String> keyList = pickPropertyKey(val.value(), 0);
for (String key : keyList) {
mapper.computeIfAbsent(key, (k) -> new ArrayList<>())
.add(new FieldPair(bean, field, val.value()));
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}

/**
* 实现一个基础的配置文件参数动态刷新支持
*
* @param value
* @return 提取key列表
*/
private List<String> pickPropertyKey(String value, int begin) {
int start = value.indexOf("${", begin) + 2;
if (start < 2) {
return new ArrayList<>();
}

int middle = value.indexOf(":", start);
int end = value.indexOf("}", start);

String key;
if (middle > 0 && middle < end) {
// 包含默认值
key = value.substring(start, middle);
} else {
// 不包含默认值
key = value.substring(start, end);
}

List<String> keys = pickPropertyKey(value, end);
keys.add(key);
return keys;
}

@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class FieldPair {
private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}",
":", true);

Object bean;
Field field;
String value;

public void updateValue(Environment environment) {
boolean access = field.isAccessible();
if (!access) {
field.setAccessible(true);
}

String updateVal = propertyPlaceholderHelper.replacePlaceholders(value, environment::getProperty);
try {
if (field.getType() == String.class) {
field.set(bean, updateVal);
} else {
field.set(bean, JSONObject.parseObject(updateVal, field.getType()));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
field.setAccessible(access);
}
}
}

上面的实现虽然有点长,但是核心逻辑就下面节点

  • processMetaValue():
    • 通过反射,捞取带有@Value注解的变量
  • pickPropertyKey()
    • 主要就是解析@Value注解中表达式,挑出变量名,用于缓存
    • 如: @value("hello ${name:xhh} ${now:111}
    • 解析之后,有两个变量,一个 name 一个 now
  • 缓存Map<String, List<FieldPair>>
    • 缓存的key,为变量名
    • 缓存的value,自定义类,主要用于反射修改配置值

3.2 修改事件同步

从命名也可以看出,我们这里选择事件机制来实现同步,直接借助Spring Event来完成

一个简单的自定义类事件类

1
2
3
4
5
6
7
8
public static class ConfigUpdateEvent extends ApplicationEvent {
String key;

public ConfigUpdateEvent(Object source, String key) {
super(source);
this.key = key;
}
}

消费也比较简单,直接将下面这段代码,放在上面的AnoValueRefreshPostProcessor, 接收到变更事件,通过key从缓存中找到需要变更的Field,然后依次执行刷新即可

1
2
3
4
5
6
7
@EventListener
public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
List<FieldPair> list = mapper.get(configUpdateEvent.key);
if (!CollectionUtils.isEmpty(list)) {
list.forEach(f -> f.updateValue(environment));
}
}

4. 实例演示

最后将前面修改配置的代码块封装一下,提供一个接口,来验证下我们的配置刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class DynamicRest {
@Autowired
ApplicationContext applicationContext;
@Autowired
ConfigurableEnvironment environment;
@Autowired
RefreshConfigProperties refreshConfigProperties;

@GetMapping(path = "dynamic/update")
public RefreshConfigProperties updateEnvironment(String key, String value) {
String name = "applicationConfig: [classpath:/application-dynamic.yml]";
MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
Map<String, Object> source = propertySource.getSource();
Map<String, Object> map = new HashMap<>(source.size());
map.putAll(source);
map.put(key, value);
environment.getPropertySources().replace(name, new MapPropertySource(name, map));

applicationContext.publishEvent(new AnoValueRefreshPostProcessor.ConfigUpdateEvent(this, key));
return refreshConfigProperties;
}
}

5.小结

本文主要通过简单的几步,对@Value进行了拓展,支持配置动态刷新,核心知识点下面三块:

  • 使用BeanPostProcess来扫描需要刷新的变量
  • 利用Spring Event事件机制来实现刷新同步感知
  • 至于配置的修改,则主要是MapPropertySource来实现配置的替换修改

请注意,上面的这个实现思路,与Spring Cloud Config是有差异的,很久之前写过一个配置刷新的博文,有兴趣的小伙伴可以看一下 SpringBoot配置信息之配置刷新

III. 不能错过的源码和相关知识点

0. 项目

配置系列博文

1. 一灰灰Blog

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

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

一灰灰blog


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