Spring Cloud--@RefreshScope动态刷新的原理

原文网址:Spring Cloud--@RefreshScope动态刷新的原理_IT利刃出鞘的博客-CSDN博客

简介

本文介绍Spring Cloud的@RefreshScope动态刷新的原理。

原理概述

Spring的作用域有:single(单例)、prototype(多例)等(详见:Spring--Bean的作用域(scope)--使用/详解_IT利刃出鞘的博客-CSDN博客)。

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

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

  1. 需要动态刷新的类标注@RefreshScope 注解(单独管理Bean生命周期)
    1. @RefreshScope 注解标注了@Scope,里边有个默认属性:ScopedProxyMode.TARGET_CLASS; 属性,此属性的功能是再创建一个代理,在每次调用的时候都用它来调用GenericScope的get 方法来获取对象。
    2. 所以在创建Bean时,如果有@RefreshScope,就缓存在一个ScopeMap中(所以带有@RefreshScope的Bean其实一共创建了两个bean)。
  2. 若属性发生变更
    1. 调用 ContextRefresher refresh() => RefreshScope refreshAll() 进行缓存清理
    2. 发送刷新事件通知,调用GenericScope 的destroy() 实现清理缓存。这里将上面的ScopeMap中的Bean清空
  3. 在下一次使用对象时
    1. 调用GenericScope get(String name, ObjectFactory<?> objectFactory) 方法创建一个新的对象,使用最新的外部化配置的值注入类中,达到热加载新值的效果。

管理Bean的获取

@RefreshScope注解

一个Bean想自动加载配置,就要打上@RefreshScope注解,看看这个注解

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

RefreshScope有一个属性:proxyMode=ScopedProxyMode.TARGET_CLASS,这个是AOP动态代理用。

上边标注了 @Scope("refresh") ,将Bean的Scope变为refresh。SpringBoot的启动类上我们都会加上@SpringBootApplication注解(里面是一个@ComponentScan),它会扫描包中通过注解注册的Bean,扫描到带有@RefreshScope注解的Bean时,将其BeanDefinition的scope变为refresh。

创建Bean时区分scope

创建一个Bean的时候,会去BeanFactory的doGetBean方法创建Bean,不同scope有不同的创建方式:AbstractBeanFactory的doGetBean方法:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {//....// Create bean instance.// 单例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 {// 由上面的RefreshScope注解可以知道,这里scopeName=refreshString scopeName = mbd.getScope();// 获取Refresh的Scope对象final Scope scope = this.scopes.get(scopeName);if (scope == null) {throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");}try {// 让Scope对象去管理BeanObject scopedInstance = scope.get(beanName, () -> {beforePrototypeCreation(beanName);try {return createBean(beanName, mbd, args);}finally {afterPrototypeCreation(beanName);}});bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);}//...}
}
  1. 除了单例和原型Bean,其他Scope是由Scope对象处理的
  2. 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象

这里scope.get获取的Scope对象为RefreshScope。可见,创建Bean还是由IOC来做(createBean方法),但是获取Bean由RefreshScope对象的get方法去获取,其get方法在父类GenericScope中实现。

RefreshScope获取Bean

RefreshScope继承了GenericScope类,最终调用的是GenericScope的get方法:

public class GenericScopeimplements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {public Object get(String name, ObjectFactory <  ?  > objectFactory) {// 将Bean缓存下来BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));this.locks.putIfAbsent(name, new ReentrantReadWriteLock());try {// 创建Bean,只会创建一次,后面直接返回创建好的Beanreturn value.getBean();} catch (RuntimeException e) {this.errors.put(name, e);throw e;}}// 其他代码
}

放入缓存

private final ScopeCache cache;// 对应上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {return (BeanLifecycleWrapper) this.cache.put(name, value);
}

这里的ScopeCache对象是一个HashMap:

 

public class StandardScopeCache implements ScopeCache {private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();//...public Object get(String name) {return this.cache.get(name);}// 如果不存在,才会put进去public Object put(String name, Object value) {// result若不等于null,表示缓存存在了,不会进行put操作Object result = this.cache.putIfAbsent(name, value);if (result != null) {// 直接返回旧对象return result;}// put成功,返回新对象return value;}
}

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

回到Scope的get方法,接下来就是调用BeanWrapper的getBean方法: 

// 实际Bean对象,缓存下来了
private Object bean;public Object getBean() {if (this.bean == null) {synchronized (this.name) {if (this.bean == null) {this.bean = this.objectFactory.getObject();}}}return this.bean;
}

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

由此可见,RefreshScope管理了Scope=Refresh的Bean的生命周期。

重新创建RefreshBean

配置中心修改配置后,有两种方式可以动态刷新Bean的配置值,(SpringCloud-Bus、Nacos都是这么实现的):

  1. 向上下文发布一个RefreshEvent事件
  2. Http访问/refresh这个EndPoint

不管是什么方式,都会调用ContextRefresher#refresh()方法.

