@RefreshScope刷新配置文件原理

2023-11-16

一、前置知识

在Spring中bean的作用域(scope)常用的有两种,单例(singleton)、原型(prototype),Bean的Scope影响了Bean的管理方式,例如创建Scope=singleton的Bean时,IOC会将这些Bean实例保存在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。而在SpringCloud中为其新添加了一种作用域为refresh,改变了Bean的管理方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。

  • 那这个scope是如何做到热加载的呢?先说结论:
    因为可以单独管理Bean的创建和销毁 创建Bean的时候如果scope为refresh,这个Bean就缓存在一个专门管理这类scope的map中, 当外部配置更改过后,会触发一个刷新动作,这个动作将上面的map中的Bean清空,这样,当再次用到这个Bean的时候,这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果 下面我们深入源码,来验证我们上述的讲法。

二、@RefreshScope探究

 可以看到@RefreshScope注解只又套了一个@Scope("refresh"),也就意味着被@RefreshScope注解类作用域会变为refresh,并且其proxyMode属性设置为了TARGET_CLASS,如果是TARGET_CLASS,ioc会为其创建一个代理对象。这里为什么设置成TARGET_CLASS后面再介绍

// 单例Bean的创建
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      //...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }
 
  // 原型Bean的创建
  else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
        // ...
    try {
      prototypeInstance = createBean(beanName, mbd, args);
    }
    //...
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
  }
 
  else {
    // 1、由上面的RefreshScope注解可以知道,这里scopeName=refresh
    String scopeName = mbd.getScope();
    // 2、获取RefreshScope对象
    final Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    }
    try {
      // 3、让Scope对象去管理Bean
      Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
          return createBean(beanName, mbd, args);
        }
        finally {
          afterPrototypeCreation(beanName);
        }
      });
      bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }

 而在SpringBoot启动时,把Bean扫描到IOC容器中,不同scope有不同的创建方式,在AbstractBeanFactory#doGetBean方法中,创建scope为refresh的Bean的逻辑就会走最下面的else逻辑。
这里可以得出几个结论:

  • 单例和原型scope的Bean是硬编码单独处理的

  • 除了单例和原型Bean,其他Scope是由Scope对象处理的

  • 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象

通过debug,this.scopes有四类scope,另外3个不常用的scope也对应上了,我们可以看到,返回的是RefreshScope对象,那这个RefreshScope是什么时候加载进来的呢?其实是通过RefreshAutoConfiguration自动装配进来的,不是本文重点,提一下。(这里可以发现我们可以自定义scope,不过一般开发中用不上)

 下面我们看下scope.get,前面我们知道这个scope为RefreshScope,所以我们去RefreshScope里面去找get方法,发现没有对其实现,而RefreshScope继承了GenericScope,GenericScope的get如下:

这里就是将Bean包装成一个BeanLifecycleWrapper对象,缓存在一个Map中,下次如果再getBean,还是那个旧的BeanLifecycleWrapper

 可以看出来,BeanLifecycleWrapper中的bean变量即为实际Bean,第一次get肯定为空,就会调用BeanFactory的createBean方法创建Bean,创建出来之后就会一直保存下来。

三、刷新Environment对象

当配置中心更改配置之后,有两种方式可以动态刷新Bean的配置变量值

  • 向上下文发布一个RefreshEvent事件
  • Http访问/actuator/refresh(springboot2.0之前为/refresh,springboot2.0之后默认没有开启refresh端点,需配置)

不管是什么方式,最终都会调用ContextRefresher这个类的refresh方法,那么我们由此为入口来分析一下,热加载配置的原理:

我们一般是使用@Value、@ConfigurationProperties去获取配置变量值,其底层在IOC中则是通过上下文的Environment对象去获取property值,然后依赖注入利用反射Set到Bean对象中去的。

那么如果我们更新Environment里的Property值,然后重新创建一次RefreshBean,再进行一次上述的依赖注入,是不是就能完成配置热加载了呢?@Value的变量值就可以加载为最新的了。

