背景
Nacos SpringBoot版本中,提供了@NacosValue
注解,支持控制臺修改值時,自動刷新,但是今天遇見了無法自動刷新的問題。
環境
SpringBoot 2.2.x
nacos-client:2.1.0
nacos-config-spring-boot-starter:0.2.12
問題排查
首先確認,nacos的配置信息:
nacos:config:bootstrap:enable: truelog-enable: falseserver-addr: xxxtype: yamlauto-refresh: truedata-ids: my-config.yml
確認auto-refresh
配置為true
,Nacos提供了注解@NacosConfigListener
可以監聽配置修改的信息,排查nacos與client的長連接通道是否正常。
@NacosConfigListener(dataId = "${nacos.config.data-ids}", timeout = 5000)
public void onConfigChange(String newConfig) {log.info("Nacos配置更新完成, config data={}", newConfig);
}
在控制臺修改配置,發現onConfigChange()可以正常觸發,說明長連通道沒有問題,看來只能追查源碼。
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) {final Listener listener = listenerWrap.listener;if (listenerWrap.inNotifying) {LOGGER.warn("[{}] [notify-currentSkip] dataId={}, group={}, md5={}, listener={}, listener is not finish yet,will try next time.",name, dataId, group, md5, listener);return;}Runnable job = () -> {long start = System.currentTimeMillis();ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();ClassLoader appClassLoader = listener.getClass().getClassLoader();try {if (listener instanceof AbstractSharedListener) {AbstractSharedListener adapter = (AbstractSharedListener) listener;adapter.fillContext(dataId, group);LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);}// Before executing the callback, set the thread classloader to the classloader of// the specific webapp to avoid exceptions or misuses when calling the spi interface in// the callback method (this problem occurs only in multi-application deployment).Thread.currentThread().setContextClassLoader(appClassLoader);ConfigResponse cr = new ConfigResponse();cr.setDataId(dataId);cr.setGroup(group);cr.setContent(content);cr.setEncryptedDataKey(encryptedDataKey);configFilterChainManager.doFilter(null, cr);String contentTmp = cr.getContent();listenerWrap.inNotifying = true;// 重點實現方法,將Nacos Server的配置寫入Spring容器中listener.receiveConfigInfo(contentTmp);// compare lastContent and contentif (listener instanceof AbstractConfigChangeListener) {Map data = ConfigChangeHandler.getInstance().parseChangeData(listenerWrap.lastContent, content, type);ConfigChangeEvent event = new ConfigChangeEvent(data);((AbstractConfigChangeListener) listener).receiveConfigChange(event);listenerWrap.lastContent = content;}.....省略部分代碼}
進入receiveConfigInfo()實現:
com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#receiveConfigInfo
@Override
public void receiveConfigInfo(String content) {// 刷新Spring容器配置onReceived(content);// 發布變更事件publishEvent(content);
}
com.alibaba.nacos.spring.core.env.NacosPropertySourcePostProcessor#addListenerIfAutoRefreshed
public static void addListenerIfAutoRefreshed(final NacosPropertySource nacosPropertySource, final Properties properties,final ConfigurableEnvironment environment) {if (!nacosPropertySource.isAutoRefreshed()) { // Disable Auto-Refreshedreturn;}final String dataId = nacosPropertySource.getDataId();final String groupId = nacosPropertySource.getGroupId();final String type = nacosPropertySource.getType();final NacosServiceFactory nacosServiceFactory = getNacosServiceFactoryBean(beanFactory);try {ConfigService configService = nacosServiceFactory.createConfigService(properties);Listener listener = new AbstractListener() {@Overridepublic void receiveConfigInfo(String config) {String name = nacosPropertySource.getName();NacosPropertySource newNacosPropertySource = new NacosPropertySource(dataId, groupId, name, config, type);newNacosPropertySource.copy(nacosPropertySource);MutablePropertySources propertySources = environment.getPropertySources();// replace NacosPropertySource// 核心實現,將Nacos的配置值刷新Spring容器中的配置值propertySources.replace(name, newNacosPropertySource);}};....省略部分代碼}
點擊replace的實現,發現一點問題:
這里面有兩個實現,其中一個是jasypt的實現,這個三方類庫是常用于對代碼中的數據庫配置信息進行加密的,難道說,是因為它?同時在控制臺也有一條日志值得注意:
[notify-error] dataId=xxx …… placeholder 'project.version' in value "${project.version}
這里是一個疑點,我們先繼續往下看:
com.alibaba.nacos.spring.context.event.config.DelegatingEventPublishingListener#receiveConfigInfo
@Override
public void receiveConfigInfo(String content) {onReceived(content);publishEvent(content);
}private void publishEvent(String content) {NacosConfigReceivedEvent event = new NacosConfigReceivedEvent(configService,dataId, groupId, content, configType);// 發布變更事件applicationEventPublisher.publishEvent(event);
}
org.springframework.context.support.AbstractApplicationContext#publishEvent
/*** Publish the given event to all listeners.* @param event the event to publish (may be an {@link ApplicationEvent}* or a payload object to be turned into a {@link PayloadApplicationEvent})* @param eventType the resolved event type, if known* @since 4.2*/protected void publishEvent(Object event, @Nullable ResolvableType eventType) {Assert.notNull(event, "Event must not be null");// Decorate event as an ApplicationEvent if necessaryApplicationEvent applicationEvent;if (event instanceof ApplicationEvent) {applicationEvent = (ApplicationEvent) event;}else {applicationEvent = new PayloadApplicationEvent<>(this, event);if (eventType == null) {eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();}}// Multicast right now if possible - or lazily once the multicaster is initializedif (this.earlyApplicationEvents != null) {this.earlyApplicationEvents.add(applicationEvent);}else {// 重點關注,發布廣播事件,通知全部監聽器getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);}// Publish event via parent context as well...if (this.parent != null) {if (this.parent instanceof AbstractApplicationContext) {((AbstractApplicationContext) this.parent).publishEvent(event, eventType);}else {this.parent.publishEvent(event);}}}
下面就是核心的實現部分,真正刷新值的實現邏輯:
com.alibaba.nacos.spring.context.annotation.config.NacosValueAnnotationBeanPostProcessor#onApplicationEvent
@Override
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 #142for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap.entrySet()) {String key = environment.resolvePlaceholders(entry.getKey());// 從Spring容器中獲取變更后的新值,通過反射的方式,更新數據String newValue = environment.getProperty(key);if (newValue == null) {continue;}List<NacosValueTarget> beanPropertyList = entry.getValue();for (NacosValueTarget target : beanPropertyList) {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);if (target.method == null) {setField(target, evaluatedValue);}else {setMethod(target, evaluatedValue);}}}}
}
OK,看到這里,基本已經明朗,了解了Nacos配置刷新的全流程,非常可疑的一點,就是三方類庫jasypt,為了驗證才想,我們將jasypt的類庫移除,再次進行嘗試,奇跡出現了!Nacos可以順利刷新配置值,終于破案,是因為jasypt的加密導致的該問題,搜了一下可能導致的原因:
1、屬性源優先級沖突:jasypt 的 PropertySource(如 EncryptablePropertySource)可能覆蓋或干擾 Nacos 的動態屬性源,導致解密后的值無法被 Nacos 更新邏輯捕獲。
2、解密邏輯未觸發:Nacos 配置更新時,新的加密值未被 jasypt 及時解密,導致 Environment 中仍是舊值。
查看一下jasypt使用的版本:
<dependency><groupId>com.github.ulisesbocchio</groupId><artifactId>jasypt-spring-boot-starter</artifactId><version>2.1.1</version>
</dependency>
猜想是否是2.1.1版本的BUG導致了該問題,于是升級至最新版本3.0.5,再次進行測試,發現Nacos可以順利更新,問題解決。