Nacos配置无法自动刷新问题排查
Nacos配置无法自动刷新问题排查
背景
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: true
log-enable: false
server-addr: xxx
type: yaml
auto-refresh: true
data-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 content
if (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-Refreshed
return;
}
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() {
@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();
// 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 necessary
ApplicationEvent 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 initialized
if (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 #142
for (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可以顺利更新,问题解决。