【深入理解SpringCloud微服务】深入理解nacos配置中心(六)——spring-cloud-context关于配置刷新的公共逻辑

【深入理解SpringCloud微服务】深入理解nacos配置中心(六)——spring-cloud-context关于配置刷新的公共逻辑

  • 原理分析
  • 源码解析
    • RefreshEventListener#onApplicationEvent(ApplicationEvent)
    • ContextRefresher#refresh()
      • ContextRefresher#refreshEnvironment()
      • RefreshScope#refreshAll()
      • @RefreshScope注解的原理

我们在上一篇文章《客户端监听配置变更并刷新的源码分析》中最后说到,nacos客户端监听到配置变更通知后,会发布一个RefreshEvent事件,触发spring-cloud-context关于配置刷新的公共逻辑。但是由于它不是nacos实现的逻辑,我们没有对这段逻辑进行分析,本篇文章将会分析spring-cloud-context关于配置刷新的公共逻辑。

原理分析

spring-cloud-context会往Spring容器中注册一个监听器RefreshEventListener,这个监听器会监听并处理RefreshEvent事件。

在这里插入图片描述

RefreshEventListener监听到RefreshEvent事件后,会调用ContextRefresher的refresh()进行配置刷新的操作。

在这里插入图片描述

ContextRefresher的refresh()会做两件事情:

  1. 创建一个新的SpringApplication再跑一遍run()方法拿到最新的配置,覆盖到当前环境Environment中
  2. 销毁被@RefreshScope注解修饰的bean,等下一次调用到该bean时从容器中获取发现没有了,会重新创建一个,此时就会拿到最新的配置

在这里插入图片描述

首先解析第一件事情:创建一个新的SpringApplication再跑一遍run()方法。

SpringApplication的run()方法就是SpringBoot工程启动时的main方法执行的方法,这里创建一个新的SpringApplication,执行它的run()方法,就会得到一个新的ApplicationContext,里面的Environment中的配置都是最新的配置,拿到这个Environment就等于拿到了最新的配置。

也就是说这里就是为了加载到最新的配置,因此才新建一个新的SpringApplication并执行它的run()方法的,这是拿到最新配置最省事的做法。

在这里插入图片描述

然后解析第二件事情:销毁被@RefreshScope注解修饰的bean。

@RefreshScope注解修饰的bean的属性引用的配置都是会动态刷新的,也就是说如果我们更新了配置,那么它就会读到最新的配置。

如果让我们去实现这个功能,我们第一时间想到的是拿到最新的配置,重新给这些bean赋值。这么做确实是可以的,就是太麻烦了。

因此最好的做法就是把它们销毁,下次如果要用到这个bean,Spring发现没有,就会重新创建一个并初始化,由于当前Environment已经被覆盖了最新的配置,因此新创建的bean的属性引用到的配置值就是最新的。

在这里插入图片描述

带着对原理理解的认知,我们就可以去看源码了。

源码解析

RefreshEventListener#onApplicationEvent(ApplicationEvent)

spring-cloud-context的spring.factories文件指定了自动配置类RefreshAutoConfiguration,通过SpringBoot的自动装配机制,会自动加载并解析RefreshAutoConfiguration。

RefreshAutoConfiguration中通过@Bean注解注册了一个监听器RefreshEventListener。

在这里插入图片描述

RefreshEventListener的onApplicationEvent方法监听RefreshEvent事件并进行处理。

