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

news2024/11/15 10:34:19

【深入理解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出一个新的Environment
			StandardEnvironment environment = copyEnvironment(
					this.context.getEnvironment());
			// SpringApplicationBuilder是SpringApplication构造器
			// 会创建一个新的SpringApplication
			SpringApplicationBuilder 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注解修饰的bean
		super.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();
		// 遍历上面返回的每个bean
		for (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();
					}
				}
			}
			// 返回bean
			return 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.coloradmin.cn/o/2151987.html

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

相关文章

记录一次显卡驱动安装

1. 驱动安装 1.1. 查看适合的版本 apt-get update ubuntu-drivers devices输出结果&#xff1a; 1.2. 安装合适的驱动版本 根据上面输出的内容 apt-get install nvidia-driver-545完成后重启 reboot查看新的驱动 nvidia-smi2. 安装/升级cuda 在nvidia-smi中显示的CUDA…

伊犁云计算22-1 apache 安装rhel8

1 局域网网络必须通 2 yum 必须搭建成功 3 apache 必须安装 开干 要用su 用户来访问 一看httpd 组件安装完毕 到这里就是测试成功了 如何修改主页的目录 网站目录默认保存在/var/WWW/HTML 我希望改变/home/www 122 127 167 行要改

AI 基础设施:构建AI时代全栈云计算体系

生成式AI 新时代下催生新的基础设施需求 随着企业在数字化转型之路上越走越远&#xff0c;期间一场新的技术革命正在发生&#xff0c;近几年涌现的生成式AI技术正在迅速改变科技、商业和整个社会的格局。这种强大的技术能够从数据中学习并生成预测性输出&#xff0c;生成式 AI …

Listener经典案例-在线用户统计

前言 要完成在线用户统计功能的监听器&#xff0c;需要实现如下3个接口。 ServletContextListener接口 使用此接口的作用是&#xff1a;在应用初始化的时候向application中添加一个空的Set集合用来保存在线用户。HttpSessionAttributeListener接口 使用此接口的作用是&#xff…

【经验技巧】IBIS AMI模型眼图仿真问题探讨

最近&#xff0c;有同事问我&#xff1a;“拿到供应商的IBIS AMI模型&#xff0c;怎么判断是否可以进行应力&#xff08;统计&#xff09;眼图的仿真呀&#xff1f;如果不能进行&#xff0c;又怎么判断结果是瞬态仿真呢&#xff1f;” 不得不说&#xff0c;这的确是一个不错的话…

VMware虚拟机密码忘记了怎么办

1.首先&#xff0c;启动系统&#xff0c;进入开机界面&#xff0c;在界面中按“e”进入编辑界面 2.进入编辑界面&#xff0c;使用键盘上的上下键把光标往下移动&#xff0c;找到以““Linux16”开头内容所在的行数”&#xff0c;在行的最后面输入&#xff08;最好把前面的语言改…

JVM 调优篇8 调优案例6- 计算合理设置内存大小

一 jmap查看堆结构配置 1.1 逻辑流程 # 查看进程ID jps -l # 查看对应的进程ID的堆内存分配 jmap -heap 3725 1.2 案例演示 1.代码 public class AdaptiveSizePolicyTest {public static void main(String[] args) {try {Thread.sleep(1000000);} catch (Interrupted…

MFC-基础架构

前言 各位师傅大家好&#xff0c;我是qmx_07&#xff0c;今天讲解MFC的基础架构 概述 介绍&#xff1a;MFC&#xff08;Microsoft Foundation Classes&#xff09;是微软公司提供的一个类库&#xff0c;用于在 Windows 操作系统下进行 C 应用程序开发MFC把Windows SDK API函…

一堆让你眼界大开的实用工具网站——搜嗖工具箱

和图书 https://www.hetushu.com/ 一个好用的免费看小说网站。和图书是一个提供各种热门电子书,书籍,小说免费在线阅读的网站&#xff0c;涵盖网游、玄幻、穿越、科幻、仙侠、都市、武侠、历史、竞技、军事灵异等多个种类的小说。在这个网站看小说最大的感触简单干净&#xff…

数据标注——AI智能时代的关键之钥

