Spring循环引用和三级缓存

前言

Spring 解决 Bean 之间的循环引用关系用到了三级缓存,那么问题来了。三级缓存是怎么用的?每一层的作用是什么?非得用三级吗?两级缓存行不行?

理解循环引用

所谓的“循环引用”是指 Bean 之间的依赖关系形成了一个循环,例如 a 依赖 b,b 又依赖 a。

@Component
public class A {@AutowiredB b;
}@Component
public class A {@AutowiredB b;
}

开发者在设计阶段,应该尽量避免出现循环引用,因为这种依赖关系本身就违反了“单一职责”的设计原则,增加了代码的复杂性和维护成本。
抛开设计原则不谈,循环引用给 Spring 带来了依赖注入的问题。在这个例子中,Spring 实例化 a 时发现要为其注入 b,于是赶忙去实例化 b,实例化 b 的过程中又发现要为其注入 a,但此时 a 还没创建完,陷入一个死循环,就像死锁一样。
为什么多级缓存可以解决循环引用的问题呢?

理解多级缓存

Spring 里的三级缓存其实就是三个 Map 容器,先抛开 Spring 不谈,如果让我们设计一个多级缓存方案来解决循环引用的问题,我们会怎么做呢?

一级缓存

一级缓存的作用是为了保证单例。在 Spring 里面,Bean 默认是单例的,多次调用getBean() 得到的是同一个实例。基于这个规则,我们可以用一级缓存来实现一个保证单例的 IOC 容器。

public class Ioc1 {private final Map<Class, Object> singletonObjects = new ConcurrentHashMap<>();public synchronized <T> T getBean(Class<T> clazz) {Object bean = singletonObjects.get(clazz);if (bean == null) {singletonObjects.put(clazz, bean = createBean(clazz));}return (T) bean;}@SneakyThrowsprivate <T> Object createBean(Class<T> clazz) {T bean = clazz.newInstance();return bean;}
}

二级缓存

一级缓存只能保证单例,对于循环引用是没办法解决的。因此,我们可以再加一级缓存,引入二级缓存来解决循环引用问题。于是,我们实现了第二版 IOC 容器,核心思路是把未被初始化的 bean 提前暴露到二级缓存,依赖注入时允许注入半成品 bean

public class Ioc2 {private final Map<Class, Object> singletonObjects = new ConcurrentHashMap<>();private final Map<Class, Object> earlySingletonObjects = new ConcurrentHashMap<>();public synchronized <T> T getBean(Class<T> clazz) {Object bean = singletonObjects.get(clazz);if (bean == null) {bean = earlySingletonObjects.get(clazz);if (bean == null) {singletonObjects.put(clazz, bean = createBean(clazz));}}return (T) bean;}@SneakyThrowsprivate <T> Object createBean(Class<T> clazz) {T bean = clazz.newInstance();// 把未被初始化的bean提前暴露到二级缓存earlySingletonObjects.put(clazz, bean);populateBean(bean);return bean;}// 属性注入@SneakyThrowsprivate <T> void populateBean(T bean) {for (Field field : bean.getClass().getDeclaredFields()) {Object fieldValue = getBean(field.getType());field.setAccessible(true);field.set(bean, fieldValue);}}
}

三级缓存

大多数情况下,二级缓存已经够用了,但是 Spring 还有一项强大的功能:基于 Bean 生成代理对象做增强。
此时,用二级缓存就面临一个问题:代理对象何时生成?

