可锐资源网

技术资源分享平台,提供编程学习、网站建设、脚本开发教程

JAVA面试|Spring是如何解决循环依赖?

Spring解决字段注入/Setter注入方式的循环依赖(两个或多个Bean相互依赖)的核心机制是 「三级缓存」+「提前暴露半成品Bean」。这个设计非常巧妙,我们用一个通俗的故事来解释:


一、场景模拟:两个邻居互相依赖

假设有两个邻居:

Bean A:依赖于Bean B(比如A需要B帮忙修电脑)

Bean B:依赖于Bean A (比如B需要A帮忙照看宠物)

他们同时搬家进来(被Spring创建),形成了循环依赖。


二、Spring的解决步骤(三级缓存登场)

Spring的容器里有三个特殊的“缓存区域”来管理Bean的创建过程:

1. 一级缓存singletonObjects (成品库):

存放完全初始化好的、可以直接使用的Bean(最终目标)。

相当于装修好、家具齐全、可以拎包入住的房子。

2. 二级缓存 earlySingletonObjects (临时样品库):

存放提前暴露出来的、尚未完全初始化的Bean引用(主要是为了解决循环依赖)。


相当于房子主体结构建好了(毛坯房),但内部装修(依赖注入、初始化方法)还没做。这个毛坯房地址先告诉需要它的人。

3. 三级缓存singletonFactories (工厂车间):

存放创建Bean的工厂对象 (ObjectFactory)。

这个工厂的作用:当有人需要引用这个正在创建的Bean时,它可以生产出一个该Bean的早期引用(代理对象或原始对象)并放到二级缓存。

相当于一个施工队,接到指令就能根据图纸造出一个毛坯房框架。


三、创建Bean A的详细过程(结合循环依赖)

1. 开始创建A:

Spring知道需要创建Bean A。

首先,调用A的构造器new A()。此时A的对象在内存中创建出来了(有了内存地址),但它的属性b(指向B)还是null,@PostConstruct等方法也没执行。这是一个“半成品”A。

关键一步:Spring立即将这个半成品A包装成一个 ObjectFactory (工厂对象),并放入三级缓存 (singletonFactories) 中。相当于登记:“A的毛坯房框架已搭好,施工队在此待命”。

2. 给A注入依赖(发现需要B):

Spring 开始给A的属性赋值,发现A依赖于Bean B。

Spring去成品库(一级缓存)找B,发现没有(B还没创建)。


Spring 去临时样品库(二级缓存)找B的半成品,也没有。

Spring去工厂车间(三级缓存)找B的工厂,也没有(因为B还没开始创建)。

3. 转而去创建B:

既然B不存在,Spring暂停A的初始化过程,先去创建Bean B。

调用B的构造器new B()。此时B的对象在内存中创建出来了(半成品),属性a(指向A)是null。

关键一步:Spring立即将这个半成品B包装成一个 ObjectFactory,放入三级缓存。

4. 给B注入依赖(发现需要A):

Spring 开始给B的属性赋值,发现 B 依赖于 Bean A。

Spring 去成品库(一级缓存)找 A,没有(A还在创建中)。

Spring 去临时样品库(二级缓存)找 A 的半成品,没有(A的半成品还在三级缓存)。

关键一步:Spring 去工厂车间(三级缓存)找A的工厂 (ObjectFactory)。找到了!

Spring 调用这个工厂(ObjectFactory.getObject())。这个工厂会做一件事:

如果A不需要AOP代理:直接返回第1步创建的那个原始半成品A对象。

如果A需要AOP代理:这里会变得复杂一些。工厂可能会提前生成一个代理对象(这是使用三级缓存而不是二级缓存直接存对象的重要原因之一,为了处理代理)。简单理解,工厂能提供一个指向那个半成品A(或其代理)的引用。

Spring 把从这个工厂得到的A的早期引用(可能是原始对象,也可能是代理对象)放入二级缓存 (earlySingletonObjects),同时从三级缓存中移除A的工厂。

