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

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

前言

源码部分涉及的版本

  • 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 类图:

从以上两张图来看,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 类准备工作

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

那么它是如何去处理 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 进行加载了.


目录
相关文章
|
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配置中心
15 0
|
2天前
|
JSON SpringCloudAlibaba Java
【微服务 SpringCloudAlibaba】实用篇 · Nacos配置中心(上)
【微服务 SpringCloudAlibaba】实用篇 · Nacos配置中心
16 1

热门文章

最新文章