  • 如果等 Bean 初始化后再生成 Proxy,那已经被注入的属性却是个未被代理 Bean,这显然是不能接受的
  • 如果 Bean 初始化前就提前生成代理对象,这样能保证注入的属性也是被代理的 Bean,但是这不符合 Spring 的设计原则

为什么提前生成代理对象不符合 Spring 的设计原则呢???
因为在 Spring 的 Bean 和 AOP 的生命周期里,应该是先实例化并初始化 Bean 以后,再调用 BeanPostProcessor 的子类 AbstractAutoProxyCreator 的后处理器方法来生成代理对象。提前基于未被初始化的半成品 Bean 生成代理对象,这一点违背了 Spring 的设计原则,后处理器理应认为要扩展的 Bean 是一个完整的 Bean,万一需要访问其属性,拿到的却是 null,可能导致程序错误。

所以,我们可以得出一个结论:Spring 循环引用的问题,只用二级缓存是完全没问题的,前提是要提前生成代理 Bean 并暴露到二级缓存。功能是没问题,但是这一点违背了 Spring 的设计原则,所以 Spring 要尽量避免这个问题,才引入的三级缓存。

Spring 给出的方案是:引入一个三级缓存,尽可能避免提前创建代理对象,万一真的发生了循环引用,不得已而为之,也只能提前生成了。
所以,我们现在可以给出一个最终版的 IOC 实现了,逻辑基本和 Spring 一致。核心思路是,Bean 实例化以后,提前暴露一个 ObjectFactory 到三级缓存,如果没有循环引用,代理对象不会提前创建,Bean 的生命周期保持一致。如果发生了循环引用,就只能提前创建代理对象,并把它从三级缓存挪到二级缓存,避免重复创建,其它 Bean 注入的也是被代理后的 Bean。

public class Ioc3 {private final Map<Class, Object> singletonObjects = new ConcurrentHashMap<>();private final Map<Class, Object> earlySingletonObjects = new ConcurrentHashMap<>();private final Map<Class, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>();// 代理对象缓存private final Map<Class, Object> proxyCache = new ConcurrentHashMap<>();public synchronized <T> T getBean(Class<T> clazz) {Object bean = singletonObjects.get(clazz);if (bean == null) {bean = earlySingletonObjects.get(clazz);if (bean == null) {ObjectFactory<?> objectFactory = singletonFactories.get(clazz);if (objectFactory != null) {bean = objectFactory.getObject();earlySingletonObjects.put(clazz, bean);singletonFactories.remove(clazz);} else {bean = createBean(clazz);singletonObjects.put(clazz, bean);}}}return (T) bean;}@SneakyThrowsprivate <T> Object createBean(Class<T> clazz) {T bean = clazz.newInstance();singletonFactories.put(clazz, () -> wrapIfNecessary(bean));populateBean(bean);return wrapIfNecessary(bean);}private <T> T wrapIfNecessary(final T bean) {if (true) {Object proxy = proxyCache.get(bean.getClass());if (proxy == null) {proxy = ProxyFactory.getProxy(bean.getClass(), new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {if (method.getDeclaringClass().isAssignableFrom(Object.class)) {return method.invoke(this, args);}System.err.println("before...");Object result = method.invoke(bean, args);System.err.println("after...");return result;}});proxyCache.put(bean.getClass(), proxy);}return (T) proxy;}return bean;}@SneakyThrowsprivate <T> void populateBean(T bean) {for (Field field : bean.getClass().getDeclaredFields()) {Object fieldValue = getBean(field.getType());field.setAccessible(true);field.set(bean, fieldValue);}}
}

Spring三级缓存实现

Spring 三级缓存对应的 Map 声明在 DefaultSingletonBeanRegistry 类,如下:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

一级缓存 singletonObjects:存放完整的,初始化后的 Bean
二级缓存 singletonFactories:存放未被初始化的半成品 Bean
三级缓存 earlySingletonObjects:存放 Bean 对应的 ObjectFactory,用于提前生成代理对象

在获取单例 Bean 时,Spring 会先查找一级和二级缓存,都没有时再查找三级缓存,如果找到了,就提前创建代理对象并返回。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {// 查一级缓存Object singletonObject = this.singletonObjects.get(beanName);// 没有且Bean在创建中if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {// 查二级缓存取singletonObject = this.earlySingletonObjects.get(beanName);// 没有且允许引用半成品Beanif (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// double checksingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {// 三级缓存如果有,创建代理对象,挪到二级缓存ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}

如果不存在循环引用,ObjectFactory#getObject 就不会被调用,也就不会提前创建代理对象。
doCreateBean() 方法里,会提前把未被初始化的半成品 Bean 封装成 ObjectFactory 暴露到三级缓存:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){......boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}......
}

getObject() 方法也就是getEarlyBeanReference() ,它会调用后处理器SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference 生成代理对象(未必所有的Bean都需要被代理),目前只有一个实现类 AbstractAutoProxyCreator:

@Override
public Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);this.earlyProxyReferences.put(cacheKey, bean);return wrapIfNecessary(bean, beanName, cacheKey);
}

尾巴

Spring 为了解决循环引用的问题,设计了三级缓存。一级缓存的作用是保证单例;二级缓存的作用是解决循环引用;三级缓存是为了尽量避免基于半成品 Bean 提前创建代理对象。单从功能上说,只用二级缓存完全没问题,前提是被代理的 Bean 都要提前创建代理对象,这一点违背了 Spring 的设计原则。在 Spring Bean 的生命周期里,应该是先实例化并初始化 Bean 后再通过后处理器进行扩展,对一个未被初始化的 Bean 做扩展,万一要访问其属性可能就会导致程序错误。Spring 的做法是提前暴露一个 ObjectFactory 对象,如果没有发生循环引用,其getObject() 就不会被调用,代理对象就不会提前生成,尽可能的保证 Bean 生命周期一致。
但是,如果真的发生了循环引用,且引用的 Bean 又是需要被代理的,也只能提前生成代理对象了。因为这么做违背了 Spring 的设计原则,所以新版本的 Spring 默认已经不允许循环引用了,必须手动开启。

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

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

相关文章

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷④

2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷4 目录 需要竞赛软件包环境以及备赛资源可私信博主&#xff01;&#xff01;&#xff01; 2023年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算应用”赛项赛卷4 模块一 …

一键搭建elk

一键启动elk 1. 生成环境的脚本 setup.sh #!/usr/bin/bash# logstash enviroment mkdir -p logstash touch logstash/logstash.conf # shellcheck disableSC1078 echo input {tcp {mode > "server"host > "0.0.0.0"port > 4560codec > jso…

多PC文件夹同步方案

在多个工作终端独立具备svn版本库的情况下&#xff0c;可使用本工具进行一键同步。 相较于传统的SVN中心检出更新方案中移动存储设备硬件及文件目录系统多终端间易损坏&#xff0c;本方案更加稳定 资料同步结构&#xff1a; 使用步骤&#xff1a; 1.修改config.ini配置文件 2…

【C++】HP-Socket(二):框架介绍、功能说明

1、接口 1.1 接口模型 1.2 三类接口 HP-Socket 定义了三类接口 组件接口&#xff08;如&#xff1a;ITcpServer / IUdpClient&#xff09;&#xff1a;声明组件操作方法&#xff0c;应用程序创建组件对象后通过该接口来使用组件组件实现类&#xff08;如&#xff1a;CTcpSer…

Mac 安装Nginx教程

Nginx官网 Nginx官网英文 1.在终端输入brew search nginx 命令检查nginx是否安装了 2. 安装命令&#xff1a;brew install nginx 3. 查看Nginx信息命令brew info nginx 4. 启动 nginx方式&#xff1a;在终端里输入 nginx 5.查看 nginx 是否启动成功 在浏览器中访问http://l…

powerdesigner导出sql将name放到comment注释上

