当前位置: 首页 > news >正文

Spring 三级缓存:破解循环依赖的底层密码

一、循环依赖的 “死结” 与 Spring 的破局思路

在软件开发中,对象之间的依赖关系是常态,但当这种依赖形成闭环时,就会出现循环依赖问题。循环依赖的本质是对象创建与依赖注入的时序冲突。例如,对象 A 在创建过程中需要注入对象 B,而对象 B 在创建过程中又需要注入对象 A,这就像两个人互相等待对方先递东西,陷入了无限等待的僵局。​

普通容器在面对这种情况时往往束手无策,因为直接通过 new 关键字创建对象的方式,无法在对象未完全创建时就将其引用传递给其他对象。当尝试创建 A 时,发现需要 B,于是去创建 B,创建 B 时又发现需要 A,而此时 A 还未创建完成,最终导致创建失败。​

Spring 作为优秀的 Java 企业级应用框架,创新性地提出了三级缓存的设计来解决这一难题。其核心动机是在对象创建过程中,提前暴露对象的引用,让其他依赖该对象的 Bean 能够获取到一个早期引用,从而打破循环等待的局面,待所有对象都完成创建和属性注入后,再将完整的对象放入缓存供应用使用。

二、解密三级缓存:数据结构与存储内容

1. 一级缓存(singletonObjects):成品对象的 “仓库”

一级缓存是一个 HashMap 结构,主要存储完全初始化完成的单例 Bean(最终可用的Bean)。这些 Bean 已经经历了实例化、属性注入、初始化等所有流程,是可以直接供应用程序使用的成品对象。​

它的核心作用就是为应用提供一个获取可用对象的统一入口,当应用需要获取某个单例 Bean 时,首先会到一级缓存中查找,如果存在则直接返回,避免了重复创建对象,提高了系统性能。

2.二级缓存(earlySingletonObjects):提前暴露的 “半成品”

二级缓存同样是一个 HashMap,存储的是已实例化但未完成属性注入的 Bean。这些 Bean 就像还在生产线上的半成品,虽然已经有了基本的形态(实例化完成),但还不具备完整的功能(属性未注入)。​

二级缓存与一级缓存的转换时机是在 Bean 的属性注入完成之后。当 Bean 完成属性注入和初始化操作后,Spring 会将其从二级缓存移至一级缓存,标志着该 Bean 已经成为可用的成品。

3. 三级缓存(singletonFactories):对象的 “生产工厂”

三级缓存也是一个 HashMap,存储的是 Bean 的工厂对象(ObjectFactory)。这些工厂对象负责生成 Bean 的早期引用。​

其中的核心方法是 getObject (),当需要获取 Bean 的早期引用时,会调用该方法。这个方法能够在 Bean 实例化之后,属性注入之前,生成一个 Bean 的早期引用,并将其暴露出去,为解决循环依赖提供了关键支持。

4.比较

缓存名称作用
一级缓存(singletonObjects)存储完全初始化完成的单例 Bean(最终可用的 Bean)。
二级缓存(earlySingletonObjects)存储实例化完成但未初始化的早期暴露 Bean(未填充属性、未执行初始化方法)。
三级缓存(singletonFactories)存储Bean 工厂对象ObjectFactory),用于生成早期暴露的 Bean 实例(可能是原始对象或代理对象)。

三、三级缓存的协作流程:循环依赖的解决步骤

以 A 依赖 B,B 依赖 A 为例

1.创建BeanA

  • 实例化A(调用构造器创建对象,但并未属性注入与初始化)

  • 将A的工厂对象(ObjectFactory)放入三级缓存中(用于后续生成A的早期实例)

  • 为A填充属性发现依赖B,暂停A的创建,去创建B

2.创建BeanB

  • 实例化B(同样未初始化)

  • 将B的工厂对象放入三级缓存中

  • 开始为B填充属性,发现依赖于A,尝试从缓存中获取A(首先从一级缓存中找,然后是二三级):

    • 一级缓存中没有(A未被初始化)

    • 二级缓存中没有(A尚未被早期暴露(已实例化,但未属性注入于初始化))

    • 从三级缓存中获取到A的工厂对象,通过工厂生成A的早期实例(通过调用该 ObjectFactory 的 getObject () 方法获取到 A 的早期实例)(若A需要AOP代理,此时会生成代理对象),并将A的早期实例放入二级缓存中,同时删除三级缓存中的A工厂

  • B获取到A的早期实例后,完成属性注入与初始化,将B放入一级缓存中(同时移除掉三级缓存中的B的工厂对象)

3.继续创建BeanA

  • B已经置于一级缓存中,A获取到B并完成属性注入与初始化

  • 将A放入一级缓存中,同时删除掉二级缓存中A的早期实例

4. 关键转折点:何时从三级缓存升级到二级缓存​

当某个 Bean 在创建过程中,需要依赖另一个处于创建中的 Bean(即发生循环依赖),且在三级缓存中找到了对应的 ObjectFactory 时,会调用 ObjectFactory 的 getObject () 方法获取该 Bean 的早期引用,随后将该 Bean 从三级缓存升级到二级缓存。这个转折点的出现,确保了循环依赖中的 Bean 能够获取到所需的早期引用,推动 Bean 的创建过程继续进行。

四、特殊场景:三级缓存失效的两种情况

1. 构造器注入循环依赖:为何三级缓存无力解决

构造器与 setter 注入的本质区别:

setter 注入是在对象实例化之后进行的,此时对象已经存在,可以通过三级缓存提前暴露早期引用;而构造器注入是在对象实例化的过程中进行的,在对象还未实例化完成时就需要依赖其他对象,此时三级缓存中还没有该对象的相关信息,无法提供早期引用。

