一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码(下)

简介: 一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码(下)

首先通过 CAS 来确保该监听器方法只会被调用一次,最核心的是调用 registerNacosListenersForApplications 方法

private void registerNacosListenersForApplications() {
  if (isRefreshEnabled()) {
    // 在这里获取 NacosPropertySourceLocator#locate 方法存入的动态刷新配置文件
    for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {
      // 再次确认是否刷新
      if (!propertySource.isRefreshable()) {
        continue;
      }
      String dataId = propertySource.getDataId();
      // 注册监听器:以 dataId、group 为注册单元
      registerNacosListener(propertySource.getGroup(), dataId);
      log.info("listening config: dataId={}, group={}", dataId, propertySource.getGroup());
    }
  }
}

NacosPropertySourceLocator#locate 方法的作用已经在文章:从源码角度分析 Nacos 配置文件加载以及加载优先级 详细阐述过了,主要就是存储所有以 dataId+group 组合的所有需要动态刷新的配置文件源,这里不再做过多阐述.

上图是通过 debug 所能看到的目前应用中需要被动态刷新加载的配置文件集合

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();
            // 追加历史刷新记录,用于 endpoint 统计分析,最多存 15 条
            nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
            // 发布刷新事件,会由 RefreshEventListener 监听器处理,该监听器又会发布 EnvironmentChangeEvent 事件,重新加载所有的环境配置信息
            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 {
    // Nacos 客户端的核心方法:NacosConfigService#addListener
    configService.addListener(dataKey, groupKey, listener);
  }
  catch (NacosException e) {
    log.warn(String.format(
      "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
      groupKey), e);
  }
}

到这里会向 Nacos 客户端发起 NacosConfigService#addListener 方法调用:用于注册监听器,这里面的逻辑会牵扯到 Nacos 客户端与服务端的通信流程,不再往下剖析了,后续单独有一篇文章对这块内容进行讲解

在这里,只是往 Nacos 注册了监听器,那么后续怎么触发这个事件的调用,若这个事件被调用了,它的回调内容的处理逻辑就是 new AbstractSharedListener() { ... } 方法块的内容,先埋一个钩子,后续再针对其源码详细分析,触发事件调用有以下两种方式:

  1. 调用 Open-API 发布配置接口:https://nacos.io/zh-cn/docs/v2/guide/user/open-api.html#1.2,请求 Nacos 服务端接口:/nacos/v2/cs/config
  2. 在 Nacos dashboard 控制台上对上图贴出的图片,其中一个 dataId 配置文件随便更改一个值内容,就会触发动态刷新,最终的入口其实都是会调用 Nacos 服务端的 /nacos/v2/cs/config 接口

比如,如下配置的内容:

config:
    info: I'm cloud-3377 version 1

我将值更新为了 I'm cloud-3377 version 2 随即 Debug 断点加到监听器回调方法上

随即就看到被调用了,接下来我们就继续分析如何处理后面的工作就可以了,到这里,最开始提到的 RefreshEventListener 核心类就起作用了,在回调方法里面发布了 RefreshEvent 事件,该事件就是由此核心类来处理的.

// RefreshEventListener.java
public void onApplicationEvent(ApplicationEvent event) {
  // CAS 修改状态应用程序已准备就绪
  if (event instanceof ApplicationReadyEvent) {
    handle((ApplicationReadyEvent) event);
  }
  else if (event instanceof RefreshEvent) {
    handle((RefreshEvent) event);
  }
}
public void handle(ApplicationReadyEvent event) {
  // 更新原子状态,应用程序已经就绪
  this.ready.compareAndSet(false, true);
}
public void handle(RefreshEvent event) {
  // 防止在不处理事件之前,应用程序已经准备好了
  if (this.ready.get()) {
    log.debug("Event received " + event.getEventDesc());
    // ContextRefresher#refresh 方法
    Set<String> keys = this.refresh.refresh();
    log.info("Refresh keys changed: " + keys);
  }
}

到这里 ContextRefresher#refresh 方法的处理就极其重要了,它分别会做两件事情

  1. 刷新环境内的属性信息,用以前的属性与当前的属性进行比对;若以前的属性在当前的属性中不存在了,就设置为 null、若以前的属性在当前属性也存在,则替换旧的
  2. 销毁之前在目标 refresh Bean 加载过程创建好的所有对象
