一文带你从零到一深入透析 @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 进行加载了.


目录
相关文章
|
Nacos
Nacos源码构建报错程序包不存在com.alibaba.nacos.consistency.entity
Nacos源码构建报错程序包不存在com.alibaba.nacos.consistency.entity
452 0
Nacos源码构建报错程序包不存在com.alibaba.nacos.consistency.entity
|
3月前
|
关系型数据库 MySQL Java
“惊呆了!无需改动Nacos源码,轻松实现SGJDBC连接MySQL?这操作太秀了,速来围观,错过等哭!”
【8月更文挑战第7天】在使用Nacos进行服务治理时,常需连接MySQL存储数据。使用特定的SGJDBC驱动连接MySQL时,一般无需修改Nacos源码。需确保SGJDBC已添加至类路径,并在Nacos配置文件中指定使用SGJDBC的JDBC URL。示例中展示如何配置Nacos使用MySQL及SGJDBC,并在应用中通过Nacos API获取配置信息建立数据库连接,实现灵活集成不同JDBC驱动的目标。
110 0
|
Java Nacos 数据库
nacos源码打包及相关配置
nacos源码打包及相关配置
314 4
|
Cloud Native Java Go
解决Nacos配置刷新问题: 如何启用配置刷新功能以及与`@RefreshScope`注解的关联问题
解决Nacos配置刷新问题: 如何启用配置刷新功能以及与`@RefreshScope`注解的关联问题
1172 0
|
6月前
|
关系型数据库 MySQL 数据库连接
我想问一下用nacos连接mysql数据库但要用sgjdbc连接,需要改nacos源码吗?
我想问一下用nacos连接mysql数据库但要用sgjdbc连接,需要改nacos源码吗?
147 0
|
Java 测试技术 Nacos
Sentinel源码改造,实现Nacos双向通信!
Sentinel源码改造,实现Nacos双向通信!
221 0
Sentinel源码改造,实现Nacos双向通信!
|
Java API Nacos
Nacos服务健康检查与服务变动事件发布源码解析
Nacos服务健康检查与服务变动事件发布源码解析
94 0
|
11天前
|
负载均衡 应用服务中间件 Nacos
Nacos配置中心
Nacos配置中心
40 1
Nacos配置中心
|
7天前
|
监控 Java 测试技术
Nacos 配置中心变更利器:自定义标签灰度
本文是对 MSE Nacos 应用自定义标签灰度的功能介绍,欢迎大家升级版本进行试用。
|
10天前
|
网络安全 Nacos 开发者
Nacos作为流行的微服务注册与配置中心,“节点提示暂时不可用”是常见的问题之一
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,“节点提示暂时不可用”是常见的问题之一。本文将探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务的正常运行。通过检查服务实例状态、网络连接、Nacos配置、调整健康检查策略等步骤,可以有效解决这一问题。
22 4