// 这就是我们上面一直分析的Scope对象(实际上可以看作一个保存refreshBean的Map)
private RefreshScope scope;public synchronized Set<String> refresh() {// 更新上下文中Environment外部化配置值Set<String> keys = refreshEnvironment();// 调用scope对象的refreshAll方法this.scope.refreshAll();return keys;
}

刷新的方案

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

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

这里说的刷新Environment对象并重新依赖注入是上边refresh()中这两个方法做的事情:

Set keys = refreshEnvironment();
this.scope.refreshAll();

刷新Environment对象

简单介绍如何刷新Environment里的Property值

public synchronized Set<String> refreshEnvironment() {// 获取刷新配置前的配置信息,对比用Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());// 刷新EnvironmentaddConfigFilesToEnvironment();// 这里上下文的Environment已经是新的值了// 进行新旧对比,结果返回有变化的值Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));return keys;
}

重点是addConfigFilesToEnvironment方法(刷新Environment)

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);// 启动上下文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对象中的配置值。

重新创建RefreshBean

经过上述动作,此时上下文中的配置值已经是最新的了。回到ContextRefresher的refresh方法,它会调用Scope的refreshAll方法:

public void refreshAll() {// 销毁Beansuper.destroy();this.context.publishEvent(new RefreshScopeRefreshedEvent());
}public void destroy() {List<Throwable> errors = new ArrayList<Throwable>();// 缓存清空Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();// ...
}

还记得上面的“管理Bean的获取”关于缓存的讨论吗,cache变量是一个Map保存着RefreshBean实例,这里直接就将Map清空了。

回到BeanFactory的doGetBean的流程中,从IOC容器中获取RefreshBean是交给RefreshScope的get方法做的:

public Object get(String name, ObjectFactory<?> objectFactory) {// 由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例BeanLifecycleWrapper value = this.cache.put(name,new BeanLifecycleWrapper(name, objectFactory));this.locks.putIfAbsent(name, new ReentrantReadWriteLock());try {// 在这里是新的BeanLifecycleWrapper实例调用getBean方法return value.getBean();}catch (RuntimeException e) {this.errors.put(name, e);throw e;}
}
public Object getBean() {// 由于是新的BeanLifecycleWrapper实例,这里一定为nullif (this.bean == null) {synchronized (this.name) {if (this.bean == null) {// 调用IOC容器的createBean,再创建一个Bean出来this.bean = this.objectFactory.getObject();}}}return this.bean;
}

此时RefreshBean被IOC容器重新创建出来了,经过IOC的依赖注入功能,@Value就是一个新的配置了。到这里热加载功能实现基本结束。

可以看出只要从IOC容器中getBean,那么拿到的RefreshBean一定是带有最新配置值的Bean。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/88454.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python+nodejs+php+springboot+vue 导师双选系统

为了直观显示系统的功能&#xff0c;运用用例图这样的工具显示分析的结果。分析的导师功能如下。导师管理导师选择信息&#xff0c;管理项目&#xff0c;管理项目提交并对学员提交的项目进行指导。 为了直观显示系统的功能&#xff0c;运用用例图这样的工具显示分析的结果。分析…

漫谈:C语言 C++ 所有编程语言 =和==的麻烦

这次不只是C语言很麻拐&#xff0c;是所有编程语言都很麻拐了。 赋值和比较是编程语言最基本的操作之二&#xff0c;C和所有类C语言都使用“”和“”来分别表示赋值和比较。 数学上等号“”是个单一的概念&#xff0c;含义是“相等”&#xff0c;左右两边是等价的&#xff0c;很…

神仙打架!谷歌和OpenAI竞相推出多模式AI

原创 | 文 BFT机器人 随着秋季的到来&#xff0c;科技界正在展开另一场季节性活动——科技巨头谷歌和OpenAI正在竞相发布下一代多模态大语言模型&#xff0c;这些高级模型能够解释图像和文本&#xff0c;使他们能够执行诸如从草图生成网站代码或以文本形式描述视觉图表等任务。…

秦时明月沧海手游阵容推荐,秦时明月沧海角色强度

秦时明月沧海角色强度如何&#xff1f;在秦时明月沧海手游中&#xff0c;您可以从大量的角色卡牌中选择并发展&#xff0c;为了顺利通过各种副本&#xff0c;玩家们需要精心搭配阵容。那么&#xff0c;具体该如何配置最强的角色呢&#xff1f; 下面&#xff0c;小编将带各位玩家…

TypeScript学习大纲

TypeScript 是 JavaScript 的一个超集&#xff0c;它为 JavaScript 添加了静态类型系统。以下是一些必须了解的 TypeScript 基本知识点和特性&#xff1a; 基本类型: TypeScript 支持与 JavaScript 相同的基本类型&#xff0c;并提供了一些额外的类型选项。 let isDone: boolea…

怎么加密U盘文件?U盘文件加密软件哪个好?

当U盘中储存重要数据时&#xff0c;我们需要保护U盘文件安全&#xff0c;避免数据泄露。那么&#xff0c;怎么加密U盘文件呢&#xff1f;U盘文件加密软件哪个好呢&#xff1f; ​U盘数据怎么避免泄露&#xff1f; 想要避免U盘数据泄露&#xff0c;最佳的方法就是对U盘文件进行…

