前言
Nacos本身已经支持了@NacosValue的属性刷新功能,必须要在配置文件中打开自动刷新,
nacos:
config:
auto-refresh: true
还必须设置@NacosValue的属性autoRefreshed = true 默认为false,但是我们项目中使用的最多的是@Value来做占位符操作,Nacos并没有支持@Value的属性工作,工作上有个需求,需求内容如下配置中心内容变更,@Value修饰的属性也需要支持刷新值操作。
如果有只需要解决问题,不需要知道原理的同学,可以直接把该项目拿过去用,项目中也包含了测试代码,有用的话麻烦Stat一下,该项目我也会接入到我们公司的项目中,后续有问题也会进行修改。
思路
拿到需求后的想法如下:
1.首先我需要知道项目中有哪些被@Value和@NacosValue修饰的属性?
2.将这些属性和对应的bean对象缓存起来(将来属性反射赋值用)
3.如何感知Nacos配置中心配置的变更?
4.比较哪些配置发生了改变
5.拿到新的配置反射给属性
只要能解决上面这些问题,那么功能就能实现。
解决方式
如果知道项目中有哪些被@Value和@NacosValue修饰的属性?
一看到这个肯定不知道咋做,程序员不会做我们还不会抄嘛,抄袭也是一门技术活呀。不过看过Spirng源码的同学肯定知道,属性赋值的过程有一个后置处理器BeanPostProcess,肯定会用到该扩展接口,最近在看Dubbo的代码,被@Reference修饰的属性也需要被找到,我们去看它是怎么找到的,我们去借鉴借鉴(copy copy)
我们可以看到继承了AbstractAnnotationBeanPostProcessor该类,该类是阿里对spirng扩展点的再一次封装,感兴趣的同学可以去了解一下。通过借鉴我们写出来自己的类
至于为啥要实现EnvironmentAware ,因为nacos第一次是不会将内容推送过来,所以我们需要自己去拿到环境对象中的Nacos配置对象,自己解析出来,后续拿到变更后的配置,需要将两次配置进行比对,然后才知道哪个属性值变了
public class NacosConfigRefreshAnnotationPostProcess extends AbstractAnnotationBeanPostProcessor
implements EnvironmentAware {
/**
* 存放被@Value和@NacosValue修饰的属性 key = 占位符 value = 属性集合
*/
private final static Map<String, List<FieldInstance>> placeholderValueTargetMap = new HashMap<>();
// 指定注解
public NacosConfigRefreshAnnotationPostProcess() {
super(Value.class, NacosValue.class);
}
// 注解赋值
@Override
protected Object doGetInjectedBean(AnnotationAttributes annotationAttributes, Object o, String s, Class<?> aClass,
InjectionMetadata.InjectedElement injectedElement) throws Exception {
String key = (String)annotationAttributes.get("value");
// 解析出占位符的内容,这部分代码也是从spring中拿出来的,只是改了一点点
key = PlaceholderUtils.parseStringValue(key, standardEnvironment, null);
Field field = (Field)injectedElement.getMember();
// 属性对象记录到缓存中
addFieldInstance(key, field, o);
// 获取当前占位符的值
Object value = currentPlaceholderConfigMap.get(key);
// 得到的全是字符串,所以需要用到类型转换器(我们直接用spring的)
return conversionService.convert(value, field.getType());
}
// 构建缓存的key
@Override
protected String buildInjectedObjectCacheKey(AnnotationAttributes annotationAttributes, Object o, String s,
Class<?> aClass, InjectionMetadata.InjectedElement injectedElement) {
return o.getClass().getName() + "#" + injectedElement.getMember().getName();
}
// 拿到第一次Nacos的配置
@Override
public void setEnvironment(Environment environment) {
this.standardEnvironment = (StandardEnvironment)environment;
for (PropertySource<?> propertySource : standardEnvironment.getPropertySources()) {
// 筛选出nacos的配置
if (propertySource.getClass().getName().equals(NACOS_PROPERTY_SOURCE_CLASS_NAME)) {
MapPropertySource mapPropertySource = (MapPropertySource)propertySource;
// 配置以键值对形式存储到当前属性配置集合中
for (String propertyName : mapPropertySource.getPropertyNames()) {
currentPlaceholderConfigMap.put(propertyName, mapPropertySource.getProperty(propertyName));
}
}
}
}
}
将这些属性和对应的bean对象缓存起来
我们创建一个缓存集合,用来存放被注解修饰的属性对象,缓存的key我们用占位符,value的话我们自己创建了一个对象,对象属性如下:
private static class FieldInstance {
final Object bean;
final Field field;
public FieldInstance(Object bean, Field field) {
this.bean = bean;
this.field = field;
}
}
然后在我们创建的注解识别类中,将属性和bean加入到缓存对象中带入具体代码如下:
/**
* 将被@Value和@NacosValue修饰的属性,以键值对的形式存放到当前属性配置集合中
*
* @param key
* @param field
* @param bean
*/
private void addFieldInstance(String key, Field field, Object bean) {
List<FieldInstance> fieldInstances = placeholderValueTargetMap.get(key);
if (CollectionUtils.isEmpty(fieldInstances)) {
fieldInstances = new ArrayList<>();
}
fieldInstances.add(new FieldInstance(bean, field));
placeholderValueTargetMap.put(key, fieldInstances);
}
如何感知Nacos配置中心配置的变更?
这个简单,因为Nacos本身也支持值刷新的操作,配置中心的源码看了很多,感知的方式也挺多,我们用到了下面这种,监听Nacos发布事件变更的事件
@NacosConfigListener(dataId = NACOS_DATA_ID_PLACEHOLDER)
public void onChange(String newContent) throws Exception {
// 将配置内容解析成键值对
Map<String, Object> newConfigMap = parseNacosConfigContext(newContent);
try {
// 刷新变更对象的值(这里会对新老属性进行比较看哪些发生了变化)
refreshTargetObjectFieldValue(newConfigMap);
} finally {
// 当前配置指向最新的配置
currentPlaceholderConfigMap = newConfigMap;
}
}
我们只需要在注解上指定配置文件的data-id,当配置变更,事件发布者会把整个配置内容原封不动的推送过来,需要我们自己去做解析,不过也不用慌,Nacos源码中就有对应的解析代码,我们照葫芦画瓢就好,具体代码如下:
/**
* 解析nacos的配置
*
* @param newContent
* @return
* @throws Exception
*/
private Map<String, Object> parseNacosConfigContext(String newContent) throws Exception {
// 解析nacos推送的配置内容为键值对(spring环境对象)
String type = standardEnvironment.getProperty(NACOS_CONFIG_TYPE);
Map<String, Object> newConfigMap = new HashMap<>(16);
// yaml的解析
if (ConfigType.YAML.getType().equals(type)) {
newConfigMap = (new Yaml()).load(newContent);
} else if (ConfigType.PROPERTIES.getType().equals(type)) {
Properties newProps = new Properties();
newProps.load(new StringReader(newContent));
newConfigMap = new HashMap<>((Map)newProps);
}
// 筛选出正确的配置(最终的配置)
return NacosConfigPaserUtils.getFlattenedMap(newConfigMap);
}
下面这些都是用来对配置文件内容操作的,基本都是从Nacos源码中copy出来的。
/**
* @author niezhiliang
* @version v0.0.1
* @date 2022/6/8 16:12
*/
public class NacosConfigPaserUtils {
/**
* 比较两个属性,筛选出值发生变更的配置 nacos中的源码
*
* @param oldMap
* @param newMap
* @return
*/
public static Map<String, ConfigChangeItem> filterChangeData(Map oldMap, Map newMap) {
Map<String, ConfigChangeItem> result = new HashMap<>(16);
for (Iterator<Map.Entry<String, Object>> entryItr = oldMap.entrySet().iterator(); entryItr.hasNext();) {
Map.Entry<String, Object> e = entryItr.next();
ConfigChangeItem cci = null;
if (newMap.containsKey(e.getKey())) {
if (e.getValue().equals(newMap.get(e.getKey()))) {
continue;
}
cci = new ConfigChangeItem(e.getKey(), e.getValue().toString(), newMap.get(e.getKey()).toString());
cci.setType(PropertyChangeType.MODIFIED);
} else {
cci = new ConfigChangeItem(e.getKey(), e.getValue().toString(), null);
cci.setType(PropertyChangeType.DELETED);
}
result.put(e.getKey(), cci);
}
for (Iterator<Map.Entry<String, Object>> entryItr = newMap.entrySet().iterator(); entryItr.hasNext();) {
Map.Entry<String, Object> e = entryItr.next();
if (!oldMap.containsKey(e.getKey())) {
ConfigChangeItem cci = new ConfigChangeItem(e.getKey(), null, e.getValue().toString());
cci.setType(PropertyChangeType.ADDED);
result.put(e.getKey(), cci);
}
}
return result;
}
/**
* nacos中的源码
*
* @param source
* @return
*/
public static final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
Map<String, Object> result = new LinkedHashMap<>(128);
buildFlattenedMap(result, source, null);
return result;
}
/**
* nacos中的源码
*
* @param result
* @param source
* @param path
*/
private static void buildFlattenedMap(Map<String, Object> result, Map<String, Object> source, String path) {
for (Iterator<Map.Entry<String, Object>> itr = source.entrySet().iterator(); itr.hasNext();) {
Map.Entry<String, Object> e = itr.next();
String key = e.getKey();
if (org.apache.commons.lang3.StringUtils.isNotBlank(path)) {
if (e.getKey().startsWith("[")) {
key = path + key;
} else {
key = path + '.' + key;
}
}
if (e.getValue() instanceof String) {
result.put(key, e.getValue());
} else if (e.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>)e.getValue();
buildFlattenedMap(result, map, key);
} else if (e.getValue() instanceof Collection) {
@SuppressWarnings("unchecked")
Collection<Object> collection = (Collection<Object>)e.getValue();
if (collection.isEmpty()) {
result.put(key, "");
} else {
int count = 0;
for (Object object : collection) {
buildFlattenedMap(result, Collections.singletonMap("[" + (count++) + "]", object), key);
}
}
} else {
result.put(key, (e.getValue() != null ? e.getValue() : ""));
}
}
}
}
比较哪些配置发生了改变
比较新旧属性是否发送变更,Nacos源码中也有具体代码,
/**
* 刷新变更对象的值
*
* @param newConfigMap
*/
private void refreshTargetObjectFieldValue(Map<String, Object> newConfigMap) {
// 对比两次配置内容,筛选出变更后的配置项
Map<String, ConfigChangeItem> stringConfigChangeItemMap =
NacosConfigPaserUtils.filterChangeData(currentPlaceholderConfigMap, newConfigMap);
// 反射给对象赋值
for (String key : stringConfigChangeItemMap.keySet()) {
ConfigChangeItem item = stringConfigChangeItemMap.get(key);
// 嵌套占位符 防止中途嵌套中的配置变了 导致对象属性刷新失败
if (placeholderValueTargetMap.containsKey(item.getOldValue())) {
List<FieldInstance> fieldInstances = placeholderValueTargetMap.get(item.getOldValue());
placeholderValueTargetMap.put(item.getNewValue(), fieldInstances);
placeholderValueTargetMap.remove(item.getOldValue());
}
// 反射修改属性值
updateFieldValue(key, item.getNewValue(), item.getOldValue());
}
}
拿到新的配置反射给属性
前面我们已经将属性对象都拿到了,赋值交给反射就行
/**
* 反射修改变更的对象属性值
*
* @param key
* @param newValue
*/
private void updateFieldValue(String key, String newValue, String oldValue) {
List<FieldInstance> fieldInstances = placeholderValueTargetMap.get(key);
for (FieldInstance fieldInstance : fieldInstances) {
try {
ReflectionUtils.makeAccessible(fieldInstance.field);
// 类型转换
Object value = conversionService.convert(newValue, fieldInstance.field.getType());
fieldInstance.field.set(fieldInstance.bean, value);
} catch (Throwable e) {
logger.warning("Can't update value of the " + fieldInstance.field.getName() + " (field) in "
+ fieldInstance.bean.getClass().getSimpleName() + " (bean)");
}
logger.info("Nacos-config-refresh: " + fieldInstance.bean.getClass().getSimpleName() + "#"
+ fieldInstance.field.getName() + " field value changed from [" + oldValue + "] to [" + newValue + "]");
}
}
到此我们的功能也就实现完了,目前我只在springboot项目中用到了,cloud配置不一样,原理应该差不多,拿过去改改就行了。我自己也写了个starter,有需求的同学也可以拿去用,该starter已经应用到我们公司的项目中,文章中的代码都是从该项目中摘抄出来的,该项目可以直接哪来用,码字不容易,如果对你有帮助,请帮我该项目点个小星星。