// ContextRefresher.java
public synchronized Set<String> refresh() {
  // 刷新环境内属性变量值
  Set<String> keys = refreshEnvironment();
  // RefreshScope#refreshAll
  this.scope.refreshAll();
  return keys;
}
public synchronized Set<String> refreshEnvironment() {
  // 获取环境中属性源所有 
  Map<String, Object> before = extract(
    this.context.getEnvironment().getPropertySources());
  addConfigFilesToEnvironment();
  // 进行旧、新 比对,去除新在旧不存在的,替换新在旧存在的
  Set<String> keys = changes(before,
                             extract(this.context.getEnvironment().getPropertySources())).keySet();
  // 这里发布环境更改事件,对现有的属性源 Bean 销毁后,重新进行赋值操作
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

重要的又回到了这个 RefreshScope 核心类,会调用 RefreshScope#refreshAll 方法

public void refreshAll() {
  // 调用 GenericScope 销毁方法 
  super.destroy();
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  // 清楚 ScopeCache#cache 集合里所有的元素
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  for (BeanLifecycleWrapper wrapper : wrappers) {
    try {
      Lock lock = this.locks.get(wrapper.getName()).writeLock();
      lock.lock();
      try {
        // 销毁 refresh Bean 实例
        wrapper.destroy();
      }
      finally {
        lock.unlock();
      }
    }
    catch (RuntimeException e) {
      errors.add(e);
    }
  }
  if (!errors.isEmpty()) {
    throw wrapIfNecessary(errors.get(0));
  }
  this.errors.clear();
}

到这里,所有在 refresh Bean 加载过程存入到 ScopeCache#cache 集合中的元素以及创建好的 Bean 实例全部都清除了.

refresh Bean 重新加载的过程

之前在讲解原始 Singleton Bean 加载过程时,它会创建代理对象,为拦截器链条绑定了 LockedScopedProxyFactoryBean 拦截器(advised.addAdvice(0, this) 添加到了首位)同时提到了 SimpleBeanTargetSource#getTargetObject 方法,会通过调用它来创建好一个新的 refresh Bean 实例,来完成

Refresh 动态刷新监听器 中会将创建的实例进行销毁,重新创建实例的过程会在我们调用接口获取动态绑定的属性时进行触发,可以 Debug 断点在 LockedScopedProxyFactoryBean 拦截器中,然后调用接口,如下图:

可以发现触发到顶级 CglibAopProxy 代理类后,首次执行的就是 LockedScopedProxyFactoryBean#invoke 方法,到这里,就看到查看该方法主要处理的事情

public Object invoke(MethodInvocation invocation) throws Throwable {
  Method method = invocation.getMethod();
  // equals、toString、hashCode、方法名称是 getTargetObject 跳过不作后续处理
  if (AopUtils.isEqualsMethod(method) || AopUtils.isToStringMethod(method)
      || AopUtils.isHashCodeMethod(method)
      || isScopedObjectGetTargetObject(method)) {
    return invocation.proceed();
  }
  // 获取父类 ScopedProxyFactoryBean 创建好的代理
  Object proxy = getObject();
  ReadWriteLock readWriteLock = this.scope.getLock(this.targetBeanName);
  if (readWriteLock == null) {
    if (logger.isDebugEnabled()) {
      logger.debug("For bean with name [" + this.targetBeanName
                   + "] there is no read write lock. Will create a new one to avoid NPE");
    }
    readWriteLock = new ReentrantReadWriteLock();
  }
  Lock lock = readWriteLock.readLock();
  lock.lock();
  try {
    // 满足
    if (proxy instanceof Advised) {
      Advised advised = (Advised) proxy;
      ReflectionUtils.makeAccessible(method);
      // advised.getTargetSource().getTarget():主要的入口获取 Bean
      return ReflectionUtils.invokeMethod(method,
                                          advised.getTargetSource().getTarget(),
                                          invocation.getArguments());
    }
    return invocation.proceed();
  }
  // see gh-349. Throw the original exception rather than the
  // UndeclaredThrowableException
  catch (UndeclaredThrowableException e) {
    throw e.getUndeclaredThrowable();
  }
  finally {
    lock.unlock();
  }
}

以上代码主要关注 advised.getTargetSource().getTarget,它会去调用 SimpleBeanTargetSource#getTarget 方法,如下:

public Object getTarget() throws Exception {
  return this.getBeanFactory().getBean(this.getTargetBeanName());
}

由于它的作用域是 scope,所以它最终又会调用到 GenericScope#get,最终往缓存中 ScopeCache#cache 集合中存入元素,随即重新去加载 Bean,填充最新的属性值!到这里,整个 @RefreshScope 动态刷新的过程就完成了闭环

总结

该篇文章从零到一分析完了 @RefreshScope 加载的过程,看这部分源码要有一定 Spring 源码的基础在哦,以达到首尾相连、融会贯通

从源码各个细节分解以及有对应的流程图整理,主要通过以下几个步骤来进行分析:

  1. 在处理 BeanDefinition 时它是如何去标识 scope=refresh 过程,以及 spring-cloud-context 是如何加载它去新增 scope 对象的
  2. 介绍了核心类:RefreshScope、GenericScope 如何去提前加载需要动态刷新的 Bean 以及属性的,RefreshEventListener、ContextRefresher 是如何在运行过程中完成监听器处理的流程以及动态刷新 Environment 环境变量信息的
  3. 在触发方法调用时,LockedScopedProxyFactoryBean 拦截器是如何去一步步去重新加载新的 refresh Bean 实例的

应用所有单例 Bean 加载完、应用程序准备就绪后,提前进行实例化动态刷新 Bean;在 IOC 容器中完美实现了热加载功能,每次收到监听的回调,主动去刷新最新的环境信息以及如何再次 getBean 获取最新实例的!

之前也看到很多文章分析了它的加载过程,但总是发现少了一些东西,没办法给它聚拢在一起,so 博主开始整理一篇完整的博客,从零到一深入透析😏@RefreshScope 在底层如何的给我们提供这一套机制的

撰写文章不易,对大家有帮助的,可以关注一波,后续会有更多相关的知识分享哦😯有问题的,也可以文末留言哦,看到了会及时回复的!

更多技术文章可以查看:vnjohn 个人博客

目录
相关文章
|
7月前
|
Nacos
Nacos源码构建报错程序包不存在com.alibaba.nacos.consistency.entity
Nacos源码构建报错程序包不存在com.alibaba.nacos.consistency.entity
161 0
Nacos源码构建报错程序包不存在com.alibaba.nacos.consistency.entity
|
6月前
|
Java Nacos 数据库
nacos源码打包及相关配置
nacos源码打包及相关配置
148 4
|
7月前
|
Cloud Native Java Go
解决Nacos配置刷新问题: 如何启用配置刷新功能以及与`@RefreshScope`注解的关联问题
解决Nacos配置刷新问题: 如何启用配置刷新功能以及与`@RefreshScope`注解的关联问题
623 0
|
2天前
|
关系型数据库 MySQL 数据库连接
我想问一下用nacos连接mysql数据库但要用sgjdbc连接,需要改nacos源码吗?
我想问一下用nacos连接mysql数据库但要用sgjdbc连接,需要改nacos源码吗?
66 0
|
7月前
|
Java 测试技术 Nacos
Sentinel源码改造,实现Nacos双向通信!
Sentinel源码改造,实现Nacos双向通信!
157 0
Sentinel源码改造,实现Nacos双向通信!
|
8月前
|
Java API Nacos
Nacos服务健康检查与服务变动事件发布源码解析
Nacos服务健康检查与服务变动事件发布源码解析
62 0
|
9月前
|
存储 Java Nacos
Nacos服务注册与发现源码剖析
本文通过Nacos源码了解服务注册与发现原理。
120 0
Nacos服务注册与发现源码剖析
|
2天前
|
Dubbo 关系型数据库 MySQL
nacos常见问题之命名空间配置数据上线修改如何解决
Nacos是阿里云开源的服务发现和配置管理平台,用于构建动态微服务应用架构;本汇总针对Nacos在实际应用中用户常遇到的问题进行了归纳和解答,旨在帮助开发者和运维人员高效解决使用Nacos时的各类疑难杂症。
103 1
|
2天前
|
SpringCloudAlibaba 应用服务中间件 Nacos
【微服务 SpringCloudAlibaba】实用篇 · Nacos配置中心(下)
【微服务 SpringCloudAlibaba】实用篇 · Nacos配置中心
14 0
|
2天前
|
JSON SpringCloudAlibaba Java
【微服务 SpringCloudAlibaba】实用篇 · Nacos配置中心(上)
【微服务 SpringCloudAlibaba】实用篇 · Nacos配置中心
16 1

热门文章

最新文章