前言
Nacos本身已经支持了@NacosValue的属性刷新功能,必须要在配置文件中打开自动刷新,
nacos:
config:
auto-refresh: true
还必须设置@NacosValue的属性autoRefreshed = true 默认为false,但是我们项目中使用的最多的是@Value来做占位符操作,Nacos并没有支持@Value的属性工作,工作上有个需求,需求内容如下配置中心内容变更,@Value修饰的属性也需要支持刷新值操作。
如果有只需要解决问题,不需要知道原理的同学,可以直接把该项目拿过去用,项目中也包含了测试代码,有用的话麻烦Stat一下,该项目我也会接入到我们公司的项目中,后续有问题也会进行修改。
Nacos配置自动刷新starter源码: https://github.com/niezhiliang/nacos-config-autofresh
思路
拿到需求后的想法如下: 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 {
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");
key = PlaceholderUtils.parseStringValue(key, standardEnvironment, null);
Field field = (Field)injectedElement.getMember();
addFieldInstance(key, field, o);
Object value = currentPlaceholderConfigMap.get(key);
return conversionService.convert(value, field.getType());
}
@Override
protected String buildInjectedObjectCacheKey(AnnotationAttributes annotationAttributes, Object o, String s,
Class<?> aClass, InjectionMetadata.InjectedElement injectedElement) {
return o.getClass().getName() + "#" + injectedElement.getMember().getName();
}
@Override
public void setEnvironment(Environment environment) {
this.standardEnvironment = (StandardEnvironment)environment;
for (PropertySource<?> propertySource : standardEnvironment.getPropertySources()) {
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加入到缓存对象中带入具体代码如下:
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源码中就有对应的解析代码,我们照葫芦画瓢就好,具体代码如下:
private Map<String, Object> parseNacosConfigContext(String newContent) throws Exception {
String type = standardEnvironment.getProperty(NACOS_CONFIG_TYPE);
Map<String, Object> newConfigMap = new HashMap<>(16);
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出来的。
public class NacosConfigPaserUtils {
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;
}
public static final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
Map<String, Object> result = new LinkedHashMap<>(128);
buildFlattenedMap(result, source, null);
return result;
}
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源码中也有具体代码,
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());
}
}
拿到新的配置反射给属性
前面我们已经将属性对象都拿到了,赋值交给反射就行
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已经应用到我们公司的项目中,文章中的代码都是从该项目中摘抄出来的,该项目可以直接哪来用,码字不容易,如果对你有帮助,请帮我该项目点个小星星。
Nacos配置自动刷新starter源码: https://github.com/niezhiliang/nacos-config-autofresh
|