Spring Cloud 使用 @RefreshScope 注解配置动态刷新

2023-11-17

一、@RefreshScope动态刷新原理

在SpringIOC中,BeanScope(Bean的作用域)影响了Bean的管理方式。

Bean的作用域:

例如创建Scope=singleton的Bean时,IOC会保存实例在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。

SpringCloud新增了一个自定义的作用域:refresh(可以理解为“动态刷新”),同样用了一种独特的方式改变了Bean的管理方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。

这个scope是如何做到热加载的呢?RefreshScope主要做了以下动作:

单独管理Bean生命周期

创建Bean的时候如果是RefreshScope就缓存在一个专门管理的ScopeMap中,这样就可以管理Scope是Refresh的Bean的生命周期了(所以含RefreshScope的其实一共创建了两个bean)。

重新创建Bean

外部化配置刷新之后,会触发一个动作,这个动作将上面的ScopeMap中的Bean清空,这样这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果。

spring cloud configsprring cloud alibaba nacos作为配置中心,其实现原理就是通过@RefreshScope 来实现对象属性的的动态更新。

@RefreshScope 实现配置的动态刷新需要满足一下几点条件:

  • @Scope注解

  • @RefreshScope注解

  • RefreshScope类

  • GenericScope类

  • Scope接口

  • ContextRefresher类

@RefreshScope 能实现动态刷新全仰仗着@Scope 这个注解。

1. @Scope注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

 /**
  * Alias for {@link #scopeName}.
  * @see #scopeName
  */
 @AliasFor("scopeName")
 String value() default "";

 /**
  *  singleton  表示该bean是单例的。(默认)
     *  prototype    表示该bean是多例的,即每次使用该bean时都会新建一个对象。
     *  request        在一次http请求中,一个bean对应一个实例。
     *  session        在一个httpSession中,一个bean对应一个实例
  */
 @AliasFor("value")
 String scopeName() default "";

 /**
    *   DEFAULT   不使用代理。(默认)
 *  NO    不使用代理,等价于DEFAULT。
 *  INTERFACES  使用基于接口的代理(jdk dynamic proxy)。
 *  TARGET_CLASS 使用基于类的代理(cglib)。
    */
 ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}

@Scope有两个主要属性value 和 proxyMode,其中proxyMode就是@RefreshScope 实现的本质了。

proxyMode属性是一个ScopedProxyMode类型的枚举对象。

public enum ScopedProxyMode {
    DEFAULT,
    NO,
    INTERFACES,// JDK 动态代理
    TARGET_CLASS;// CGLIB 动态代理

    private ScopedProxyMode() {
    }
}

proxyMode属性的值为ScopedProxyMode.TARGET_CLASS时,会给当前创建的bean 生成一个代理对象,会通过代理对象来访问,每次访问都会创建一个新的对象。

2. @RefreshScope注解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
 /**
  * @see Scope#proxyMode()
  */
 ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

它使用就是 @Scope ,一个scopeName="refresh"@Scope

proxyMode值为ScopedProxyMode.TARGET_CLASS,通过CGLIB动态代理的方式生成Bean。

使用 @RefreshScope 注解的 bean,不仅会生成一个beanName的bean,默认情况下同时会生成 scopedTarget.beanName的 bean。

@RefreshScope不能单独使用,需要和其他其他bean注解结合使用,如:@Controller@Service@Component@Repository等。

3. Scope接口

public interface Scope {

 /**
  * Return the object with the given name from the underlying scope,
  * {@link org.springframework.beans.factory.ObjectFactory#getObject() creating it}
  * if not found in the underlying storage mechanism.
  * <p>This is the central operation of a Scope, and the only operation
  * that is absolutely required.
  * @param name the name of the object to retrieve
  * @param objectFactory the {@link ObjectFactory} to use to create the scoped
  * object if it is not present in the underlying storage mechanism
  * @return the desired object (never {@code null})
  * @throws IllegalStateException if the underlying scope is not currently active
  */
 Object get(String name, ObjectFactory<?> objectFactory);

 @Nullable
 Object remove(String name);

 void registerDestructionCallback(String name, Runnable callback);

 @Nullable
 Object resolveContextualObject(String key);
 @Nullable
 String getConversationId();
}
Object get(String name, ObjectFactory<?> objectFactory)

这个方法帮助我们来创建一个新的bean ,也就是说,@RefreshScope 在调用刷新的时候会使用此方法来给我们创建新的对象,这样就可以通过spring 的装配机制将属性重新注入了,也就实现了所谓的动态刷新。