Spring将这个A的早期引用注入给B的属性a。此时,B的属性a被成功赋值了(指向了A的半成品)!B的依赖注入完成。

5. 完成B的初始化:

给B的其他属性赋值(如果有)。

执行B的初始化方法(如@PostConstruct)。

此时,Bean B完成了初始化,成为一个“成品”!

Spring将成品B放入一级缓存(singletonObjects),并从二级和三级缓存中清除B的相关记录。

6. 回到A的依赖注入:

现在创建A的过程(在第2步暂停了)可以继续了。

Spring需要给A的属性b注入依赖。它现在去成品库(一级缓存)找B。

找到了成品B!Spring将成品B注入给A的属性b。

7. 完成A的初始化:

给A的其他属性赋值。

执行A的初始化方法(如@PostConstruct)。注意:此时A里的b指向的是完全初始化好的B。


此时,Bean A也完成了初始化,成为一个“成品”!

Spring将成品A放入一级缓存 (singletonObjects),并从二级缓存中移除A的早期引用记录(如果还在的话)。


四、结果

成品库(一级缓存)中有了完全初始化好的Bean A和Bean B。

A内部的b属性指向了完全初始化好的B。

B内部的a属性指向了完全初始化好的A。

循环依赖成功解决!两个Bean都能正常工作。


五、关键点总结(通俗版)

1. 先造框架再装修

Spring在调用构造器new出对象后(搭好毛坯房框架),立刻把这个“半成品”的引用登记到“工厂车间”(三级缓存),告诉别人“这个Bean的框架我搭好了,需要的话可以来找我拿个临时引用”。

2. 提前暴露地址

当另一个Bean在初始化过程中需要依赖这个半成品时,Spring 就去“工厂车间”找。工厂车间负责提供一个指向那个半成品的引用(可能是原始对象,也可能是处理了AOP的代理)。拿到这个引用后,Spring 把它存到“临时样品库”(二级缓存)备用,并把工厂从车间撤走(三级缓存移除)。

3. 按顺序完工

依赖方(如B)拿到被依赖方(如A)的半成品引用后,就能完成自己的初始化,变成“成品”入住“成品库”(一级缓存)。然后最早创建的那个Bean(A)就能从“成品库”拿到它依赖的成品(B),最终自己也变成成品。

4. 三级缓存的分工

一级缓存 (成品库):最终可用的Bean

二级缓存 (临时样品库):临时存放提前暴露出来的Bean引用(解决循环依赖时用),避免每次都去工厂创建。

三级缓存 (工厂车间):存放能生产Bean早期引用的工厂。核心在于这个工厂能处理AOP代理的生成!如果只是原始对象,二级缓存理论上够用,但为了统一处理代理,引入了三级缓存。


六、重要限制

1. 构造器注入无法解决

如果循环依赖是通过构造器参数发生的(A(B b) 和 B(A a)),Spring无法解决,会直接抛出
BeanCurrentlyInCreationException。

原因:构造对象时就必须提供依赖项。在new A(b) 时,b必须存在;而要new B(a),a又必须存在。这就成了死锁,Spring在调用构造器之前没有机会提前暴露半成品引用。字段注入/Setter注入是在对象构造之后才进行的,所以有机会暴露半成品。

2. Prototype作用域的Bean无法解决

Spring不缓存prototype Bean。每次请求都创建一个新实例。因此,无法利用三级缓存机制提前暴露一个半成品的prototype Bean 来解决循环依赖。

3. 某些AOP代理场景需注意

虽然三级缓存的设计考虑了AOP代理,但在极端复杂的代理逻辑下,仍需注意配置。


七、简单记忆

Spring通过“先创建空壳对象 -> 提前暴露引用 -> 互相注入引用 -> 最后完成初始化”的策略,利用三级缓存作为中转站,巧妙地打破了字段/Setter注入的循环依赖僵局。构造器注入的循环依赖是无解的。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言