在这里插入图片描述

	public void onApplicationEvent(ApplicationEvent event) {...handle((RefreshEvent) event);...}public void handle(RefreshEvent event) {...// 调用ContextRefresher的refresh()方法进行配置刷新操作Set<String> keys = this.refresh.refresh();log.info("Refresh keys changed: " + keys);...}

RefreshEventListener的onApplicationEvent方法监听RefreshEvent事件后,会调用ContextRefresher的refresh()方法进行配置刷新操作。

在这里插入图片描述

这个ContextRefresher也是在RefreshAutoConfiguration中通过@Bean注册到Spring容器中的,并且会作为RefreshEventListener构造方法的参数。

在这里插入图片描述

ContextRefresher#refresh()

	public synchronized Set<String> refresh() {// 第一件事情:创建一个新的SpringApplication再跑一遍run()方法Set<String> keys = refreshEnvironment();// 第二件事情:销毁被@RefreshScope注解修饰的bean,// 调用的是RefreshScope#refreshAll()方法this.scope.refreshAll();return keys;}

ContextRefresher的refresh()方法中的这两行代码,做的就是我们上面说的两件事情。

refreshEnvironment()方法会创建一个新的SpringApplication再跑一遍run()方法,得到一个新的Environment,获取里面最新的配置,覆盖到当前环境的Environment中。

this.scope.refreshAll()方法则是销毁被@RefreshScope注解修饰的bean,调用的是RefreshScope#refreshAll()方法。下一从容器中获取时就会重新创建并初始化,这个bean引用的配置值自然就是最新的。

在这里插入图片描述

ContextRefresher#refreshEnvironment()

	public synchronized Set<String> refreshEnvironment() {...addConfigFilesToEnvironment();...}ConfigurableApplicationContext addConfigFilesToEnvironment() {...try {// 使用当前环境的Environment,copy出一个新的EnvironmentStandardEnvironment environment = copyEnvironment(this.context.getEnvironment());// SpringApplicationBuilder是SpringApplication构造器// 会创建一个新的SpringApplicationSpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class).bannerMode(Mode.OFF).web(WebApplicationType.NONE)// 新的Environment设置到SpringApplicationBuilder中.environment(environment);...// SpringApplicationBuilder的run()方法,// 里面会执行新建的SpringApplication的run()方法,// 执行完后,新的Environment就会加载到最新的配置capture = builder.run();...// 拿到当前环境的Environment中的MutablePropertySources// MutablePropertySources包含了Environment中的所有属性MutablePropertySources target = this.context.getEnvironment().getPropertySources();...// 遍历新的Environment中的所有属性for (PropertySource<?> source : environment.getPropertySources()) {// 这里面就是把新的Environment的属性覆盖到当前Environment中的逻辑			...if (target.contains(name)) {target.replace(name, source);}		...		}}finally {...}...}

ContextRefresher#refreshEnvironment()方法的处理流程如下:

  1. 使用当前环境的Environment,copy出一个新的Environment
  2. 创建一个SpringApplicationBuilder,SpringApplicationBuilder是SpringApplication构造器,SpringApplicationBuilder的构造方法会创建一个新的SpringApplication
  3. 将新的Environment设置到SpringApplicationBuilder中
  4. 执行SpringApplicationBuilder的run()方法,里面会执行新的SpringApplication的run()方法,执行完后,新的Environment就会加载到最新的配置
  5. for循环遍历新的Environment中的所有属性,覆盖到当前Environment中

在这里插入图片描述

RefreshScope#refreshAll()

我们回到ContextRefresher的refresh()方法中,看一下第二行代码“this.scope.refreshAll();”里面的逻辑。

	public void refreshAll() {// 销毁被@RefreshScope注解修饰的beansuper.destroy();// 发布一个RefreshScopeRefreshedEvent事件,// 我们可以监听RefreshScopeRefreshedEvent事件,// 从而得知@RefreshScope注解修饰的bean被刷新(也就是被销毁了)this.context.publishEvent(new RefreshScopeRefreshedEvent());}

RefreshScope的refreshAll()方法调用父类的destroy()方法销毁被@RefreshScope注解修饰的bean。

super.destroy()方法会进入到GenericScope的destroy方法中。

	public void destroy() {...// 清空GenericScope中的缓存,// 这里面缓存的都是@RefreshScope注解修饰的bean// 只是每个bean都被包裹在一个BeanLifecycleWrapper对象中Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();// 遍历上面返回的每个beanfor (BeanLifecycleWrapper wrapper : wrappers) {try {...try {// 销毁bean// 如果实现了DisposableBean接口,执行bean的destroy()方法// 如果配置了自定义销毁方法destroyMethod,执行它wrapper.destroy();}...}catch (...) {...}}...}

GenericScope的cache属性是一个缓存池,里面缓存了被@RefreshScope注解修饰的bean。只是这些bean每一个都被包装在一个BeanLifecycleWrapper对象中。

调用this.cache.clear()就是清空GenericScope的缓存,然后返回这里面的bean。

获取到this.cache.clear()方法返回的bean后,就for循环遍历每一个bean,调用BeanLifecycleWrapper的destroy()去销毁bean。

BeanLifecycleWrapper的destroy()最终会执行bean的销毁方:如果这个bean实现了DisposableBean接口,执行bean的destroy()方法;如果这个bean配置了自定义销毁方法destroyMethod,执行这个自定义销毁方法。

在这里插入图片描述

把@RefreshScope注解修饰的bean销毁后,下一次从Spring容器中获取时,就会重新创建并初始化,那么bean的属性引用到的配置值自然就是最新的,也就是达到了配置刷新的效果。

可能有人会有疑问,为什么销毁的是GenericScope中缓存的bean,而不是从Spring的单例缓存池中清除出去呢?

这里就要说到@RefreshScope注解的原理了。

@RefreshScope注解的原理

那是因为被@RefreshScope注解修饰的bean,都不会缓存到Spring的单例缓存池,而是缓存到GenericScope的cache缓存池中。

被@RefreshScope注解修饰的bean的作用域是自定义作用域,不是单例作用域,因此不会缓存到单例缓存池中。

@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注解上面有一个@Scope(“refresh”),代表被@RefreshScope注解修饰的bean的作用域都是名为“refresh”的自定义作用域,生成的bean会放入到这个作用域对应的Scope对象中,由于这里的作用域是refresh,那么就是放入到RefreshScope中。而RefreshScope继承了GenericScope,那么就是放入到GenericScope的cache缓存池中。

在这里插入图片描述

而@RefreshScope注解的proxyMode()属性默认是ScopedProxyMode.TARGET_CLASS。

	/*** Create a class-based proxy (uses CGLIB).*/TARGET_CLASS

可以看到注释上说使用CGLIB。

那么依赖到这个bean的属性,注入进去的不是这个bean本身,而是一个用CGLIB生成的代理对象。然后调用这个属性的方法时,调用的其实是这个代理对象,会通过代理对象调用GenericScope的get()方法从GenericScope的cache缓存池中获取到这个bean,然后调用这个bean的对应方法。

好像很抽象,画个图就知道了。

在这里插入图片描述

因此这样就形成了闭环:@Scope(“refresh”)使得该bean缓存在GenericScope的cache缓存池中,而proxyMode()属性是ScopedProxyMode.TARGET_CLASS使得引用该bean的属性被注入的都是CGLIB生成的代理对象,代理对象的增强逻辑又会从GenericScope的cache缓存池中取到真正的bean并调用对应方法。

在这里插入图片描述

我们看看GenericScope#get()方法:

	public Object get(String name, ObjectFactory<?> objectFactory) {// objectFactory包装成BeanLifecycleWrapper,放入cache中BeanLifecycleWrapper value = this.cache.put(name,new BeanLifecycleWrapper(name, objectFactory));...try {// 调用BeanLifecycleWrapper的getBean()方法return value.getBean();}catch (...) {...}}

再看一下BeanLifecycleWrapper的getBean()方法:

		public Object getBean() {// bean属性为空,加双重锁,调用objectFactory.getObject()创建// 如果bean顺序不为空,则不会再创建if (this.bean == null) {synchronized (this.name) {if (this.bean == null) {this.bean = this.objectFactory.getObject();}}}// 返回beanreturn this.bean;}

这个objectFactory是个什么东西呢?答案就在Spring的AbstractBeanFactory的doGetBean方法中:

在这里插入图片描述

objectFactory就是上面的这个lambda表达式,this.objectFactory.getObject()会调用createBean()方法创建bean。

而这里的scope对象就是RefreshScope,scope.get(beanName, () -> {…})会调用到GenericScope的get方法。

在这里插入图片描述

因此,把GenericScope中的cache缓存池清空了,那么下次再次获取该bean时,就会重新创建,重新创建的bean引用的配置就会被赋值为最新的配置值(因为此时Environment中的配置已被覆盖为最新的配置值),这也就是为什么被@RefreshScope注解修饰的bean的属性具有动态刷新的效果的原因。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Spring IDEA 2024 安装Lombok插件

1.简介 Lombook插件的Data标签可以自动生成类的get和set以及toString方法。 2.安装步骤 在idead设置的插件中搜索lombok插件&#xff0c;安装。 在Spring项目的pom.xml中添加依赖项 <dependency><groupId>org.projectlombok</groupId><artifactId…

2024年研赛 C、D、F三题论文首发+部分代码分享

本届研赛助攻题目 C D F三题论文均已经全部完成。后更新计划 如图所示。 免费给大家分享 三个问题的论文部分代码 2024年华为杯-研赛分享资料&#xff08;论文部分代码&#xff09;&#xff08;已更新部分代码&#xff09;&#xff1a; 链接&#xff1a;https://pan.baidu.com…

【HTML样式】加载动画专题 每周更新

加载动画专题 煎蛋加载动画方块移动加载动画电子风变脸正方体组合跳跃式加载动画 煎蛋加载动画 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width…

Science Robotic 内在触觉实现直观的物理人机交互

触觉传感器和电子皮肤是为机器人提供物理交互感的常见设备&#xff0c;但当用于机器人的大面积覆盖时&#xff0c;它们会变得复杂且昂贵。德国宇航中心近期发表的Science Robotics研究工作&#xff0c;使用内部高分辨率关节力扭矩传感器&#xff0c;在机械臂中实现了固有的全身…

读构建可扩展分布式系统:方法与实践11强一致性

1. 强一致性 1.1. 最终一致数据库通过跨多台机器分区和复制数据集来获得可扩展性&#xff0c;其代价是要跨副本维持强数据一致性以及允许冲突写入 1.1.1. 在更新数据对象后&#xff0c;不同的客户端可能会看到该对象的旧值或新值&#xff0c;直到所有副本都收敛到最新值 1.2…

UDS诊断ISO14229新手入门篇

前言:参考ISO-14229 UDS 诊断教程(一) UDS 由 ISO-14229 系列标准定义,ISO 14229-1 定义了诊断服务,不涉及网络及实现,只有应用层的内容。而 ISO 14229-3 则定义了 UDS 在 CAN 总线上的实现。诊断通信的过程从用户角度来看非常容易理解,诊断仪发送诊断请求(request),…

MQ入门(一):同步调用和异步调用--RabbitMQ基础入门

目录 1.初识MQ 1.1.同步调用 1.2.异步调用 1.3.技术选型 2.RabbitMQ 2.1.安装部署 2.2.RabbitMQ基本架构 2.3.收发消息 2.3.1.交换机 2.3.2.队列 2.3.3.绑定关系 2.3.4.发送消息 2.4.数据隔离 2.4.1.用户管理 2.4.2.virtual host 1.初识MQ 微服务一旦拆分&…

DIN模型实现推荐算法

1. 项目简介 项目A030-DIN的核心是通过深度学习技术实现个性化推荐算法&#xff0c;旨在为用户提供精准、高效的商品或内容推荐。在现代电子商务、社交媒体及内容平台中&#xff0c;推荐系统作为核心功能&#xff0c;极大地提升了用户体验。DIN&#xff08;Deep Interest Netw…

Codeforces Round 973 (Div. 2) - D题

传送门&#xff1a;Problem - D - Codeforces 题目大意&#xff1a; 思路&#xff1a; 尽量要 最大值变小&#xff0c;最小值变大 即求 最大值的最小 和 最小值的最大 -> 二分答案 AC代码&#xff1a; 代码有注释 #include<bits/stdc.h> using namespace std; #…

无人机+自组网:中继通信增强技术详解

无人机与自组网技术的结合&#xff0c;特别是通过中继通信增强技术&#xff0c;为无人机在复杂环境中的通信提供了稳定、高效、可靠的解决方案。以下是对该技术的详细解析&#xff1a; 一、无人机自组网技术概述 无人机自组网技术是一种利用无人机作为节点&#xff0c;通过无…

指针修仙之实现qsort

文章目录 回调函数什么是回调函数回调函数的作用 库函数qsort使用qsort函数排序整形使用qsort函数排序结构体 qsort函数模拟实现说明源码and说明 回调函数 什么是回调函数 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数…

深度学习02-pytorch-01-张量的创建

深度学习 pytorch 框架 是目前最热门的。 深度学习 pytorch 框架相当于 机器学习阶段的 numpy sklearn 它将数据封装成张量(Tensor)来进行处理&#xff0c;其实就是数组。也就是numpy 里面的 ndarray . pip install torch1.10.0 -i https://pypi.tuna.tsinghua.edu.cn/simp…

蓝桥杯【物联网】零基础到国奖之路:七. 串口

蓝桥杯【物联网】零基础到国奖之路:七. 串口 第一节 串口通信理论第二节 软件通信协议第三节 DMA理论第四节 CubeMX的配置第五节 代码模版 第一节 串口通信理论 通用异步收发传输器&#xff08;UART&#xff09;是一种串行异步收发协议&#xff0c;应用十分广泛。UART将数据二…

HashMap扩容时机是插入前还是插入后?

结论 不管是HashMap还是ConcurrentHashMap都是插入后。 过程为&#xff1a; 先计算哈希值。对应的哈希槽插入数据&#xff0c;决定是红黑树还是链表插入完毕才计算是否需要扩容&#xff0c;假如需要则扩容 源码 源码如下&#xff1a; 其中addCount方法里面写入扩容。

Dash稳定版更新

大家好&#xff0c;今天要和大家聊聊一个开发Python网页应用的超级神器——Dash 2.18.1稳定版本正式发布啦&#xff01;此次更新&#xff0c;针对2.18.0版本的问题进行了修复和优化&#xff0c;为我们带来了更为稳定、强大的开发体验。 Dash是什么&#xff1f; Dash是一款基于P…

深度学习03-神经网络02-激活函数

可以使用这个进行跳转链接​​​​​​​http://playground.tensorflow.org/#activationrelu&batchSize11&datasetspiralDatasetreg-gauss&learningRate0.01ularizationRate0.1&noise0&networkShape7,5,4,3,2&seed0.54477&showTestDatafalse&d…

Excel VLOOKUP函数怎么用?vlookup函数的使用方法及案例

大家好&#xff0c;这里是效率办公指南&#xff01; &#x1f50e; 在Excel的世界里&#xff0c;VLOOKUP函数无疑是查询和数据分析中的明星。无论是从庞大的数据表中提取特定信息&#xff0c;还是进行数据的快速匹配&#xff0c;VLOOKUP都能大显身手。今天&#xff0c;我们将深…

第15章 程序的动态加载和执行

第15章 程序的动态加载和执行 该章节讲解了MBR加载内核&#xff0c;然后内核加载用户程序这样一套流程&#xff0c;模拟操作系统的工作原理。 本章代码清单 本章的代码实现的功能位&#xff1a;主引导扇区加载内核&#xff0c;内核加载用户程序&#xff0c;用户程序通过调用…

速通LLaMA3:《The Llama 3 Herd of Models》全文解读

文章目录 概览论文开篇IntroductionGeneral OverviewPre-TrainingPre-Training DataModel ArchitectureInfrastructure, Scaling, and EfficiencyTraining Recipe Post-TrainingResultsVision ExperimentsSpeech Experiments⭐Related WorkConclusionLlama 3 模型中的数学原理1…

【力扣每日一题——2374. 边积分最高的节点】python

2374. 边积分最高的节点 给你一个有向图&#xff0c;图中有 n 个节点&#xff0c;节点编号从 0 到 n - 1 &#xff0c;其中每个节点都 恰有一条 出边。 图由一个下标从 0 开始、长度为 n 的整数数组 edges 表示&#xff0c;其中 edges[i] 表示存在一条从节点 i 到节点 edges[…