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

      • 前言
      • ConfigurationClassPostProcessor 类准备工作处理
      • RefreshScope 类准备工作
      • 原始 singleton Bean 加载过程
      • 目标 refresh Bean 加载过程
      • Refresh 动态刷新监听器
      • refresh Bean 重新加载的过程
      • 总结

前言

源码部分涉及的版本

  • spring-boot-version:2.6.7
  • spring-cloud-version:2021.0.1.0

先从 @RefreshScope 注解源码观察,如下:

package org.springframework.cloud.context.config.annotation;
// 可标注在类以及方法上,方法上一般与 @Bean 组合
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh") // 动态刷新特有 scope 标识
@Documented
public @interface RefreshScope {
	/**
	 * 默认代理:目标类 CGLIB
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}RefreshScope

结合 Spring 生命周期来看,每个 Bean 最终的实例化、初始化都需要有 Bean 定义信息存在,才能被 Spring 所扫描后注入,它就是 BeanDefinition(简写 BD),所以在这里先要清楚在处理标注了 @RefreshScope 注解的类、方法是如何处理 BD 的,@RefreshScope 标注的 Bean,在 Spring Cloud 中它使用了类似热部署的方式,动态刷新了属性值

Spring-cloud 是以 spring-boot 基础组件进行实现的,一般都是以注解方式进行开发,之前有文章分析过注解扫描的核心类就在于 ConfigurationClassPostProcessor 中

结合 @RefreshScope 所在的 package 包名,它来自 spring-cloud-context 模块,在该模块下会自动装配下两个核心类:RefreshScope、RefreshEventListener

ConfigurationClassPostProcessor 类准备工作处理

先观察 ConfigurationClassPostProcessor、RefreshScope 类图:
一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第1张图片
一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第2张图片

从以上两张图来看,ConfigurationClassPostProcessor 实现了 BFPP、BDRPP、PriorityOrdered,RefreshScope 也实现了 BFPP、BDRPP 但它只实现了 Ordered;在以前文章讲解了 Refresh 中 invokeBeanFactoryPostProcessors 核心方法时,已经可以知道 ConfigurationClassPostProcessor 会优先加载,然后再加载 RefreshScope

PriorityOrdered > Ordered > NonOrdered

优先加载 ConfigurationClassPostProcessor 类时,扫描注解 ClassPathBeanDefinitionScanner#doScan 方法中会在内部调用 AnnotationConfigUtils#applyScopedProxyMode 方法,以下是它的源码:

static BeanDefinitionHolder applyScopedProxyMode(
  ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
  // 获取 @Scope 注解 proxyMode 属性值
  ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
  if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
    return definition;
  }
  // 属性值=目标类 情况下,调用 ScopedProxyCreator#createScopedProxy
  boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
  return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}

当前这个是当 @RefreshScope 标注在类上的情况下,还有一种标注在 @Bean 方法的场景,核心处理在 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod 方法,以下是它的部分源码:

ScopedProxyMode proxyMode = ScopedProxyMode.NO;
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class);
if (attributes != null) {
  beanDef.setScope(attributes.getString("value"));
  proxyMode = attributes.getEnum("proxyMode");
  // 获取 @Scope 注解的 proxyMode 属性值,DEFAULT 就是不代理
  if (proxyMode == ScopedProxyMode.DEFAULT) {
    proxyMode = ScopedProxyMode.NO;
  }
}

// Replace the original bean definition with the target one, if necessary
BeanDefinition beanDefToRegister = beanDef;
if (proxyMode != ScopedProxyMode.NO) {
  // 这里发现它和处理类时调用了同样的方法
  BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy(
    new BeanDefinitionHolder(beanDef, beanName), this.registry,
    proxyMode == ScopedProxyMode.TARGET_CLASS);
  beanDefToRegister = new ConfigurationClassBeanDefinition(
    (RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, beanName);
}

所以在这里要观察它是如何提前处理这些准备工作的

// ScopedProxyCreator#createScopedProxy->ScopedProxyUtils#createScopedProxy
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, BeanDefinitionRegistry registry, boolean proxyTargetClass) {
  String originalBeanName = definition.getBeanName();
  BeanDefinition targetDefinition = definition.getBeanDefinition();
  // "scopedTarget." + originalBeanName:拼接目标类的名称
  String targetBeanName = getTargetBeanName(originalBeanName);
  // 此时 proxyDefinition scope 属性就是 "" 值了,在 AbstractBeanDefinition 无参构造可以看出
  RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
  proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
  proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
  proxyDefinition.setSource(definition.getSource());
  proxyDefinition.setRole(targetDefinition.getRole());
  // proxyDefinition 动态追加一个属性 targetBeanName
  proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
  if (proxyTargetClass) {
    // 设置属性:preserveTargetClass
    targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
  } else {
    // 设置属性:proxyTargetClass
    proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
  }
  proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
  proxyDefinition.setPrimary(targetDefinition.isPrimary());
  if (targetDefinition instanceof AbstractBeanDefinition) {
    proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition)targetDefinition);
  }
  targetDefinition.setAutowireCandidate(false);
  targetDefinition.setPrimary(false);
  // 注册一个新的 BD,它是目标类
  registry.registerBeanDefinition(targetBeanName, targetDefinition);
  // 调整传递过来的 BD,它是原始类:为它修改了原有的 BeanClass 以及新增了一个属性 targetBeanName
  return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}

从以上来说确实会有点混乱,下面进行举例:

原始 Bean:AppConfig,经过 createScopedProxy 方法处理以后

1、原始 Bean:AppConfig,会追加一个 targetBeanName 属性,它的属性值就是 scopedTarget.appConfig,同时修改原始 Bean 的 beanClass 为 ScopedProxyFactoryBean 后返回

2、第一点是原始 Bean 处理过后的信息会返回,然后以上源码会新增一个 BD,它把原始 Bean 所有信息都赋值了过来,但它的 beanName 不再是以前的 appConfig 了,而是变成了 scopedTarget.appConfig

好,到这里 ConfigurationClassPostProcessor 处理 @RefreshScope 注解的工作已经完成了,关于 ConfigurationClassPostProceessor 更多功能的源码介绍,可以阅读该文章:Spring 核心类 ConfigurationClassPostProcessor 流程讲解及源码全面分析

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第3张图片

RefreshScope 类准备工作

在 Spring 中,scope 只会存在这几种作用域,如下图:

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第4张图片

那么它是如何去处理 scope=refresh 这种作用域的呢?

从以上的 RefreshScope 类图可以看出,它继承了 GenericScope 类,而它的父类实现了 BFPP、BDRPP 接口,它会和 ConfigurationClassPostProcessor 同时处理,只不过在它的后面,所以在这里就需要看 GenericScope 类是如何处理这两个方法的呢?

BDRPP#postProcessBeanDefinitionRegistry 方法会优先加载,源码如下:

public RefreshScope() {
  super.setName("refresh");
}

// GenericScope.java
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
  throws BeansException {
  for (String name : registry.getBeanDefinitionNames()) {
    BeanDefinition definition = registry.getBeanDefinition(name);
    if (definition instanceof RootBeanDefinition) {
      RootBeanDefinition root = (RootBeanDefinition) definition;
      // root.getBeanClass() == ScopedProxyFactoryBean.class 条件就说明了是原始 BD
      if (root.getDecoratedDefinition() != null && root.hasBeanClass()
          && root.getBeanClass() == ScopedProxyFactoryBean.class) {
        // getName()=refresh,BD 的 DecoratedDefinition 属性是目标 BD,那么此时肯定是满足条件的
        if (getName().equals(root.getDecoratedDefinition().getBeanDefinition().getScope())) {
          // 把 BeanClass 重新调整为 LockedScopedProxyFactoryBean 
          root.setBeanClass(LockedScopedProxyFactoryBean.class);
          root.getConstructorArgumentValues().addGenericArgumentValue(this);
          root.setSynthetic(true);
        }
      }
    }
  }
}

BFPP#postProcessBeanFactory 方法会后面才加载,源码如下:

// GenericScope.java
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
  throws BeansException {
  this.beanFactory = beanFactory;
  // 当前的 this.name 是 Generic,但它的子类构造方法将它的 name 属性修改为了 refresh
  // registerScope:代表 AbstractBeanFactory#scopes 会新增一个元素:key=refresh、value=RefreshScope.class
  beanFactory.registerScope(this.name, this);
  setSerializationId(beanFactory);
}

这些 BD 信息准备工作作好了,后续就是这些 Bean 进行加载了.

原始 singleton Bean 加载过程

从 ScopedProxyUtils#createScopedProxy 方法中可以得知原始 Bean scope 属性是 “” 值,而目标 Bean scope 属性继承了原始 Bean 未改变之前的值,也就是 refresh

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第5张图片

完成 Bean 单例 Bean 加载的过程是在 DefaultListableBeanFactory#preInstantiateSingletons 方法中完成的,该方法中部分源码如下:

// 当前方法的判别表明了原始 Bean 就是单例的,而目标 Bean 是非单例的
public boolean isSingleton() {
  return "singleton".equals(this.scope) || "".equals(this.scope);
}
// 非抽象的、单例的、非懒加载的,只有满足这三个条件才会继续往下面去 getBean->doGetBean->createBean->doCreateBean
} while(bd.isAbstract());
} while(!bd.isSingleton());
} while(bd.isLazyInit());

// 此处原始 Bean 就是一个 FactoryBean 了,所以先 getBean 创建 FactoryBean 接口的实例
if (this.isFactoryBean(beanName)) {
  bean = this.getBean("&" + beanName);
  break;
}
// 这里就会去触发调用 FactoryBean#getObject 方法获取里面的实例
this.getBean(beanName);

在以上中为什么说原始 Bean 已经是一个 FactoryBean 对象了呢?

因为在前面执行 GenericScope#postProcessBeanDefinitionRegistry 时,为原始 Bean 重新赋值了 beanClass 属性,对应的值就是 LockedScopedProxyFactoryBean

查看 LockedScopedProxyFactoryBean 类图即可明白,如下所示:

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第6张图片

从继承关系来看,当前类就实现了 FactoryBean 接口,同时它也实现了 MethodInterceptor 接口(这里埋一个引子,后续生成代理对象以后就会调用 LockedScopedProxyFactoryBean#invoke 方法)

说到了 FactoryBean,那必然就需要看它的 getObject、isSingleton 方法,但是在调用 getObject 方法在实际应用时是通过手动去调用的,在这边创建先要创建 FactoryBean 实例,它必然会经过填充属性(populateBean)->初始化 Bean(initializeBean) 这个过程的

再观察类图,它实现了 BeanFactoryAware 方法, 同时在初始化 Bean(initializeBean) 会调用 invokeAwareMethods 方法,它会处理三个 Aware 接口的方法:BeanNameAware#setBeanName、BeanClassLoaderAware#setBeanClassLoader、BeanFactoryAware#setBeanFactory,在这里只有一个 Aware 方法满足,所以接下来观察它的 setBeanFactory 作了什么处理

// LockedScopedProxyFactoryBean.java
public void setBeanFactory(BeanFactory beanFactory) {
  // 调用父类的 setBeanFactory 方法,主要的处理也是在它的父方法中
  super.setBeanFactory(beanFactory);
  // 获取父类创建好的代理对象
  Object proxy = getObject();
  if (proxy instanceof Advised) {
    // 将父类创建好的代理对象被优先加载调用
    Advised advised = (Advised) proxy;
    advised.addAdvice(0, this);
  }
}

// LockedScopedProxyFactoryBean 父类 ScopedProxyFactoryBean
private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();

public ScopedProxyFactoryBean() {
  // proxyTargetClass=true
  this.setProxyTargetClass(true);
}

public void setTargetBeanName(String targetBeanName) {
  this.targetBeanName = targetBeanName;
  // 在填充属性阶段同时为 scopedTargetSource 设置了目标名称
  this.scopedTargetSource.setTargetBeanName(targetBeanName);
}

public void setBeanFactory(BeanFactory beanFactory) {
  if (!(beanFactory instanceof ConfigurableBeanFactory)) {
    throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
  } else {
    ConfigurableBeanFactory cbf = (ConfigurableBeanFactory)beanFactory;
    this.scopedTargetSource.setBeanFactory(beanFactory);
    // 创建动态代理的核心类
    ProxyFactory pf = new ProxyFactory();
    pf.copyFrom(this);
    // 设置目前源对象类:SimpleBeanTargetSource
    pf.setTargetSource(this.scopedTargetSource);
    Assert.notNull(this.targetBeanName, "Property 'targetBeanName' is required");
    Class<?> beanType = beanFactory.getType(this.targetBeanName);
    if (beanType == null) {
      throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName + "': Target type could not be determined at the time of proxy creation.");
    } else {
      // proxyTargetClass=true,无参构造方法设置过了
      if (!this.isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
        pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
      }
			// 准备好,后续拦截器会调用 SimpleBeanTargetSource#getTargetObject 方法
      ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
      pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));
      pf.addInterface(AopInfrastructureBean.class);
      this.proxy = pf.getProxy(cbf.getBeanClassLoader());
    }
  }
}

// 子类手动调用
public Object getObject() {
  if (this.proxy == null) {
    throw new FactoryBeanNotInitializedException();
  } else {
    return this.proxy;
  }
}

这里先记录一下,后面阶段会调用此处:ProxyFactory 代理工厂类设置了目标源对象 SimpleBeanTargetSource,该目标源对象在填充属性阶段设置了 targetBeanName 值

但是你没想到吧,到这里,还是它动态刷新的准备工作,这里只是把原始 Bean 加载完成,目标 Bean 还没有进行处理呢!!!

目标 refresh Bean 加载过程

刚刚说到了,目标 Bean 是非单例的,它的 scope 属性值为 refresh,所以说在执行 DefaultListableBeanFactory#preInstantiateSingletons 是不会对它作任何处理工作,除非我们绕开这个方法,直接调用 getBean 方法就可以获取实例对象了,那么下面就看 spring-cloud 是如何应用 spring-boot 组件来完成这个骚操作的呢?

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第7张图片

重要的点还是在 RefreshScope 类里面的方法中,这里再把它的类图贴出来:

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第8张图片

发现了它实现了 ApplicationListener 接口,它肯定是监听了某个事件,事件名:ContextRefreshedEvent,并且它肯定实现了 onApplicationEvent 方法

// RefreshScope.java
public void onApplicationEvent(ContextRefreshedEvent event) {
  start(event);
}

public void start(ContextRefreshedEvent event) {
  // eager 默认值就是 true
  if (event.getApplicationContext() == this.context && this.eager
      && this.registry != null) {
    eagerlyInitialize();
  }
}

// 提前初始化
private void eagerlyInitialize() {
  for (String name : this.context.getBeanDefinitionNames()) {
    BeanDefinition definition = this.registry.getBeanDefinition(name);
    // scope 属性值=当前 name 也就是 refresh、lazyInit 属性为设置的话默认就是 false
    if (this.getName().equals(definition.getScope())
        && !definition.isLazyInit()) {
      // 此处就是直接绕过了,调用 getBean 方法
      Object bean = this.context.getBean(name);
      if (bean != null) {
        bean.getClass();
      }
    }
  }
}

先整理一下 spring-boot 是在什么发送 ContextRefreshedEvent 这个事件的呢?

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第9张图片

其实就是在执行完 DefaultListableBeanFactory#preInstantiateSingletons 方法以后,马上就调用 finishRefresh 方法随即就发送事件啦!!

getBean->doGetBean 方法,该方法内有这么个调用逻辑,贴上部分 doGetBean 方法的源码:

// isSingleton & isPrototype 都不满足,所以就只需要关注 else 分支的逻辑了
if (mbd.isSingleton()) {
  .....
} else if (mbd.isPrototype() {
  .....
} else {
  String scopeName = mbd.getScope();
  if (!StringUtils.hasLength(scopeName)) {
    throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
  }
  // scopeName=refresh,scopes 集合中在之前 GenericScope#postProcessBeanFactory 方法已经添加进去了
  Scope scope = this.scopes.get(scopeName);
  if (scope == null) {
    throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
  }
  try {
    // 核心方法就是这个了
    Object scopedInstance = scope.get(beanName, () -> {
      beforePrototypeCreation(beanName);
      try {
        return createBean(beanName, mbd, args);
      }
      finally {
        afterPrototypeCreation(beanName);
      }
    });
    bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
  }
  catch (IllegalStateException ex) {
    throw new BeanCreationException(beanName,
                                    "Scope '" + scopeName + "' is not active for the current thread; consider " +
                                    "defining a scoped proxy for this bean if you intend to refer to it from a singleton",
                                    ex);
  }
}

Scope#get 方法,Scope 是一个顶级接口,当然是找它的子类 GenericScope、RefreshScope 了,而 RefreshScope 并没有实现这个方法,所以直接定位到 GenericScope#get 方法,源码如下:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 存入 ScopeCache#cache 集合中
  BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 这里调用的是上面 lambda 表达式中的 createBean 方法
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

BeanLifecycleWrapper 是用于存储缓存的,cache 是一个 ConcurrentMap 结构,创建的这个对象实例就是一个普通的对象,不会做任何的代理增强处理

Refresh 动态刷新监听器

结合前言中提到:@RefreshScope 所在的 package 包名,在该模块下会自动装配下两个核心类:RefreshScope、RefreshEventListener,前面的内容都是准备工作,而且只介绍了核心类 RefreshScope 作用以及提前准备好的 scope=refresh 实例对象;RefreshEventListener 未介绍,它是一个监听器,那么肯定有地方会发布这个事件,所以这边又要介绍另外一个自动装配进来的配置类了.

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第10张图片

介绍 spring-cloud-alibaba-config 模块内会自动装配进来 NacosContextRefresher 核心类,它实现了 ApplicationListener 接口,而它是在执行完之前所有的准备工作以后,发起了应用准备就绪事件,随即 NacosContextRefresher#onApplicationEvent 方法就会接受到事件进行调用,到这里,就弄清楚它的来龙去脉,继续往下走,看该方法处理做了什么样的操作!

private AtomicBoolean ready = new AtomicBoolean(false);

public void onApplicationEvent(ApplicationReadyEvent event) {
  // CAS 操作来确保只能加载一次
  if (this.ready.compareAndSet(false, true)) {
    this.registerNacosListenersForApplications();
  }
}

首先通过 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 重新加载的过程

一文带你从零到一深入透析 @RefreshScope 结合 Nacos 动态刷新源码_第11张图片

之前在讲解原始 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 个人博客

你可能感兴趣的:(Nacos,spring,java,spring,boot,Nacos)