1. 批量设置 2. 脚本 Option Explicit ValidationMode True InteractiveMode im_Batch Dim mdl the current modelget the current active model Set mdl ActiveModel If (mdl Is Nothing) ThenMsgBox"There is no current Model " ElseIf Not mdl.IsKindOf(PdPD…

JSON数据处理

1.添加json依赖 springmvc 默认使用jackson作为json类库,不需要修改applicationContext-servlet.xml任何配置&#xff0c;只需引入以下类库springmvc就可以处理json数据&#xff1a; <!--spring-json依赖--> <dependency><groupId>com.fasterxml.jackson.c…

构建自己的私人GPT-支持中文

上一篇已经讲解了如何构建自己的私人GPT&#xff0c;这一篇主要讲如何让GPT支持中文。 privateGPT 本地部署目前只支持基于llama.cpp 的 gguf格式模型&#xff0c;GGUF 是 llama.cpp 团队于 2023 年 8 月 21 日推出的一种新格式。它是 GGML 的替代品&#xff0c;llama.cpp 不再…

【cmu15445c++入门】(2)c++中的std::move() 左值引用右值引用

左值右值 要理解move语义&#xff0c;必须理解左值和右值的概念。左值的简化定义是左值是对象&#xff0c;指向内存中某个位置。右值是左值之外的任何。 std::move() move语义&#xff0c;在C中是一个有用的方法&#xff0c;它允许在对象之间高效和优化地转移数据所有权。m…

SpringMVC SpringMVC概述

1.1.MVC介绍 MVC是一种设计模式&#xff0c;将软件按照模型、视图、控制器来划分&#xff1a; M&#xff1a;Model&#xff0c;模型层&#xff0c;指工程中的JavaBean&#xff0c;作用是处理数据 JavaBean分为两类&#xff1a; 一类称为数据承载Bean&#xff1a;专门存储业务数…

vue3 useAttrs的使用场景,提取共有props

1 场景 多个类似组件都需要传参data&#xff0c;用于请求接口或者处理数据&#xff0c;想让组件干净整洁&#xff0c;把参数data提出来 2 方法 选项式 可以使用mixin混入或者extends继承&#xff08;略&#xff09; 组合式 可以使用hook 无脑式踩坑&#xff08;如下代码…

LabVIEW在设备状态监测与故障诊断中的应用

在现代工业自动化领域&#xff0c;LabVIEW的系统设计平台在设备状态监测与故障诊断中扮演着举足轻重的角色。通过提供一个可视化和数据流编程语言&#xff0c;LabVIEW大大提升了设备安全监测的效率&#xff0c;减少了系统维护成本&#xff0c;同时增强了设备的可靠性和可维护性…

Verilog 高级教程笔记——持续更新中

Verilog advanced tutorial 转换函数 调用系统任务任务描述int_val $rtoi( real_val ) ;实数 real_val 转换为整数 int_val 例如 3.14 -> 3real_val $itor( int_val ) ;整数 int_vla 转换为实数 real_val 例如 3 -> 3.0vec_val $realtobits( real_val ) ;实数转换为…

Yolov5水果分类识别+pyqt交互式界面(附代码)

本文介绍了基于Yolov5模型的水果分类识别系统以及使用PyQt库构建的交互式界面。 首先&#xff0c;Yolov5是一种目标检测算法&#xff0c;它可以通过输入图片&#xff0c;自动识别出其中包含的不同目标&#xff0c;并标注出它们的位置和类别。我们利用Yolov5模型对水果图片进行…

【算法Hot100系列】有效的数独

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…

6款实用的Git可视化管理工具

前言 俗话说得好“工欲善其事&#xff0c;必先利其器”&#xff0c;合理的选择和使用可视化的管理工具可以降低技术入门和使用门槛。我们在团队开发中统一某个开发工具能够降低沟通成本&#xff0c;提高协作效率。今天给大家分享6款实用的Git可视化管理工具。 Git是什么&…

01-连接池项目背景:C++的数据库操作

从0开始学习C与数据库的联动 1.原始方式-使用MySQL Connector/C 提供的API查询 1.1 数据库预操作 我的本地电脑上有mysql数据库&#xff0c;里面预先创建了一个database名叫chat&#xff0c;用户名root&#xff0c;密码password。 1.2 Visual Studio预操作 在Windows上使用…

openGauss学习笔记-189 openGauss 数据库运维-常见故障定位案例-TPCC-WAL-内存

文章目录 openGauss学习笔记-189 openGauss 数据库运维-常见故障定位案例-TPCC-WAL-内存189.1 TPCC运行时&#xff0c;注入磁盘满故障&#xff0c;TPCC卡住的问题189.1.1 问题现象189.1.2 原因分析189.1.3 处理分析 189.2 备机处于need repair(WAL)状态问题189.2.1问题现象189.…

1876_电感的特性小结

Grey 全部学习内容汇总&#xff1a; GitHub - GreyZhang/g_hardware_basic: You should learn some hardware design knowledge in case hardware engineer would ask you to prove your software is right when their hardware design is wrong! 1876_电感的特性小结 主要是…

何小鹏的「超级爆品」X9诞生背后

作者 |张祥威 编辑 |德新 小鹏汽车将X9的上市日期定在了2024年的第一天&#xff0c;也是元旦假期的最后一天。 内部选择这天上市发布&#xff0c;据HiEV了解&#xff0c;这既包含了X9要争夺纯电MPV品类销冠的寓意&#xff08;No.1&#xff09;&#xff0c;也是告诉内外部&…