前言
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 方法,再發布應用事件。
總結
@NacosPropertySource 的 autoRefreshed 欄位控制全域性環境變數的更新。
@NacosValue 的 autoRefreshed 欄位控制區域性 Bean 物件欄位更新。
Bean 物件欄位更新還是會受到 @NacosPropertySource 的 autoRefreshed 欄位的影響,只有 @NacosPropertySource 和 @NacosValue 同時開啟自動重新整理才能真正自動更新。