彻底弄懂js函数柯里化

彻底弄懂js函数柯里化 1、前言2、什么是柯里化3、实现原理4、应用场景4.1 参数复用4.2 遍历数组 1、前言 函数柯里化(Currying)在JavaScript中总感觉属于一种不温不火的存在&#xff0c;甚至有些开发者在提起柯里化时&#xff0c;竟然会有点生疏不懂。其实不然&#xff0c;对于…

关于埋点上报

一、埋点上报结构包含哪些&#xff1f; 埋点上报结构一般包含以下信息&#xff1a; 事件名称&#xff1a;标识上报的是哪个事件&#xff0c;例如“注册成功”或“点击按钮”等。事件发生时间&#xff1a;记录事件发生的时间戳。用户ID&#xff1a;标识事件所属的用户。设备信息…

Vue实现Hello World

<div id"aa"> <p>{{h}}</p> </div> <script src"https://cdn.jsdelivr.net/npm/vue2/dist/vue.js"></script> <script> const hello new Vue({ el:#aa, data:{ h : Hello World } }) </script>

Konva基本处理流程和相关架构设计

前言 canvas是使用JavaScript基于上下文对象进行2D图形的绘制的HTML元素&#xff0c;通常用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。基于Canvas之上&#xff0c;诞生了例如 PIXI、ZRender、Fabric、Konva等 Canvas渲染引擎&#xff0c;兼顾易用的同时…

TCP协议与UDP协议

TCP&#xff08;传输控制协议&#xff09;和UDP&#xff08;用户数据报协议&#xff09;是两种常见的互联网传输协议&#xff0c;它们在数据传输方面有一些重要的区别&#xff1a; 连接性&#xff1a;TCP是面向连接的协议&#xff0c;而UDP是无连接的协议。这意味着在使用TCP进…

基于微信小程序的电影院订票系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言运行环境说明用户微信小程序端的主要功能有&#xff1a;管理员的主要功能有&#xff1a;具体实现截图详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考论文参考源码获取 前言 &#x1f497;博主介绍&…

ASCII码-对照表

ASCII 1> ASCII 控制字符2> ASCII 显示字符3> 常用ASCII码3.1> 【CR】\r 回车符3.2> 【LF】\n 换行符3.3> 不同操作系统&#xff0c;文件中换行 1> ASCII 控制字符 2> ASCII 显示字符 ASCII&#xff08;American Standard Code for Information Interc…

【计算机网络】IP协议

目录 前言 IP协议 基本概念 IP协议格式 分片 16位标识 3位标志与13位片偏移 分片流程 网段划分 网络号和主机号 DHCP协议 CIDR划分方案 特殊的ip地址 ip地址数量限制 私有ip地址与公网ip地址 路由转发 前言 我们前面讲了HTTP/HTTPS协议和TCP/…

ElementUI - 主页面--动态树右侧内容管理

一.左侧动态树 1.定义组件 ①样式&数据处理 <template><el-menu class"el-menu-vertical-demo" background-color"#334157"text-color"#fff" active-text-color"#ffd04b" :collapse"collapsed" router :def…

centos7通过docker搭建nginx+php环境

以下环境都是基于centos7.9完成。 1.安装docker yum install docker-ce 说明&#xff1a;这一步&#xff0c;由于centos软件仓库没有收纳docker&#xff0c;需要自己去官网爬文档安装。 安装完成之后&#xff0c;就是启动docker服务以及添加到开机启动。 systemctl enable do…

相乘(蓝桥杯)

相乘 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 小蓝发现&#xff0c;他将 1 至 1000000007 之间的不同的数与 2021 相乘后再求除以 1000000007 的余数&#xff0c;会得到不同的数。 小蓝想知道&#xff0c;能不能在 1 …

WordPress主题开发( 七)之—— 模版文件继承规则

WordPress主题开发&#xff08; 七&#xff09;之—— 模版文件继承规则 概述模板文件层次结构示例可视化概述层次结构详细信息主页显示首页显示单文章页面单页分类目录标签自定义分类自定义文章类型作者显示日期搜索结果404&#xff08;未找到&#xff09;附件嵌入功能非ASCII…

Spring Cloud Alibaba快速整合OpenFeign

文章目录 spring cloud alibaba 整合OpenFeign整合流程1.导入依赖2. 编写调用接口2.1 service&#xff08;这里写的是clients&#xff09;2.2 controller 3.设置其最大链接时间3.1 配置文件3.2 client3.3 接口3.4 被访问的controller spring cloud alibaba 整合OpenFeign Fore…

如何评估商城源码的安全性和稳定性?

评估商城源码的安全性和稳定性是选择合适的商城源码的关键一步。以下是一些方法和指标&#xff0c;可用于评估商城源码的安全性和稳定性。希望对大家有所帮助(仅供参考)。 1、源码质量 商城源码的质量是评估其安全性和稳定性的重要指标之一。我们技术可以检查源码的编码规范、…