Spring中动态监听Nacos配置更新 您所在的位置:网站首页 docker运行状态变更实时获取信息 Spring中动态监听Nacos配置更新

Spring中动态监听Nacos配置更新

2023-10-25 05:41| 来源: 网络整理| 查看: 265

需求

在上一篇文章Spring中动态获取Nacos配置的几种方式中,我们介绍了动态获取Nacos配置的两种方法,然而有时候我们不只是要获取更新的配置属性,而是要监听配置文件修改事件,然后重新初始化某些类,或者做一些别的事的时候,以上的方式就不能满足了。

实现

Nacos提供了一个回调接口Listener,来帮我们实现此类需求。

使用示例 @Slf4j @Configuration @RequiredArgsConstructor public class AppConfig implements InitializingBean { private final NacosConfigManager nacosConfigManager; @Override public void afterPropertiesSet() throws Exception { AbstractConfigChangeListener listener = new AbstractConfigChangeListener() { @Override public void receiveConfigChange(ConfigChangeEvent event) { Collection changeItems = event.getChangeItems(); log.info(changeItems.toString()); } }; this.nacosConfigManager .getConfigService() .addListener("config.yml", "DEFAULT_GROUP", listener); } }

当我们在后台修改config.yml后,控制台输出:

[ConfigChangeItem{key='app.appName', oldValue='ab', newValue='ab2', type=MODIFIED}]

可以很清楚地拿到哪个key被修改了,oldValue和newValue都是什么,我们就可以根据这些信息来实现我们的需求了。

注意

在receiveConfigChange()方法里,需要手动解析配置,不能通过在外部类中使用@Value和注入Properties类来获取最新的配置,否则获取到的还是旧值,原因稍后会说明。

原理

在Nacos客户端创建时,会创建NacosConfigService对象,在NacosConfigService内部会委托ClientWorker来进行一些config client相关的操作。

其内部就执行了监听配置变更的逻辑:

@Override public void startInternal() { executor.schedule(() -> { while (!executor.isShutdown() && !executor.isTerminated()) { try { listenExecutebell.poll(5L, TimeUnit.SECONDS); if (executor.isShutdown() || executor.isTerminated()) { continue; } executeConfigListen(); } catch (Throwable e) { LOGGER.error("[ rpc listen execute ] [rpc listen] exception", e); } } }, 0L, TimeUnit.MILLISECONDS); }

executor开启了一个任务,任务内部用while来循环执行。

listenExecutebell是一个容量为1的阻塞队列,每poll一次就等待5秒,达到每5秒监听一次配置的目的。

executeConfigListen()方法内部会校验配置的md5值来判断文件是否修改过,如果修改过的话,就调用 safeNotifyListener()来「异步」通知我们注册的Listener:

listener.receiveConfigInfo(contentTmp); // compare lastContent and content if (listener instanceof AbstractConfigChangeListener) { Map data = ConfigChangeHandler.getInstance() .parseChangeData(listenerWrap.lastContent, contentTmp, type); ConfigChangeEvent event = new ConfigChangeEvent(data); ((AbstractConfigChangeListener) listener).receiveConfigChange(event); listenerWrap.lastContent = contentTmp; }

可以看到我们前面配置的AbstractConfigChangeListener在这里就被调用了。

其实前面的@Value和@ConfigurationProperties的方式也是通过Listener来实现的。

这个监听器是在NacosContextRefresher类中添加的:

private void registerNacosListener(final String groupKey, final String dataKey) { String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey); Listener listener = listenerMap.computeIfAbsent(key, lst -> new AbstractSharedListener() { @Override public void innerReceive(String dataId, String group, String configInfo) { refreshCountIncrement(); nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); applicationContext.publishEvent( new RefreshEvent(this, null, "Refresh Nacos config")); if (log.isDebugEnabled()) { log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo)); } } }); try { configService.addListener(dataKey, groupKey, listener); log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey, groupKey); } catch (NacosException e) { log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), e); } }

其监听了Spring的ApplicationReadyEvent事件,当应用程序准备就绪之后,就会触发这个事件,完成Listener的注册。

拓展

虽然我们通过Listener的回调方法拿到的ConfigChangeEvent信息已经足够详细,但是还要手动去解析配置的内容,处理起来比较麻烦。

如果在Listener收到信息之后,能够使用@Value或者注入Properties类来直接获取最新的属性就好了,就像这样:

@Override public void afterPropertiesSet() throws Exception { AbstractConfigChangeListener listener = new AbstractConfigChangeListener() { @Override public void receiveConfigChange(ConfigChangeEvent event) { log.info("app Name: {}", appProperties.getAppName()); } }; this.nacosConfigManager .getConfigService() .addListener("config.yml", "DEFAULT_GROUP", listener); }

但是测试之后发现log打印的还是旧的配置值。

这是为什么呢?

通过前面的原理我们知道了@Value和@ConfigurationProperties实现自动刷新的方式也是通过Listener来实现的,它们的监听器是在收到ApplicationReadyEvent事件之后添加的,而我们配置类的InitializingBean在这个事件之前就已经被触发了,我们添加的监听器就排在它之前,当触发配置文件修改时,我们的监听器也会先执行,这时候properties的属性都还没刷新,当然就获取不到了。

解决方案 监听ApplicationReadyEvent事件

既然你这个事件的触发比较晚,那我也监听它,并且做到比你更晚执行。

示例代码

@EventListener(ApplicationReadyEvent.class) public void refresh() throws Exception { AbstractConfigChangeListener listener = new AbstractConfigChangeListener() { @Override public void receiveConfigChange(ConfigChangeEvent event) { log.info("app Name: {}", appProperties.getAppName()); } }; this.nacosConfigManager .getConfigService() .addListener("config.yml", "DEFAULT_GROUP", listener); }

这种方式必须要使用@EventListener注解来监听事件。

通过注解监听事件,我们的事件监听就会排在Nacos的之后执行,这样我们注册的监听器也就在它之后了。

我们可以在Listener的回调中判断是否是我们需要的配置更新了,然后用properties获取最新的属性来执行逻辑。

监听RefreshScopeRefreshedEvent事件

配置属性在刷新完成之后,会发送一个RefreshScopeRefreshedEvent事件,通过监听这个事件,我们就能确保获取的是最新的属性了。

示例代码

@EventListener(RefreshScopeRefreshedEvent.class) public void refresh() throws Exception { log.info("app Name: {}", appProperties.getAppName()); }

这种方式无需向Nacos注册Listener,但是也无法判断是不是我们需要的配置更新了。适合一些配置简单的场景。

使用BeanPostProcessor(推荐)

通过BeanPostProcessor来在配置类初始化完成之后获取最新的值,这种方式最简单也最准确。

示例代码

@Slf4j @Component public class AppPropertiesBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof AppProperties properties) { log.info("post: " + properties.getAppName()); } return bean; } }


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有