洞见AI+专题 篇首语 在这个充满无限可能的时代,人工智能正以前所未有的速度改变着我们的世界。从日常生活的便利到行业效率的飞跃,AI技术的应用几乎无处不在。在银行业务中,同样可以看到AI带来的巨大潜力。本专题旨在展示农业银行科技部门在AI技术应用上的最新探索与实践成…

力扣之178.分数排名

1. 178.分数排名 1.1 题干 表: Scores -------------------- | Column Name | Type | -------------------- | id | int | | score | decimal | -------------------- id 是该表的主键&#xff08;有不同值的列&#xff09;。 该表的每一行都包含了一场比赛的分数。Score 是…

Docker+PyCharm远程调试环境隔离解决方案

DockerPyCharmMiniconda实现深度学习代码远程调试和环境隔离 本文详细介绍了如何在局域网环境下&#xff0c;利用Docker、PyCharm和Miniconda构建一个高效的深度学习远程调试平台。首先在服务器&#xff08;server&#xff09;上&#xff0c;通过Docker构建包含不同CUDA环境的镜…

MCS-51汇编

伪指令&#xff1a; EQU: Equal&#xff0c;定义常量 COUNT EQU 10H ; 定义一个符号名COUNT&#xff0c;其值为10H DELAY EQU 500 ; 定义一个符号名DELAY&#xff0c;其值为500 数据传送&#xff1a; MOV: MOVE&#xff0c;传送数据 MOVC: 算术运算&#xff1a; 跳转…

详解npm源及其使用方法

详解npm源及其使用方法 npm源是一个用于存储和提供npm包的服务器地址&#xff0c;npm在安装包时会通过这个源地址下载对应的依赖包。默认情况下&#xff0c;npm使用官方的npm源&#xff08;https://registry.npmjs.org/&#xff09;&#xff0c;该源存储了海量的Node.js开源包…

Android Studio 汉化教程,直接授人以渔,又菜又爱学英语还不好,不愧是我

Android Studio 汉化教程,直接授人以渔 查看使用的 Android Studio 版本号 当前版本号&#xff1a;241.18034.62.2412.12266719 打开官网插件地址 插件地址选择对应版本进行下载 版本怎么选&#xff1f; 我的版本号 241.18034.62.2412.12266719选择的版本号只有前三位对应的…

【JAVA开源】基于Vue和SpringBoot的网上超市系统

本文项目编号 T 037 &#xff0c;文末自助获取源码 \color{red}{T037&#xff0c;文末自助获取源码} T037&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

全国31省对外开放程度、经济发展水平、政府干预程度指标数据(2000-2022年)

旨在分析2000-2022年间中国31个省份的对外开放程度、经济发展水平和政府干预程度&#xff0c;探讨其背后的动因与影响。 2000年-2022年 全国31省对外开放程度、经济发展水平、政府干预程度指标数据https://download.csdn.net/download/2401_84585615/89478612 数据概览 对外…

Hexo博客私有部署Twikoo评论系统并迁移评论记录(自定义邮件回复模板)

部署 之前一直使用的artalk&#xff0c;现在想改用Twikoo&#xff0c;采用私有部署的方式。 私有部署 (Docker) 端口可以根据实际情况进行修改 docker run --name twikoo -e TWIKOO_THROTTLE1000 -p 8100:8100 -v ${PWD}/data:/app/data -e TWIKOO_PORT8100 -d imaegoo/twi…

英集芯IP5912:集成开关充电功能的低功耗8位POWER MCU芯片

英集芯IP5912是一款功能丰富的、集成了降压充电管理功能的8位MCU芯片&#xff0c;它内置了一个5V输入的同步降压充电DC-DC&#xff0c;功率管也是内置的&#xff0c;同时提供最大1.5A的充电电流。封装方式采用SOP16&#xff0c;方案应用时只需要很少的外围器件&#xff0c;就可…

Java面试篇基础部分-ReentrantLock详解(二)

Lock 接口的主要方法 void lock():给对象加锁,如果锁没有被其他线程使用,则当前线程获取到这个锁;如果锁正在被其他线程持有,则将禁用当前线程,直到当前线程获取到锁。boolean tryLock():试图给对象进行加锁操作,如果锁没有被其他线程使用,则将获取到这个锁并且返回tr…