在我们的日常开发中,使用@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如何找到的?
- 配置变更
- 注意修改配置是新建了一个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); }
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); } }
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():
- 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或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

打赏
如果觉得我的文章对您有帮助,请随意打赏。
微信打赏
支付宝打赏