切換語言為:簡體

Nacos 在 Spring 應用中的全域性變數熱更新以及區域性 Bean 欄位熱更新

  • 爱糖宝
  • 2024-08-30
  • 2071
  • 0
  • 0

前言

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 同時開啟自動重新整理才能真正自動更新。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.