在我们的日常开发中,使用@Value
来绑定配置属于非常常见的基础操作,但是这个配置注入是一次性的,简单来说就是配置一旦赋值,则不会再修改; 通常来讲,这个并没有什么问题,基础的SpringBoot项目的配置也基本不存在配置变更,如果有使用过SpringCloudConfig的小伙伴,会知道@Value
可以绑定远程配置,并支持动态刷新
接下来本文将通过一个实例来演示下,如何让@Value
注解支持配置刷新;本文将涉及到以下知识点
本项目借助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
进行开发
开一个web服务用于测试
org.springframework.boot
spring-boot-starter-web
要支持配合的动态刷新,重点在于下面两点
Environment
中的配置源相信很多小伙伴都不会去修改Environment
中的数据源,突然冒出一个让我来修改配置源的数据,还是有点懵的,这里推荐之前分享过一篇博文 SpringBoot基础篇之自定义配置源的使用姿势open in new window
当我们知道如何去自定义配置源之后,再来修改数据源,就会有一点思路了
定义一个配置文件application-dynamic.yml
xhh:
dynamic:
name: 一灰灰blog
然后在主配置文件中使用它
spring:
profiles:
active: dynamic
使用配置的java config
@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;
}
接下来进入修改配置的正题
@Autowired
ConfigurableEnvironment environment;
// --- 配置修改
String name = "applicationConfig: [classpath:/application-dynamic.yml]";
MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(name);
Map source = propertySource.getSource();
Map map = new HashMap<>(source.size());
map.putAll(source);
map.put(key, value);
environment.getPropertySources().replace(name, new MapPropertySource(name, map));
上面的实现中,有几个疑问点
上面虽然是实现了配置的修改,但是对于使用@Value
注解修饰的变量,已经被赋值了,如何能感知到配置的变更,并同步刷新呢?
这里就又可以拆分两块
我们这里额外增加了一个注解,用来修饰需要支持动态刷新的场景
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshValue {
}
接下来我们就是找出有上面这个注解的类,然后支持这些类中@Value
注解绑定的变量动态刷新
关于这个就有很多实现方式了,我们这里选择BeanPostProcessor
,bean创建完毕之后,借助反射来获取@Value
绑定的变量,并缓存起来
@Component
public class AnoValueRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements EnvironmentAware {
private Map> 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 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 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 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);
}
}
}
上面的实现虽然有点长,但是核心逻辑就下面节点
@Value
注解的变量@Value
注解中表达式,挑出变量名,用于缓存@value("hello ${name:xhh} ${now:111}
name
一个 now
Map>
从命名也可以看出,我们这里选择事件机制来实现同步,直接借助Spring Event来完成
一个简单的自定义类事件类
public static class ConfigUpdateEvent extends ApplicationEvent {
String key;
public ConfigUpdateEvent(Object source, String key) {
super(source);
this.key = key;
}
}
消费也比较简单,直接将下面这段代码,放在上面的AnoValueRefreshPostProcessor
, 接收到变更事件,通过key从缓存中找到需要变更的Field,然后依次执行刷新即可
@EventListener
public void updateConfig(ConfigUpdateEvent configUpdateEvent) {
List list = mapper.get(configUpdateEvent.key);
if (!CollectionUtils.isEmpty(list)) {
list.forEach(f -> f.updateValue(environment));
}
}
最后将前面修改配置的代码块封装一下,提供一个接口,来验证下我们的配置刷新
@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 source = propertySource.getSource();
Map 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;
}
}
本文主要通过简单的几步,对@Value
进行了拓展,支持配置动态刷新,核心知识点下面三块:
MapPropertySource
来实现配置的替换修改请注意,上面的这个实现思路,与Spring Cloud Config是有差异的,很久之前写过一个配置刷新的博文,有兴趣的小伙伴可以看一下 SpringBoot配置信息之配置刷新open in new window
配置系列博文