IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Nacos配置中心配置变更,自己编码实现自动刷新的功能 -> 正文阅读

[Java知识库]Nacos配置中心配置变更,自己编码实现自动刷新的功能

前言

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 {

   /**
    * 存放被@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已经应用到我们公司的项目中,文章中的代码都是从该项目中摘抄出来的,该项目可以直接哪来用,码字不容易,如果对你有帮助,请帮我该项目点个小星星。

Nacos配置自动刷新starter源码: https://github.com/niezhiliang/nacos-config-autofresh

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-06-16 21:31:54  更:2022-06-16 21:35:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 18:28:52-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码