下面说一下几个核心方法

  • refreshEnvironment()方法对比新老配置,返回有变化的配置keys,其中有个重点方法addConfigFilesToEnvironment(),通过名字可判断将最新配置加入到环境变量中
    ConfigurableApplicationContext addConfigFilesToEnvironment() {
      ConfigurableApplicationContext capture = null;
      try {
        // 从上下文拿出Environment对象,copy一份
        StandardEnvironment environment = copyEnvironment(
          this.context.getEnvironment());
        // SpringBoot启动类builder,准备新做一个Spring上下文启动
        SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
          // banner和web都关闭,因为只是想单纯利用新的Spring上下文构造一个新的Environment
          .bannerMode(Mode.OFF).web(WebApplicationType.NONE)
          // 传入我们刚刚copy的Environment实例
          .environment(environment);
           //设置一个监听器,监听环境改变
    	   builder.application()
    				.setListeners(Arrays.asList(new BootstrapApplicationListener(),
    							new ConfigFileApplicationListener()));
        // 启动上下文
        capture = builder.run();
        // 这个时候,通过上下文SpringIOC的启动,刚刚Environment对象就变成带有最新配置值的Environment了
        // 获取旧的外部化配置列表
        MutablePropertySources target = this.context.getEnvironment()
          .getPropertySources();
        String targetName = null;
        // 遍历这个最新的Environment外部化配置列表
        for (PropertySource<?> source : environment.getPropertySources()) {
          String name = source.getName();
          if (target.contains(name)) {
            targetName = name;
          }
          // 某些配置源不做替换,读者自行查看源码
          // 一般的配置源都会进入if语句
          if (!this.standardSources.contains(name)) {
            if (target.contains(name)) {
              // 用新的配置替换旧的配置
              target.replace(name, source);
            }
            else {
              //....
            }
          }
        }
      }
      //....
    }

    可以看到,这里归根结底就是SpringBoot启动上下文那种方法,新做了一个Spring上下文,因为Spring启动后会对上下文中的Environment进行初始化,获取最新配置,所以这里利用Spring的启动,达到了获取最新的Environment对象的目的。然后去替换旧的上下文中的Environment对象中的配置值即可。

  • refreshAll()

    这里调用了destroy()就将上文的this.cache(实际就是个map)清空了。 

思路回到sopce.get这里,由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例,value.getBean()方法中也会重新去createBean。

  

最后为什么proxyMode属性设置为了TARGET_CLASS?

首先我们要知道ScopedProxyMode的作用:ScopedProxyMode是一个枚举类,该类共定义了四个枚举值,分别为NO、DEFAULT、INTERFACE、TARGET_CLASS,其中DEFAULT和NO的作用是一样的。INTERFACES代表要使用JDK的动态代理来创建代理对象,TARGET_CLASS代表要使用CGLIB来创建代理对象。比如下面这个场景:

@Component
@RefreshScope      
public class Config {
}

@Component
public class UserService {
    @Autowired
    private Config config;
}

@RestController
public class TestController {
    @Autowired
    Config config;
  

}

我们知道对象都是 @Autowired 或者 @Resource 注入进去的,那就会出现一个问题,refresh bean 被销毁重建后,其它类依赖的这个bean 怎么更新?也就是UserService怎么去更新Config对象,答案是代理对象

首先从代码上解释来说UserService持有的是Config的一个代理bean,而代理bean才持有真正Config的bean。而在refersh的时候是销毁是代理bean持有的bean,代理bean是不会被销毁的,然后再次通过代理bean创建新的Config bean即可。也就是说,这个Config的bean在 ioc容器里已经不是原始的类,而是一个代理对象。

下篇文章会通过一个demo演示refresh何时被调用

 

参考文章:

@RefreshScope 刷新机制都不懂,还敢说会?_架构核心技术的博客-CSDN博客_@refreshscope

Spring Cloud 2.2.2 源码之三十九nacos配置动态刷新原理一_王伟王胖胖的博客-CSDN博客_nacos动态刷新原理

@RefreshScope 自动刷新原理(三) - C/C++教程 - 找一找教程网


 

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

@RefreshScope刷新配置文件原理 的相关文章

