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. 对开发者的启示:依赖设计的最佳实践​

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

相关推荐
源码宝5 分钟前
【智慧工地源码】智慧工地云平台系统,涵盖安全、质量、环境、人员和设备五大管理模块,实现实时监控、智能预警和数据分析。
java·大数据·spring cloud·数据分析·源码·智慧工地·云平台
David爱编程1 小时前
面试必问!线程生命周期与状态转换详解
java·后端
J_bean1 小时前
Spring AI Alibaba 项目接入兼容 OpenAI API 的大模型
人工智能·spring·大模型·openai·spring ai·ai alibaba
LKAI.2 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
HeyZoeHey2 小时前
Mybatis执行sql流程(一)
java·sql·mybatis
2301_793086872 小时前
SpringCloud 07 微服务网关
java·spring cloud·微服务
该用户已不存在4 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端
TT哇5 小时前
@[TOC](计算机是如何⼯作的) JavaEE==网站开发
java·redis·java-ee
Tina学编程5 小时前
48Days-Day19 | ISBN号,kotori和迷宫,矩阵最长递增路径
java·算法