当出现构造器注入循环依赖时,Spring 会抛出 BeanCurrentlyInCreationException 异常。该异常表明当前正在创建的 Bean 之间存在循环依赖,且由于构造器注入的特性,三级缓存无法解决这种情况。

2. 原型 Bean 的循环依赖:缓存设计的天然限制

原型模式下 Bean 的创建逻辑:在原型模式下,每次获取 Bean 时,Spring 都会创建一个新的 Bean 实例,而不会对其进行缓存。这与单例模式下的缓存机制有本质区别。​

Spring 对原型循环依赖的处理策略:由于原型 Bean 不会被缓存,三级缓存无法对其进行有效的管理和引用传递,因此当原型 Bean 之间存在循环依赖时,Spring 无法解决,会直接抛出异常。

3.总结

Spring的三级缓存机制只能解决单例Bean的循环依赖问题(不能解决原型Bean的循环依赖问题),并且仅支持setter注入与属性注入(构造器注入的循环依赖问题也不能解决)

五、三级缓存的设计智慧:为什么需要三级而不是两级?

1. 二级缓存的局限性:无法处理 AOP 代理场景

代理对象的创建时机与循环依赖的冲突:在使用 AOP 时,Spring 需要为 Bean 创建代理对象。代理对象的创建通常是在 Bean 初始化完成之后进行的。如果只有两级缓存,当发生循环依赖时,暴露的是原始对象的早期引用,而不是代理对象,这会导致依赖注入的是原始对象,而不是预期的代理对象,引发问题。

三级缓存如何通过工厂延迟生成代理:三级缓存中存储的 ObjectFactory 可以在需要的时候生成代理对象。当发生循环依赖时,通过 ObjectFactory 的 getObject () 方法,能够在适当的时机生成代理对象的早期引用,并将其暴露出去,确保依赖注入的是代理对象,解决了 AOP 代理场景下的循环依赖问题。

2. 三级缓存的性能考量:减少不必要的代理创建

如果只有两级缓存,为了应对可能的循环依赖和 AOP 代理场景,需要在 Bean 实例化后立即创建代理对象并放入二级缓存。但实际上,很多 Bean 可能并不会发生循环依赖,此时提前创建代理对象就造成了不必要的性能消耗。三级缓存通过 ObjectFactory 延迟生成代理对象,只有在确实发生循环依赖时才会创建代理对象,减少了不必要的代理创建,提高了系统性能。

六、总结:三级缓存的价值与启示

1. 三级缓存对 Spring 容器设计的意义

三级缓存是 Spring 容器设计中的一个精妙之处,它成功解决了单例 Bean 之间的循环依赖问题,尤其是在存在 AOP 代理的场景下,保证了 Spring 容器能够正常创建和管理 Bean。这一设计极大地提升了 Spring 容器的灵活性和可靠性,使其能够应对复杂的业务场景。

2. 对开发者的启示:依赖设计的最佳实践​

三级缓存的存在虽然解决了循环依赖问题,但这并不意味着开发者可以随意设计循环依赖的代码。在实际开发中,应该尽量避免循环依赖,通过合理的代码设计,如采用依赖注入的最佳实践、进行模块拆分等,减少循环依赖的产生。当不可避免地出现循环依赖时,要了解三级缓存的工作原理,以便更好地排查和解决问题。

http://www.lryc.cn/news/625011.html

相关文章:

  • 使用Idea安装JDK
  • [Code Analysis] docs | Web应用前端
  • 计算机视觉(9)-实践中遇到的问题(六路相机模型采集训练部署全流程)
  • OpenTelemetry、Jaeger 与 Zipkin:分布式链路追踪方案对比与实践
  • 大模型的底层运算线性代数
  • 关系型数据库与非关系型数据库
  • 母猪姿态转换行为识别:计算机视觉与行为识别模型调优指南
  • 我的 LeetCode 日记:Day 9 - 字符串终章与 KMP 算法
  • Baumer高防护相机如何通过YoloV8深度学习模型实现手势识别和指尖检测识别(C#代码UI界面版)
  • 第十六届蓝桥杯青少组C++省赛[2025.8.10]第二部分编程题(6、魔术扑克牌排列)
  • 算法题——字符串
  • RecSys:排序中的融分公式与视频播放建模
  • OVS:ovn为什么默认选择Geneve作为二层隧道网络协议?
  • 【EI会议征稿通知】第五届高性能计算、大数据与通信工程国际学术会议(ICHBC 2025)
  • 人工智能与生物科技的融合:重塑生命未来的无限可能​
  • android 实现表格效果
  • 力扣(LeetCode) ——100. 相同的树(C语言)
  • Rust 异步中的 Waker
  • PMP-项目管理-十大知识领域:资源管理-管理团队、设备、材料等资源
  • OpenCV Python——Numpy基本操作(Numpy 矩阵操作、Numpy 矩阵的检索与赋值、Numpy 操作ROI)
  • 3D检测笔记:基础坐标系与标注框介绍
  • JAiRouter 架构揭秘:一个面向 AI 时代的响应式网关设计
  • JUC读写锁
  • 宁波市第八届网络安全大赛初赛(REVERSE-Writeup)
  • 基于Spring Boot+Vue的社区便民服务平台 智慧社区平台 志愿者服务管理
  • day25|学习前端js
  • Product Hunt 每日热榜 | 2025-08-18
  • 【yocto】为什么要选择yocto?
  • 亚马逊新手突围:从流量破冰到持续出单
  • Less (CSS 预处理器)