最近看到了一篇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
方法的时候,会调用AbstractBeanFactory
的doGetBean
方法进行创建。有兴趣的小伙伴可以打断点跑一下,思路会更清晰。
@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;
}
}
}
参考文章: