【中间件系列】Nacos配置热更新源码

​ 最近看到了一篇nacos-config源码分析,感觉写的挺好的,有兴趣的读者可以去看看,文章很清楚的描述了nacos配置是如何拉取,并且触发更新事件的,我就不重复制造轮子了。只是在这里做下补充。

CacheData.checkListenerMd5()

通过md5判断信息是否和内存中缓存的一致,不一致就回调监听中的回调的方法。

  void checkListenerMd5() {
        for (ManagerListenerWrap wrap : listeners) {
            // 这里比对Md5的值,如果md5不一样,则触发更新
            if (!md5.equals(wrap.lastCallMd5)) {
                safeNotifyListener(dataId, group, content, md5, wrap);
            }
        }
    }
   private void safeNotifyListener(final String dataId, final String group,
    final String content,
                                    final String md5, final ManagerListenerWrap listenerWrap) {
        final Listener listener = listenerWrap.listener;

        Runnable job = new Runnable() {
            public void run() {
                ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
                ClassLoader appClassLoader = listener.getClass().getClassLoader();
                try {
                    if (listener instanceof AbstractSharedListener) {
                        AbstractSharedListener adapter = (AbstractSharedListener)listener;
                        adapter.fillContext(dataId, group);
                        log.info(name, "[notify-context] dataId={}, group={}, md5={}", dataId, group, md5);
                    }
                    // 执行回调之前先将线程classloader设置为具体webapp的classloader,以免回调方法中调用spi接口是出现异常或错用(多应用部署才会有该问题)。
                    Thread.currentThread().setContextClassLoader(appClassLoader);

                    ConfigResponse cr = new ConfigResponse();
                    cr.setDataId(dataId);
                    cr.setGroup(group);
                    cr.setContent(content);
                    configFilterChainManager.doFilter(null, cr);
                    String contentTmp = cr.getContent();
                    // 这里触发更新事件
                    listener.receiveConfigInfo(contentTmp);
                    listenerWrap.lastCallMd5 = md5;
                    log.info(
                        name,
                        "[notify-ok] dataId={}, group={}, md5={}, listener={} ",
                        dataId, group, md5, listener);
                } catch (NacosException de) {
                    log.error(name, "NACOS-XXXX",
                        "[notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", dataId,
                        group, md5, listener, de.getErrCode(), de.getErrMsg());
                } catch (Throwable t) {
                    log.error(name, "NACOS-XXXX",
                        "[notify-error] dataId={}, group={}, md5={}, listener={} tx={}", dataId, group, md5,
                        listener, t.getCause());
                } finally {
                    Thread.currentThread().setContextClassLoader(myClassLoader);
                }
            }
        };

        final long startNotify = System.currentTimeMillis();
        try {
            if (null != listener.getExecutor()) {
                listener.getExecutor().execute(job);
            } else {
                job.run();
            }
        } catch (Throwable t) {
            log.error(
                name,
                "NACOS-XXXX",
                "[notify-error] dataId={}, group={}, md5={}, listener={} throwable={}",
                dataId, group, md5, listener, t.getCause());
        }
        final long finishNotify = System.currentTimeMillis();
        log.info(name, "[notify-listener] time cost={}ms in ClientWorker, dataId={}, group={}, md5={}, listener={} ",
            (finishNotify - startNotify), dataId, group, md5, listener);
    }

​ safeNotifyListener里面代码很多,但是如果只是关注如何进行更新的,只需要listener.receiveConfigInfo这一段即可,然后这里会跳入NacosContextRefresher.registerNacosListener方法中。

private void registerNacosListener(final String group, final String dataId) {
		Listener listener = listenerMap.computeIfAbsent(dataId, i -> new Listener() {
			@Override
			public void receiveConfigInfo(String configInfo) {
				loadCount.incrementAndGet();
				String md5 = "";
				if (!StringUtils.isEmpty(configInfo)) {
					try {
						MessageDigest md = MessageDigest.getInstance("MD5");
						md5 = new BigInteger(1, md.digest(configInfo.getBytes("UTF-8")))
								.toString(16);
					}
					catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
						LOGGER.warn("[Nacos] unable to get md5 for dataId: " + dataId, e);
					}
				}
				refreshHistory.add(dataId, md5);
                //触发applicationeven事件,到这里就算是结束了。
				applicationContext.publishEvent(
						new RefreshEvent(this, null, "Refresh Nacos config"));
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Refresh Nacos config group{},dataId{}", group, dataId);
				}
			}

			@Override
			public Executor getExecutor() {
				return null;
			}
		});
		try {
			configService.addListener(dataId, group, listener);
		}
		catch (NacosException e) {
			e.printStackTrace();
		}
	}

