nacos spring 热更新

106 阅读4分钟

前言

nacos 热更新主要分为全局环境变量热更新和局部 Bean 字段热更新,分别由 @NacosPropertySource 和 @NacosValue 的 autoRefreshed 字段控制,接下来分别看看原理。

全局环境变量热更新

全局环境变量热更新由 @NacosPropertySource 的 autoRefreshed 字段控制,接下来看看源码。

// com.alibaba.nacos.spring.core.env.NacosPropertySourcePostProcessor#doProcessPropertySource
protected void doProcessPropertySource(String beanName, BeanDefinition beanDefinition) {
        // 根据注解或者xml配置解析bean对象中配置的配置源
        List<NacosPropertySource> nacosPropertySources = buildNacosPropertySources(
                beanName, beanDefinition);

        // Add Orderly
        for (NacosPropertySource nacosPropertySource : nacosPropertySources) {
            // 将配置源添加到环境对象中
            addNacosPropertySource(nacosPropertySource);
            // 将NacosPropertySource注解中的非空字符串属性和全局配置合并返回
            Properties properties = configServiceBeanBuilder
                    .resolveProperties(nacosPropertySource.getAttributesMetadata());
            // 添加配置变更监听器
            addListenerIfAutoRefreshed(nacosPropertySource, properties, environment);
        }
    }

// com.alibaba.nacos.spring.core.env.NacosPropertySourcePostProcessor#addListenerIfAutoRefreshed
public static void addListenerIfAutoRefreshed(
            final NacosPropertySource nacosPropertySource, final Properties properties,
            final ConfigurableEnvironment environment) {
    // 如果NacosPropertySource未开启自动刷新,直接返回
        if (!nacosPropertySource.isAutoRefreshed()) { // Disable Auto-Refreshed
            return;
        }
        // 省略部分代码
        try {
            ConfigService configService = nacosServiceFactory.createConfigService(properties);
            Listener listener = new AbstractListener() {
                @Override
                public void receiveConfigInfo(String config) {
                    String name = nacosPropertySource.getName();
                    // 使用新配置数据构建配置员
                    NacosPropertySource newNacosPropertySource = new NacosPropertySource(
                            dataId, groupId, name, config, type);
                    // 拷贝旧源配置的元数据
                    newNacosPropertySource.copy(nacosPropertySource);
                    MutablePropertySources propertySources = environment
                            .getPropertySources();
                    // 替换环境对象中的源配置
                    propertySources.replace(name, newNacosPropertySource);
                }
            };
      // 省略部分代码
            configService.addListener(dataId, groupId, listener);
        }
        catch (NacosException e) {
        }
    }

从上面的代码可以看到,@NacosPropertySource 的 autoRefreshed 字段仅控制环境对象中的配置源更新。

局部 Bean 字段热更新

接下来看看局部 Bean 字段更新,局部 Bean 字段更新主要由 @NacosValue 的 autoRefreshed 字段控制,接下来同样看看源码。

// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor
@Override
public Object postProcessBeforeInitialization(Object bean, final String beanName)
        throws BeansException {
    doWithFields(bean, beanName);
    doWithMethods(bean, beanName);
    return super.postProcessBeforeInitialization(bean, beanName);
}

// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#doWithFields
private void doWithFields(final Object bean, final String beanName) {
    // 解析bean对象中字段的NacosValue注解
    ReflectionUtils.doWithFields(bean.getClass(),
            new ReflectionUtils.FieldCallback() {
                @Override
                public void doWith(Field field) throws IllegalArgumentException {
                    NacosValue annotation = getAnnotation(field, NacosValue.class);
                    doWithAnnotation(beanName, bean, annotation, field.getModifiers(),
                            null, field);
                }
            });
}

// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#doWithAnnotation
private void doWithAnnotation(String beanName, Object bean, NacosValue annotation,
            int modifiers, Method method, Field field) {
        if (annotation != null) {
          // 静态字段不处理
            if (Modifier.isStatic(modifiers)) {
                return;
            }
            // 开启自动刷新
            if (annotation.autoRefreshed()) {
              // 解析占位符
                String placeholder = resolvePlaceholder(annotation.value());
        // 将占位符,bean对象,字段信息等缓存在内存中
                NacosValueTarget nacosValueTarget = new NacosValueTarget(bean, beanName,
                        method, field, annotation.value());
                put2ListMap(placeholderNacosValueTargetMap, placeholder,
                        nacosValueTarget);
            }
        }
    }

