一、 循环依赖的困境
设想两个Bean:BeanA
和 BeanB
。
-
BeanA
在其属性中需要注入BeanB
的实例。 -
BeanB
在其属性中也需要注入BeanA
的实例。
Spring容器在启动时,需要创建这些单例Bean。如果按照严格的顺序创建:
-
开始创建
BeanA
-> 发现需要BeanB
-> 暂停BeanA
的创建,转而去创建BeanB
。 -
开始创建
BeanB
-> 发现需要BeanA
-> 但此时BeanA
尚未创建完成(卡在等待BeanB
的阶段) -> 陷入死锁状态。
这就是经典的循环依赖(Circular Dependency)问题。Spring的三级缓存机制就是为了打破这种僵局。
二、 三级缓存:Spring的解决之道
Spring通过三个不同的缓存区域(通常命名为 singletonObjects
, earlySingletonObjects
, singletonFactories
)协作解决循环依赖。它们的作用和生命周期如下:
-
一级缓存 (成品池 -
singletonObjects
):-
作用: 存放已经完全初始化完成的单例Bean对象。这是最终的、可以直接使用的Bean。
-
生命周期: Bean完成所有步骤(实例化、属性填充、初始化)后放入此缓存。后续所有获取该Bean的请求都直接从这里返回。
-
-
二级缓存 (半成品池 -
earlySingletonObjects
):-
作用: 存放已被实例化但尚未完成属性填充和初始化的Bean的早期引用(Early Reference)。这是打破循环依赖的关键。
-
生命周期: 当一个Bean被实例化(调用构造器
new
出来)后,Spring会立即将一个指向这个“半成品”对象的引用放入二级缓存(如果当前存在循环依赖的可能性)。当其他Bean在创建过程中需要依赖这个Bean时,可以从这里获取到它的早期引用进行注入。一旦该Bean最终完成初始化,它的引用会从二级缓存移入一级缓存。
-
-
三级缓存 (对象工厂池 -
singletonFactories
):-
作用: 存放用于创建Bean的对象工厂(
ObjectFactory<?>
)。这个工厂的核心方法是getObject()
,它有能力返回Bean的早期引用,并且在必要时提前生成代理对象。 -
生命周期: 在Bean被实例化后,Spring会立即将一个能生产该Bean(可能是原始对象,也可能是代理对象)的
ObjectFactory
放入三级缓存。当需要从二级缓存获取早期引用时,如果二级缓存没有,则会调用三级缓存中对应工厂的getObject()
方法来获取(并可能同时将结果升级放入二级缓存)。当Bean最终放入一级缓存后,其对应的工厂会从三级缓存移除。
-
三、 核心流程解析:三级缓存如何协作
让我们结合 BeanA
和 BeanB
的循环依赖场景,一步步拆解三级缓存的协作过程:
-
开始创建
BeanA
:-
Spring 调用
BeanA
的构造器进行实例化,得到一个原始beanA
对象(此时属性为null
)。 -
关键动作1: 创建一个能生产
beanA
的ObjectFactory
,并将其放入三级缓存(singletonFactories
)。 -
关键动作2: (注意:此时通常 不会 直接将对象引用放入二级缓存!这是与早期简化理解的重要区别)。
-
-
填充
BeanA
属性(发现依赖BeanB
):-
Spring 准备为
beanA
注入属性。 -
发现
beanA
依赖BeanB
。 -
于是 Spring 暂停
BeanA
的装配,转而去创建BeanB
。
-
-
开始创建
BeanB
:-
调用
BeanB
的构造器进行实例化,得到一个原始beanB
对象。 -
关键动作3: 创建一个能生产
beanB
的ObjectFactory
,放入三级缓存(singletonFactories
)。
-
-
填充
BeanB
属性(发现依赖BeanA
):-
Spring 准备为
beanB
注入属性。 -
发现
beanB
依赖BeanA
。 -
Spring 开始查找
BeanA
的实例:-
查一级缓存(
singletonObjects
):没有(BeanA
还没创建完)。 -
查二级缓存(
earlySingletonObjects
):没有(此时还未放入)。 -
查三级缓存(
singletonFactories
):找到BeanA
的ObjectFactory
!
-
-
关键动作4: 调用
BeanA
的ObjectFactory.getObject()
方法。-
如果
BeanA
不需要 AOP 代理:这个方法直接返回步骤1中创建的原始beanA
引用。 -
如果
BeanA
需要 AOP 代理:Spring 会在此时(提前)生成beanA
的代理对象proxyA
。(这是三级缓存的核心价值所在!) -
无论返回的是原始对象
beanA
还是代理对象proxyA
,Spring 都会将这个对象引用放入二级缓存(earlySingletonObjects
),并从三级缓存移除该ObjectFactory
。
-
-
Spring 将获取到的
BeanA
的引用(可能是beanA
或proxyA
)注入到beanB
的属性中。 -
BeanB
继续完成后续的属性注入(如果还有其他依赖)和初始化方法。 -
关键动作5:
BeanB
完全创建完成,将其最终对象放入一级缓存(singletonObjects
),并从二级缓存和三级缓存中移除其相关引用。
-
-
回到
BeanA
的创建:-
此时
BeanB
已创建完成并在一级缓存中。 -
Spring 从一级缓存(
singletonObjects
) 中获取到BeanB
的完成品对象。 -
将
BeanB
注入到beanA
的属性中。 -
BeanA
继续完成后续的属性注入(如果还有其他依赖)和初始化方法。 -
关键动作6:
BeanA
完全创建完成。Spring 需要将其放入一级缓存:-
检查二级缓存(
earlySingletonObjects
):发现里面存放着之前通过ObjectFactory.getObject()
得到的引用(可能是原始beanA
或proxyA
)。 -
一致性保证: 最终放入一级缓存的
BeanA
必须与 之前注入给BeanB
的那个引用是同一个对象。 -
如果二级缓存中的是原始对象
beanA
,且BeanA
最终需要代理,Spring 会在此刻生成代理对象proxyA
,放入一级缓存。注意: 这会导致注入给BeanB
的是原始对象beanA
,而最终容器管理的是proxyA
,两者不一致!这就是为什么需要三级缓存在步骤4提前处理代理。 -
由于三级缓存在步骤4已经处理了代理(如果需要),此时二级缓存中的引用
proxyA
就是最终需要的代理对象。Spring 直接将其从二级缓存移入一级缓存。
-
-
关键动作7: 将最终对象(
proxyA
或原始beanA
)放入一级缓存(singletonObjects
),并从二级缓存和三级缓存中移除其相关引用。
-
四、 为什么是三级?二级缓存不行吗?
理解三级缓存价值的关键在于处理需要AOP代理的Bean的循环依赖:
-
二级缓存的缺陷(假设只有一级和二级缓存):
-
创建
BeanA
实例 -> 将原始beanA
放入二级缓存。 -
创建
BeanB
发现需要BeanA
-> 从二级缓存获取到原始beanA
-> 注入给beanB
。 -
BeanB
完成 -> 放入一级缓存。 -
继续完成
BeanA
-> 此时发现BeanA
需要AOP代理 -> 生成代理对象proxyA
-> 将proxyA
放入一级缓存。
-
问题:
BeanB
中持有的引用是原始beanA
,而Spring容器管理的最终对象是代理对象proxyA
。BeanB
通过beanA
调用的方法不会被AOP切面增强!这破坏了AOP的功能和容器内对象的一致性。
-
-
三级缓存如何解决(核心在于
ObjectFactory.getObject()
):-
在步骤4(
BeanB
需要注入BeanA
)时,不是直接从二级缓存拿对象,而是通过三级缓存的ObjectFactory.getObject()
获取。 -
这个工厂方法在被调用时(即第一次被其他Bean索取依赖时)会进行判断:
-
如果该Bean不需要代理,返回原始对象引用(并放入二级缓存)。
-
如果该Bean需要代理,立即生成代理对象并返回(同时放入二级缓存,替换掉等待生成代理的概念)。
-
-
这样,无论
BeanA
最终是否需要代理:-
注入给
BeanB
的引用(通过工厂获取的)和最终放入一级缓存的引用(代理对象或原始对象)始终是同一个对象。 -
确保了AOP功能在存在循环依赖时也能正确生效。
-
-
五、 重要注意事项
-
只适用于单例(Singleton) Bean: 三级缓存机制是为解决单例Bean的循环依赖设计的。原型(Prototype) Bean的循环依赖Spring无法解决,会直接抛出
BeanCurrentlyInCreationException
。 -
只适用于Setter注入或字段注入: 该机制依赖于在属性注入阶段能够拿到早期引用。如果循环依赖是通过构造器注入(Constructor Injection) 发生的,在实例化
BeanA
时就需要BeanB
的实例(此时BeanB
甚至还没开始创建,更谈不上放入三级缓存),Spring将无法处理,同样会抛出异常。因此,推荐使用Setter或字段注入来避免构造器循环依赖问题。 -
并非所有场景都完美: 虽然三级缓存解决了大部分常见的循环依赖,但过于复杂的依赖关系或某些特定的Bean生命周期回调组合仍可能带来挑战。良好的设计应尽量避免循环依赖。
六、 总结
Spring的三级缓存机制是其IoC容器设计中一个精妙且核心的部分。它通过:
-
提前暴露引用(二级缓存) 打破循环依赖的僵局。
-
使用对象工厂(三级缓存) 延迟决策并确保在需要时提前、正确地生成代理对象。
-
最终一致性(一级缓存) 保证容器内管理的是完全初始化的Bean。
三者协同工作,高效且优雅地解决了单例Bean通过Setter/Field注入方式产生的循环依赖问题,即使涉及复杂的AOP代理也能保持对象引用的一致性和功能的正确性。理解这一机制,有助于我们更深入地掌握Spring容器的运作原理,编写更健壮的应用程序,并在遇到相关问题时能够快速定位和解决。
思考题:
-
如果
BeanA
和BeanB
都采用构造器注入依赖对方,Spring还能解决吗?为什么? -
在三级缓存的工作流程中,哪个步骤是保证AOP代理对象一致性的最核心环节?
欢迎在评论区交流讨论!
📌 关注我,每天带你掌握底层原理,写出更强健的 Java 代码!