看到applicationContext.publishEvent之后,心也就放下来了, 到这里,基本算是结束了。

科普下applicationContext.publishEvent

​ ApplicationContext的事件机制是观察者设计模式的实现,通过 ApplicationEvent 类和 ApplicationListener 接口,可以实现 ApplicationContext 的事件处理。如果容器中有一个 ApplicationListener Bean 每当 ApplicationContext 发布 ApplicationEvent时,ApplicationListener Bean将自动触发。

​ 如此可见,上文中的new RefreshEvent(this, null, "Refresh Nacos config")可以认为是发布订阅中的类型,在此可以简单的认为是mq中的topic。只要有发布,就一定有位置订阅此topic详细的可到此

​ 那既然有发布了一个事件,哪里接收呢?RefreshEventListener对发布的RefreshEvent进行了处理。

@EventListener
	public void handle(RefreshEvent event) {
		if (this.ready.get()) { // don't handle events before app is ready
			log.debug("Event received " + event.getEventDesc());
            // 在此进行主要逻辑
			Set<String> keys = this.refresh.refresh();
			log.info("Refresh keys changed: " + keys);
		}
	}

这里看到Refresh keys changed: 这个日志打印就知道找对位置了。因为,每次发布配置更新之后,在控制台都会有此打印,说明更新了哪个key。详细的更新逻辑在this.refresh.refresh();中。

	public synchronized Set<String> refresh() {
		Map<String, Object> before = extract(
				this.context.getEnvironment().getPropertySources());
		addConfigFilesToEnvironment();
		Set<String> keys = changes(before,
				extract(this.context.getEnvironment().getPropertySources())).keySet();
		this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
        //主要是清理缓存,这里再下面说
		this.scope.refreshAll();
		return keys;
	}

这里可能会有一点晕,这段逻辑又是如何做到更新的呢?清理了缓存是什么,清理了又是如何创建bean的呢?

科普一下@RefreshScope注解了。

面试的时候有这样一个问题,spring中的bean是单例的么?或者说下spring 中bean的声明周期。spring中bean主要为单例(scope=“singleton”)和多例(scope=“prototype”)。

AbstractBeanFactory#doGetBean创建Bean实例也对于这两种模式做了特定的处理。

protected <T> T doGetBean(...){
    final RootBeanDefinition mbd = ...
    // 单例情况
    if (mbd.isSingleton()) {
        ...
    } else if (mbd.isPrototype())  // 多例情况
       ...
    } else {
    	// 其他情况
          String scopeName = mbd.getScope();
          final Scope scope = this.scopes.get(scopeName);
    		//GenericScope.get 进行处理
          Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {...});
          ...
    }
    ...
}

RefreshScope注解中清楚说明,它是@Scope("refresh"),就是上面代码的的第三种情况。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
	/**
	 * @see Scope#proxyMode()
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

GenericScope的get方法最终调用getBean方法,此方法从内存中获取信息,如果内存中有,则直接返回。所有,如果想重新生成RefreshScope标注的bean的话,只需要从内存中清理即可。

public Object getBean() {
			if (this.bean == null) {
				synchronized (this.name) {
					if (this.bean == null) {
						this.bean = this.objectFactory.getObject();
					}
				}
			}
			return this.bean;
		}

所以,在this.refresh.refresh方法中,调用this.scope.refreshAll()清理缓存。等下一次的请求有涉及到bean的时候,会进行初始化。比如如下TestController使用RefreshScope标注。通过发布修改didispace.title之后,该bean会从内存中清理。等下一次调用/test方法的时候,会调用AbstractBeanFactorydoGetBean方法进行创建。有兴趣的小伙伴可以打断点跑一下,思路会更清晰。

@SpringBootApplication
public class NacosConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(NacosConfigApplication.class, args);
    }
    @RestController
    @RefreshScope
    static class TestController {

        @Value("${didispace.title}")
        private String title;

        @GetMapping("/test")
        public String hello() {
            return title;
        }
    }
}

参考文章:

nacos-config配置源码

Spring ApplicationContext的事件机制

@RefreshScope 原理是什么

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叁滴水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值