从上述代码中可以看到,在 Bean 对象初始化前,会解析对象中添加了 @NacosValue 的字段或者方法,并将相关的字段、对象、注解信息缓存在内存中。接下来看看这些字段是如何实现热更新的。

// com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#onApplicationEvent
public void onApplicationEvent(NacosConfigReceivedEvent event) {
        // In to this event receiver, the environment has been updated the
        // latest configuration information, pull directly from the environment
        // fix issue #142
        for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap
                .entrySet()) {
            String key = environment.resolvePlaceholders(entry.getKey());
            // 从环境变量中取出,因为环境变量更新是在事件发布之前应用事件发布前完成的
            // 所以此处获取到的值是已经更新完成之后的数据
            String newValue = environment.getProperty(key);

            if (newValue == null) {
                continue;
            }
            List<NacosValueTarget> beanPropertyList = entry.getValue();
            for (NacosValueTarget target : beanPropertyList) {
              // 比较新旧数据md5校验和
                String md5String = MD5Utils.md5Hex(newValue, "UTF-8");
                boolean isUpdate = !target.lastMD5.equals(md5String);
                if (isUpdate) {
                    target.updateLastMD5(md5String);
                    Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);
                    // 更新bean对象字段或方法
                    if (target.method == null) {
                        setField(target, evaluatedValue);
                    }
                    else {
                        setMethod(target, evaluatedValue);
                    }
                }
            }
        }
    }

可以看到,当接收到配置变更事件时,会遍历内存中的需要自动更新的 Bean 字段信息,对比 MD5 校验和,如果发现存在变更,则更新 Bean 字段或方法。(此处存在一个疑问,每接收到一个事件都会遍历所有 Bean 字段信息,效率是否较低?)

这里还有一个点需要注意,新的配置数据是直接从环境对象中取出的,这也意味着 @NacosValue 字段的自动更新是会受 @NacosPropertySource 自动更新的影响的。如果 @NacosPropertySource 未开启自动更新,即使 @NacosValue 开启自动更新最终还是无法更新。

更新事件接送

接下来再来验证一下上面所说的,全局环境变量的更新是局部 Bean 字段更新之前完成的。

// com.alibaba.nacos.client.config.impl.CacheData#safeNotifyListener
private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
            final String md5, final String encryptedDataKey, final ManagerListenerWrap listenerWrap) {
  
    NotifyTask job = new NotifyTask() {
        
        @Override
        public void run() {
            long start = System.currentTimeMillis();
            ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
            ClassLoader appClassLoader = listener.getClass().getClassLoader();
            ScheduledFuture<?> timeSchedule = null;
            
            try {
                // 省略部分代码
                timeSchedule = getNotifyBlockMonitor().schedule(
                        new LongNotifyHandler(listener.getClass().getSimpleName(), dataId, group, tenant, md5,
                                notifyWarnTimeout, Thread.currentThread()), notifyWarnTimeout,
                        TimeUnit.MILLISECONDS);
                listenerWrap.inNotifying = true;
                listener.receiveConfigInfo(contentTmp);
                // 省略部分代码
            } catch (NacosException ex) {
            } catch (Throwable t) {
            } finally {
            }
        }
    };
    // 省略部分代码
}
    
// com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#receiveConfigInfo
public void receiveConfigInfo(String content) {
    onReceived(content);
    publishEvent(content);
}

// com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#publishEvent
private void publishEvent(String content) {
    NacosConfigReceivedEvent event = new NacosConfigReceivedEvent(configService,
            dataId, groupId, content, configType);
    applicationEventPublisher.publishEvent(event);
}

// com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#onReceived
private void onReceived(String content) {
        delegate.receiveConfigInfo(content);
    }

可以看到,先执行 Nacos 原生监听器的 receiveConfigInfo 方法,再发布应用事件。

总结

  1. @NacosPropertySource 的 autoRefreshed 字段控制全局环境变量的更新。
  2. @NacosValue 的 autoRefreshed 字段控制局部 Bean 对象字段更新。
  3. Bean 对象字段更新还是会受到 @NacosPropertySource 的 autoRefreshed 字段的影响,只有 @NacosPropertySource 和 @NacosValue 同时开启自动刷新才能真正自动更新。