Spring中动态监听Nacos配置更新 | 您所在的位置:网站首页 › docker运行状态变更实时获取信息 › Spring中动态监听Nacos配置更新 |
需求
在上一篇文章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 实验室设备网 版权所有 |