当前位置: 首页 > 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.dtcms.com/a/338427.html

相关文章:

  • 【Python语法基础学习笔记】常量变量运算符函数
  • LeetCode 每日一题 2025/8/11-2025/8/17
  • 【嵌入式基础梳理#12】风压计Modbus框架示例
  • RAG:让AI成为你的知识专家
  • Maven Assembly Plugin 插件使用说明
  • Linux下使用ssh-agent实现集群节点间无免密安装部署
  • 深度学习——R-CNN及其变体
  • 【轨物交流】轨物科技与华为鲲鹏生态深度合作 光伏清洁机器人解决方案获技术认证!
  • Session共享与Sticky模式:优化Web应用性能
  • [激光原理与应用-296]:理论 - 非线性光学 - 线性光学与非线性光学对比
  • SpringBoot校园商铺运营平台
  • 跨平台RTSP播放器深度对比:开源方案与商业SDK的取舍之道
  • MiniMax Agent 上线 Market Place ,AI一键复制克隆网站
  • 视觉语言导航(5)——VLN的具体工作原理——Seq2Seq CMA模型 KL散度 TRANSFORMER 3.1前半段
  • PMP-项目管理-十大知识领域:资源管理-管理团队、设备、材料等资源
  • Win10下配置WSL2后nvidia-smi不正常显示问题
  • 第一阶段C#基础-15:面向对象梳理
  • python-----机器学习中常用的数据预处理
  • 【前端面试题】JavaScript 核心知识点解析(第二十二题到第六十一题)
  • 【数据分析】R语言在生态学数据分析中的应用:从数据处理到可视化
  • 美图披露半年报:AI应用取得突破,净利润同比大增71.3%
  • C++11列表初始化 {}
  • GitHub Actions 从核心思想到最佳实践
  • 宋红康 JVM 笔记 Day04|双亲委派机制、沙箱安全机制、类的自动使用与被动使用
  • 电子电气架构 --- 软件会给汽车带来哪些变化?
  • 鸿蒙生态7月技术月报 | HarmonyOS 5.1 开发特性详解
  • 蓝池参与雅江水电工程融资,助力国家基础设施建设与经济发展
  • 08.常见文本处理工具
  • 03.文件管理和操作命令
  • 解读60页全面认识大数据基础知识培训【附全文阅读】