Nacos配置中心配置变更,自己编码实现自动刷新的功能

22 篇文章 0 订阅
6 篇文章 0 订阅

前言

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

在Spring Boot项目中,可以通过使用@RefreshScope注解和@NacosValue注解来实现Nacos配置中心自动刷新。 使用@RefreshScope注解是一种实现Nacos属性值自动刷新的方式。在需要动态刷新的类或方法上添加@RefreshScope注解,当Nacos上的属性值发生变化时,应用程序会自动刷新注解的类或方法中的属性值。这样就可以避免重启应用程序来应用最新的属性值。 另一种方式是使用@NacosValue注解。该注解可以直接应用于类的属性上,在属性值变化时自动刷新注解的属性。在Spring Boot项目的pom.xml文件中添加相关依赖后,需要在属性上添加@NacosValue注解,并设置autoRefreshed参数为true,以开启自动刷新功能。 示例代码如下: ```java import com.alibaba.nacos.api.config.annotation.NacosValue; import org.springframework.stereotype.Component; @Component public class MyComponent { @NacosValue(value = "${my.property}", autoRefreshed = true) private String myProperty; public String getProperty() { return myProperty; } } ``` 通过使用@RefreshScope注解和@NacosValue注解,您可以实现Nacos配置中心自动刷新,使应用程序能够在运行时动态应用最新的属性值,而无需重启应用。这样可以提高开发效率和系统的灵活性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [实现Nacos属性值自动刷新的三种方式](https://blog.csdn.net/run65536/article/details/131477092)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值