RefreshScope extends GenericScope, GenericScope implements Scope`

GenericScope 实现了 Scope 最重要的 get(String name, ObjectFactory<?> objectFactory) 方法,在GenericScope 里面 包装了一个内部类 BeanLifecycleWrapperCache 来对加了 @RefreshScope 从而创建的对象进行缓存,使其在不刷新时获取的都是同一个对象。(这里你可以把 BeanLifecycleWrapperCache 想象成为一个大Map 缓存了所有@RefreshScope 标注的对象)

知道了对象是缓存的,所以在进行动态刷新的时候,只需要清除缓存,重新创建就好了。

// ContextRefresher 外面使用它来进行方法调用 ============================== 我是分割线

 public synchronized Set<String> refresh() {
  Set<String> keys = refreshEnvironment();
  this.scope.refreshAll();
  return keys;
 }

// RefreshScope 内部代码  ============================== 我是分割线

 @ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.")
 public void refreshAll() {
  super.destroy();
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
 }


// GenericScope 里的方法 ============================== 我是分割线

 //进行对象获取,如果没有就创建并放入缓存
 @Override
 public Object get(String name, ObjectFactory<?> objectFactory) {
  BeanLifecycleWrapper value = this.cache.put(name,
    new BeanLifecycleWrapper(name, objectFactory));
  locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
   return value.getBean();
  }
  catch (RuntimeException e) {
   this.errors.put(name, e);
   throw e;
  }
 }
 // 初始化Bean
 public Object getBean() {
      if (this.bean == null) {
        String var1 = this.name;
        synchronized(this.name) {
          if (this.bean == null) {
            this.bean = this.objectFactory.getObject();
          }
        }
      }
      return this.bean;
    }
    
 //进行缓存的数据清理
 @Override
 public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  for (BeanLifecycleWrapper wrapper : wrappers) {
   try {
    Lock lock = locks.get(wrapper.getName()).writeLock();
    lock.lock();
    try {
     wrapper.destroy();
    }
    finally {
     lock.unlock();
    }
   }
   catch (RuntimeException e) {
    errors.add(e);
   }
  }
  if (!errors.isEmpty()) {
   throw wrapIfNecessary(errors.get(0));
  }
  this.errors.clear();
 }

通过观看源代码我们得知,我们截取了三个片段所得之,ContextRefresher 就是外层调用方法用的。

GenericScope类中有一个成员变量BeanLifecycleWrapperCache,用于缓存所有已经生成的Bean,在调用get方法时尝试从缓存加载,如果没有的话就生成一个新对象放入缓存,并通过初始化getBean其对应的Bean。

destroy 方法负责再刷新时缓存的清理工作。清空缓存后,下次访问对象时就会重新创建新的对象并放入缓存了。

所以在重新创建新的对象时,也就获取了最新的配置,也就达到了配置刷新的目的。

4. @RefreshScope 实现流程

  • 需要动态刷新的类标注@RefreshScope 注解。

  • @RefreshScope 注解标注了@Scope 注解,并默认了ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能就是再创建一个代理,在每次调用的时候都用它来调用GenericScope get 方法来获取对象。

  • 如属性发生变更

    • 调用 ContextRefresher refresh() -->> RefreshScope refreshAll() 进行缓存清理方法调用;

    • 发送刷新事件通知,GenericScope 真正的清理方法destroy() 实现清理缓存。

  • 在下一次使用对象的时候,会调用GenericScope get(String name, ObjectFactory<?> objectFactory) 方法创建一个新的对象,并存入缓存中,此时新对象因为Spring 的装配机制就是新的属性了。

5. @RefreshScope原理总结

1.SpringCloud程序的存在一个自动装配的类,这个类默认情况下会自动初始化一个RefreshScope实例,该实例是GenericScope的子类,然后注册到容器中。(RefreshAutoConfiguration.java,)

2.当容器启动的时候,GenericScope会自己把自己注册到scope中(ConfigurableBeanFactory#registerScope)(GenericScope

3.然后当自定义的Bean(被@RefreshScope修饰)注册的时候,会被容器读取到其作用域为refresh。(AnnotatedBeanDefinitionReader#doRegisterBean)

通过上面三步,一个带有@RefreshScope的自定义Bean就被注册到容器中来,其作用域为refresh。

4.当我们后续进行以来查找的时候,会绕过SingletonPrototype分支,进入最后一个分支,通过调用Scope接口的get()获取到该refresh作用域的实例。(AbstractBeanFactory.doGetBean

二、@RefreshScope注意事项

1. @RefreshScope使用注意事项

  • @RefreshScope作用的类,不能是final类,否则启动时会报错。

  • @RefreshScope不能单独使用,需要和其他其他bean注解结合使用,如:@Controller@Service@Component@Repository@Configuration等。

  • @RefreshScope 最好不要修饰在 @ScheduledlistenerTimmer等类中,因为配置的刷新会导致原来的对象被清除,需要重新使用对象才能出发生成新对象(但因为对象没了,又没法重新使用对象,死循环)

2. @RefreshScope动态刷新失效

考虑使用的bean是否是@RefreshScope生成的那个scopedTarget.beanName的 bean

springboot某些低版本貌似有问题,在Controller类上使用不会生效(网上有这么说的,没具体研究)

  • 解决方法1:注解上加属性@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)

  • 解决方法2:直接使用其他类单独封装配置参数,使用@RefreshScope+@Value方式

  • 解决方法3:直接使用@ConfigurationProperties

3. 不使用@RefreshScope也能实现动态刷新

直接使用@ConfigurationProperties,并不需要加@RefreshScope就能实现动态更新。

@ConfigurationProperties实现动态刷新的原理:

@ConfigurationPropertiesConfigurationPropertiesRebinder这个监听器,监听着EnvironmentChangeEvent事件。当发生EnvironmentChange事件后,会重新构造原来的加了@ConfigurationProperties注解的Bean对象。这个是Spring Cloud的默认实现。

4. 静态变量利用@RefreshScope动态刷新的坑(求大佬解答)

@RefreshScope
@Component
public class TestConfig {
    public static int url;

    @Value("${pesticide.url}")
    public void setUrl(int url) {
        TestConfig.url = url;
    }

    public void getUrl() {
    }
}
@RestController
@RequestMapping("test")
public class TestController {
    @Autowired
    private TestConfig testConfig;

    @GetMapping("testConfig")
    public int testConfig(){
        System.out.println("TestConfig:"+ TestConfig.url);
        testConfig.getUrl();
        System.out.println("TestConfig:"+ TestConfig.url);
        return TestConfig.url;
    }
}

1.url初始配置的值为1

请求接口日志:

TestConfig:1
TestConfig:1

2.修改url配置的值为2,动态刷新成功

请求接口日志:

TestConfig:1
TestConfig:2

这里就出现了问题,不调用@RefreshScope生产的代理对象testConfig的方法前(注意,该方法内无代码),取到的值还是为1;调了之后,取到的值为2.后续再次请求接口,取到的值都为2。

TestConfig:2
TestConfig:2
TestConfig:2
TestConfig:2

个人大胆猜想原因:参考上面@RefreshScope 实现流程可知,在第2步骤动态刷新成功时,此时仅仅是再创建类一个代理对象,并清除了实际对象的缓存;当再次通过代理对象来使用,才会触发创建一个新的实例对象,此时才会更新url的值。所以使用静态变量来是实现动态刷新时,一点要注意:使用对象才能出发创建新的实际对象,更新静态变量的值。

Spring Cloud的参考文档指出:

@RefreshScope在@Configuration类上工作,但可能导致令人惊讶的行为:例如,这并不意味着该类中定义的所有@Beans本身都是@RefreshScope。具体来说,依赖于这些bean的任何东西都不能依赖于刷新启动时对其进行更新,除非它本身在@RefreshScope中从刷新的@Configuration重新初始化(在刷新中将其重建并重新注入其依赖项,此时它们将被刷新)。

三、使用@RefreshScope的bean问题

这里之所以要会讨论使用@RefreshScope的bean问题,由上面上面所讲可以总结得到:

  • 使用 @RefreshScope 注解的 bean,不仅会生成一个名为beanName的bean,默认情况下同时会生成名为scopedTarget.beanName的bean

  • 使用 @RefreshScope 注解的会生成一个代理对象,通过这个代理对象来调用名为scopedTarget.beanName的 bean

  • 刷新操作会导致原来的名为scopedTarget.beanName的bean被清除,再次使用会新生成新的名为scopedTarget.beanName的bean,但原来的代理对象不会变动

下面举例说明:

nacos配置

test:
  value: 1

配置类获取配置值

@Data
@Component
@RefreshScope
public class TestConfig {

    @Value("${test.value}")
    private String value;
}

测试接口

@RestController
public class TestController {
    
    @Autowired
    private TestConfig testConfig;
    
    @RequestMapping("test11")
    public void test11() {
        // 代理对象
        System.out.println("@Autowired bean==========" + testConfig.getClass().getName());

        // 代理对象
        TestConfig bean = SpringUtils.getBean(TestConfig.class);
        System.out.println("Class bean==========" + bean.getClass().getName());

        // 代理对象
        Object bean1 = SpringUtils.getBean("testConfig");
        System.out.println("name(testConfig) bean==========" + bean1.getClass().getName());

        // 原类对象
        Object bean2 = SpringUtils.getBean("scopedTarget.testConfig");
        System.out.println("name(scopedTarget.testConfig) bean==========" + bean2.getClass().getName());

  System.out.println("================================================================================");

    }
}

测试

@Autowired注入的是代理对象

  • 通过Class得到的是代理对象

  • 通过名为beanName的得到的是代理对象

  • 通过名为scopedTarget.beanName的得到的是由@RefreshScope生成的那个原类对象

修改配置的值,测试

test:
  value: 2

动态刷新后,代理对象没有变化,由@RefreshScope生成的那个原类对象被清除后重新生成了一个新的原类对象

小结:

  • @Autowired方式注入的是代理对象

  • beanName的得到的是代理对象

  • scopedTarget.beanName的得到的@RefreshScope生成的那个原类对象

  • 代理对象不会随着配置刷新而更新

  • @RefreshScope生成的那个原类对象会随着配置的刷新而更新(属性时清除原来的,使用时才生成新的)

四、其它配置刷新方式

这种方法必须有 spring-boot-starter-actuator 这个starter才行。

POST http://localhost:7031/refresh

refresh的底层原理详见:org.springframework.cloud.context.refresh.ContextRefresher#refresh

SpringCloud2.0以后,没有/refresh手动调用的刷新配置地址。

SpringCloud2.0前

加入依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>

在类上,变量上打上@RefreshScope的注解

在启动的时候,都会看到

RequestMappingHandlerMapping : Mapped "{/refresh,methods=[post]}" 

也就是SpringCloud暴露了一个接口 /refresh 来给我们去刷新配置,但是SpringCloud 2.0.0以后,有了改变。

SpringCloud 2.0后

我们需要在bootstrap.yml里面加上需要暴露出来的地址

management:
  endpoints:
    web:
      exposure:
        include: refresh,health

现在的地址也不是/refresh了,而是/actuator/refresh

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring Cloud 使用 @RefreshScope 注解配置动态刷新 的相关文章

随机推荐

  • Wordpess百度自动推送代码

    直接将代码中的token换成百度站长中自己的即可 将下列代码放入functions php中就行了 WordPress 百度快速收录 API 提交代码 if function exists Baidu Submit function Bai
  • 操作系统实验进程调度模拟

    操作系统实验 实验一 实验1 基于优先数的时间片轮转调度算法调度处理模拟程序设计 一 实验目的 1 对进程调度的工作做进一步的理解 2 了解进程调度的任务 3 通过编程掌握基于优先数的时间片轮转调度算法具体实现过程 二 实验内容及实验要求
  • 遗传算法的实现

    请用遗传算法实现如下最大化问题 首先先来学习下 遗传算法的本质和过程 遗传算法是计算数学中用于解决最佳化的搜索算法 是进化算法的一种 进化算法最初是借鉴了进化生物学中的一些现象而发展起来的 这些现象包括遗传 突变 自然选择以及杂交等 遗传算
  • C#关键字 abstract,override,virtual的用法

    什么是抽象类 abstract关键字修饰的类称为抽象类 抽象类不能被实例化 抽象类是派生类的基类 关键字 abstract 语法 public abstract class 类名 1 一个抽象类可以同时包含抽象方法和非抽象方法 但不能实例化
  • IDEA工具快捷键---补全返回值

    Ctrl alt v 自动提示
  • 接口测试开发之:一篇搞懂 Cache、Cookie及Session的爱恨情仇

    Cashe Cookie与Session 1 引言 2 Cache 2 1 缓存定义 2 1 1 缓存概念 2 1 2 缓存优点 2 2 浏览器缓存 2 2 1 存储路径 2 2 2 缓存优点 2 2 3 缓存弊端 2 2 4 原理图 2
  • 【习题三】【数据库原理】

    文章目录 一 单选题 二 填空题 一 单选题 1 X Y能从推理规则导出的充分必要条件是 正确答案 B 2 设有关系模式R A B C D E 函数依赖集F A B B C C D D A AB BC AD 是R上的一个分解 那么分解 相对
  • [1143]Flink的Checkpoint和Savepoint

    文章目录 Flink的Checkpoint和Savepoint介绍 第一部分 Flink的Checkpoint 1 Flink Checkpoint原理介绍 2 Checkpoint的简单设置 3 保存多个Checkpoint 4 从Che
  • C++面试题(四)——智能指针的原理和实现

    C 面试题 一 二 和 三 都搞定的话 恭喜你来到这里 这基本就是c 面试题的最后一波了 1 你知道智能指针吗 智能指针的原理 2 常用的智能指针 3 智能指针的实现 1答案 智能指针是一个类 这个类的构造函数中传入一个普通指针 析构函数中
  • vimium使用

    vimium使用 2019 03 07 22 16 by 轩脉刃 阅读 评论 收藏 编辑 vimium使用 chrome下面的vimium插件已经慕名已久 迟迟没有做尝试 今天在家有空就熟悉了一下vimium 感觉还是棒棒的 记录一下一些使
  • 《面试准备》中兴2018笔试题

    include
  • 论文阅读 AutoML: A Survey of the State-of-the-Art

    论文阅读 AutoML A Survey of the State of the Art 摘要 略 简介 从两个角度介绍NAS 首先是模型的结构 常见的结构包括整体结构 基于单元的结构 层次结构和基于态射的结构等 其次是模型的超参数优化 H
  • Java序列化

    Java序列化 Java 提供了一种对象序列化的机制 该机制中 一个对象可以被表示为一个字节序列 该字节序列包括该对象的数据 有关对象的类型的信息和存储在对象中数据的类型 将序列化对象写入文件之后 可以从文件中读取出来 并且对它进行反序列化
  • 智慧监控vue实现的新型冠状病毒肺炎疫情可视化统计分析大屏前端案例

    2020年春节前后 新型冠状病毒肺炎疫情的消息牵动着全国人民的心 大家都非常关注疫情的变化和发展 非常关注疫情 在春节期间 针对疫情的发展变化集合在我们的专门的网页 实现一个可视化统计分析大屏前端 基于Vue技术实现 基于此项目可以做一些调
  • 基于MATLAB实现图像处理常用应用案例(附上100个仿真源码+数据)

    MATLAB是一款功能强大的图像处理软件 可以用于实现各种常见的图像处理应用 下面将介绍几个常见的图像处理应用案例 文章目录 1 图像去噪 2 图像增强 3 图像分割 4 特征提取 5 图像拼接 6 完整源码 数据下载 1 图像去噪 图像去
  • 通过Wireshark抓包疯狂聊天程序聊天记录

    文章目录 一 WireShark 简介 二 抓取聊天网络数据包 1 设备连接 2 使用wireshark进行抓包 3 测试分析 三 总结 四 参考链接 一 WireShark 简介 Wireshark是一个网络封包分析软件 网络封包分析软件
  • SQL Server 数据库增删改查

    一开始我们先讲一下 今给大家带来的是SQL Server 数据库的增删改查 我吗先了解一下里面要用到的方法 增加 insert 增加 into 到 values 值 删除 delete 删除 where条件 修改 update 修改 set
  • 【Android Studio】解决Android SDK -(unavailable)和Target folder is neither...问题

    1 JDK 是从 oracle 官网下载的 配置 Android Studio 选择 jdk 时指向对应目录 注意不是选择 Android Studio 内部的jre目录 而是选择另外下载的 JDK 的目录 2 图示问题出现时 选择目录要在
  • 关于压力测试的思路

    思路 把压力测试 SQL优化 MEMCACHED优化 SQL压力测试等进行模拟样例测试 并形成一系列办法 为以后可能出现的情况准备我们的知识储备 当然 就目前而言我们的小系统不需要这样的那样的优化 可能只能提升不到一毫秒 但我们是在整理办法
  • Spring Cloud 使用 @RefreshScope 注解配置动态刷新

    一 RefreshScope动态刷新原理 在SpringIOC中 BeanScope Bean的作用域 影响了Bean的管理方式 Bean的作用域 例如创建Scope singleton的Bean时 IOC会保存实例在一个Map中 保证这个