随机推荐

  • Lua : 流程控制也没啥,if/嵌套仅需知

    目录 1 if else end 语法 2 if elseif else end 语法 3 if的嵌套 Lua中流程控制 使用if 和if的嵌套就好 当然goto也可以用于流程控制 其实我在想 C C 中如此好用的switch 语法 Lua
  • Java研发京东4面:事务隔离+乐观锁+HashMap+秒杀设计+微服务(面试真题)

    在朋友圈看到一个以前的同事这些天去京东面试了 就让他整理了一下面试官问了些他什么 然后就有了这篇文章 这篇文章主要介绍了 Java岗 京东的四次面试 面试题全为面试真题 一面 基础面 约1小时 二面 问数据库较多 三面 综合面 约一个小时
  • 6.STM32中断优先级管理

    1 中断 stm32的芯片通常有90多个以上的中断 具有16级可编程的中断优先级 2 中断管理方法 1 首先对STM32中断进行分组 有组0 4 同时对每一个中断设置一个抢占优先级和一个响应优先级值 分组配置是在寄存器SCB gt AIRC
  • 【Python】工程与包(2)

    创建工程及第三方包管理 New environment using 新建的项目里有一个venv virtualenv 文件夹 专门存放本项目所依赖的第三方模块 Existing interpreter 表示新建的项目所依赖的第三方模块是存放
  • 推荐工具url

    https www processon com diagrams 很好的web画图工具 https www tapd cn letters from top nav worktable v2 产品 研发 测试工具
  • uboot环境变量的讲解

    1 环境变量的作用域和全局变量相同 环境变量的生命周期为一旦设置好环境变量并保存好后 下次开机还存在 而全局变量在关机后就灭亡了 下次开机产生了一个新的全局变量 2 环境变量如何参与程序运行 1 环境变量有2份 分别在Flash和DDR中
  • hadoop3.3.1单机版环境搭建详细流程记录

    1 在centos7中创建必要的目录 2 上传JDK安装包到tools目录 3 解压JDK到 opt server 目录 tar zxvf jdk 8u221 linux x64 tar gz C opt server 4 vim 未找到命
  • 视音频编解码技术零基础学习方法

    一直想把视音频编解码技术做一个简单的总结 可是苦于时间不充裕 一直没能完成 今天有着很大的空闲 终于可以总结一个有关视音频技术的入门教程 可以方便更多的人学习从零开始学习视音频技术 需要注意的是 本文所说的视音频技术 指的是理论层面的视音频
  • Python-Jenkins 在 Jenkins 中的应用

    Author rab Python 版本 3 9 Jenkins 版本 2 409 官方文档 https python jenkins readthedocs io en latest 目录 前言 一 案例 1 1 管理 Jenkins V
  • Java远程调试(Remote Debug)方法

    Java远程调试的原理是两个VM之间通过debug协议进行通信 然后以达到远程调试的目的 两者之间可以通过socket进行通信 首先被debug程序的虚拟机在启动时要开启debug模式 启动debug监听程序 jdwp是Java Debug
  • 简易的打包器--webpack打包原理

    手写一个简单的类似webpack的打包器 打包流程说明 定义依赖分析函数 通过读取文件内容 分析得到该文件导入的依赖项 code gt AST gt 得到导入声明 记录导入声明中的依赖项路径 gt AST gt code gt 返回记录当前
  • 扫描局域网内所有电脑的端口

    扫描局域网内所有电脑端口可以使用工具如Nmap来完成 Nmap是一个开源的网络探测工具 可以用来扫描端口 识别操作系统 检测服务和插件等 使用Nmap扫描局域网内所有电脑端口的命令如下 nmap sP 192 168 1 0 24 其中19
  • kvm直通sata_PVE 中 对 KVM虚拟机的USB设备设置为直通

    Proxmox支持将USB设备直接映射给KVM虚拟机使用 也就是所谓的USB Passthrough 配置步骤可以参考Proxmox wiki上的有关说明 具体地址是https pve proxmox com wiki USB physic
  • Java随机数

    1 指定数字范围 package com jiayou peis official account biz utils import java util Random public class Test public static void
  • Python模拟登陆万能法-微博

    Python模拟登陆让不少人伤透脑筋 今天奉上一种万能登陆方法 你无须精通HTML 甚至也无须精通Python 但却能让你成功的进行模拟登陆 本文讲的是登陆所有网站的一种方法 并不局限于微博与知乎 仅用其作为例子来讲解 用到的库有 sele
  • ubuntu使用教程与常用命令

    ubuntu使用教程 一 Ubuntu简介 Ubuntu 乌班图 是一个基于Debian的以桌面应用为主的Linux操作系统 据说其名称来自非洲南部祖鲁语或科萨语的 ubuntu 一词 意思是 人性 我的存在是因为大家的存在 是非洲传统的一
  • pdf.js详细解析

    pdf js可以实现在html下直接浏览pdf文档 是一款开源的pdf文档读取解析插件 pdf js主要包含两个库文件 一个pdf js和一个pdf worker js 一个负责API解析 一个负责核心解析 pdf js可通过pdf文件的地
  • 神奇的tmux

    一 Tmux 是什么 1 1 会话与进程 命令行的典型使用方式是 打开一个终端窗口 terminal window 以下简称 窗口 在里面输入命令 用户与计算机的这种临时的交互 称为一次 会话 session 会话的一个重要特点是 窗口与其
  • STM32+4G模块实战项目(连接阿里云物联网平台+OTA升级):(一)初识硬件

    STM32 4G模块实战项目 OTA升级 构思占坑中ing 3天一章 请耐心等待 STM32 4G模块实战项目 连接阿里云物联网平台 OTA升级 一 初识硬件 STM32 4G模块实战项目 连接阿里云物联网平台 OTA升级 二 stm32通
  • @RefreshScope刷新配置文件原理

    一 前置知识 在Spring中bean的作用域 scope 常用的有两种 单例 singleton 原型 prototype Bean的Scope影响了Bean的管理方式 例如创建Scope singleton的Bean时 IOC会将这些B