前言
看到这个文章标题,也许有的看官就觉得很多余,
因为Nacos 可以设置 @NacosValue(value = "${XXX}",autoRefreshed = true) 实现动态刷新;
又因为cloud config的@RefreshScope 实现动态刷新;
还有阿波罗...等
这些玩意的原理其实都很简单,简单说 就是检测到配置文件的修改项后,发布内容变更事件,然后重新刷新绑定值。
那如果我说不准用这些东西呢?
现在就是一个老项目,不给整合这些阿猫阿狗,我想问阁下应该如何应对?
ps: 最近有个朋友在整改旧项目,做了一套小的配置中心系统,在这个配置平台系统上,通过页面能够动态修改刷新配置值。 做完后,这个朋友有些心得,想分享一下。
不多说,开搞。
正文
我们结合示例玩一下。
@Value
@ConfigurationProperties
对应代码:
@Component
public class YouInfos {
@Value("${u.infos.name}")
private String name;
@Value("${u.infos.age}")
private Integer age;
@Autowired
private Environment env;
public String getName() {
return env.getProperty("u.infos.name");
}
public Integer getAge() {
return Integer.valueOf(Objects.requireNonNull(env.getProperty("u.infos.age")));
}
public void setName(String name) {
this.name = name;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "YouInfos{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
@Component
@ConfigurationProperties(prefix = "my.infos")
public class MyInfos {
private String name;
private Integer age;
@Autowired
private Environment env;
public String getName() { return env.getProperty("my.infos.name"); }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public String toString() {
return "MyInfos{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
对应application.yml的配置(当然也可以是自己额外的配置文件值):
my: infos: name: JCccc age: 18 u: infos: name: Doli age: 25
细心的看官,看到这里,已经发现了一些不同。
是的 我把配置项的属性字段的get方法,魔改了一下,写成了重新在 Environment 再拿一次。
简单来说,我希望哪些字段属性是可以达到获取实时数据的,那我就改掉这个字段属性的get方法,让它从头再来过,去 Environment 再拿一次 自己。
接下来,就是我们怎么去改 Environment 里面的key属性值。
结合源码实操玩一下。
① 把环境配置属性拿出来
private static ConfigurableEnvironment environment;
MutablePropertySources propertySources = environment.getPropertySources();
可以看到,这8组配置属性里面, 有一组的名字包含了 application.yml 。
点进去看看是什么:
没错,就是我们的yml 的key 以及value 。
看到这, 大家思路已经比较开明了吧, 我们把这个玩意拿出来, 我们改了哪些key就对应改哪些key(当然新增了哪些,也可以对应去搞)。
看一下这个玩意MutablePropertySources 的源码:
好好好,这么玩是吧,又private 又final 。
这样的情况,我们如何应对?
那当然是最简单的暴力破解 ,反射了:
Field valueFieldOfPropertySources = MutablePropertySources.class.getDeclaredField("propertySourceList");
//设置value属性的访问权限为true
valueFieldOfPropertySources.setAccessible(true);
//获取对象上的value属性的值
List<PropertySource<?>> valueList = (List<PropertySource<?>>) valueFieldOfPropertySources.get(propertySources);
这样我们就把这八组的值拿出来了, 然后就遍历,然后把这个 OriginTrackedMapPropertySource(yml的配置值) 拿出来,改完值,再丢回去, 完事。
代码:
MyRefreshConfigUtil.java
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author JCccc 2023-08-01
*/
@Component
public class MyRefreshConfigUtil implements EnvironmentAware {
private static ConfigurableEnvironment environment;
public static void refreshValue(String key, Object newValue) {
try {
MutablePropertySources propertySources = environment.getPropertySources();
Field valueFieldOfPropertySources = MutablePropertySources.class.getDeclaredField("propertySourceList");
//设置value属性的访问权限为true
valueFieldOfPropertySources.setAccessible(true);
//获取对象上的value属性的值
List<PropertySource<?>> valueList = (List<PropertySource<?>>) valueFieldOfPropertySources.get(propertySources);
for (PropertySource<?> propertySource :valueList){
if (propertySource instanceof OriginTrackedMapPropertySource){
Map<String, Object> source = (Map<String, Object>) propertySource.getSource();
Map<String, Object> map = new HashMap<>(source.size());
map.putAll(source);
map.put(key, newValue);
environment.getPropertySources().replace(propertySource.getName(), new OriginTrackedMapPropertySource(propertySource.getName(), map));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void setEnvironment(Environment environment) {
MyRefreshConfigUtil.environment = (ConfigurableEnvironment) environment;
}
}
代码简析 :
那么我们再暴露出一个api 接口 ,满足key 和 value的实时刷新(可以写成批量,这里就简单写个单个key意思下):
@GetMapping("/doRefresh")
public String doRefresh(@RequestParam String key ,@RequestParam String value) {
MyRefreshConfigUtil.refreshValue(key, value);
return "refresh success";
}
也写个简单的获取配置值接口,看看整体的效果:
@Autowired
MyInfos myInfos;
@Autowired
YouInfos youInfos;
@GetMapping("/getInfos")
public String getInfos() {
String myName = myInfos.getName();
String yourName = youInfos.getName();
return myName+"---"+yourName;
}
服务跑起来,先看看我们的配置值:
接下来,我们去修改配置项,然后检测到了配置项哪些key做了改动(这些需要配合前端页面系统化去做比较流畅,就像是一些配置中心页面的保存按钮),然后调用我们的key刷新通知接口:
再调用看下我们的配置值:
好了,就这样吧。