banner

背景

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的实现,发现一点问题: 疑点1 这里面有两个实现,其中一个是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可以顺利更新,问题解决。