[面试题] Spring的循环依赖问题是什么?如何解决循环依赖问题?
📖 版块 A:知识讲解版 (温故知新)
-
什么是循环依赖:指两个或多个 Bean 之间互相持有对方的引用,形成了一个闭环。例如:Bean A 依赖 Bean B,而 Bean B 又依赖 Bean A。
-
Spring 解决的范围:
- 只支持 单例(Singleton) 作用域的 Bean。原型(Prototype)作用域的循环依赖无法解决,会抛出异常。
- 只支持 Setter 注入 或 Field 属性注入。构造器注入产生的循环依赖无法自动解决。
-
核心机制:三级缓存:
- 一级缓存 (singletonObjects):存放已经完全初始化好的单例 Bean。
- 二级缓存 (earlySingletonObjects):存放早期的 Bean(半成品,已实例化但未填充属性),用于解决循环依赖时的提前引用。
- 三级缓存 (singletonFactories):存放 ObjectFactory(工厂对象),用于生成 Bean 的代理对象。
-
解决逻辑:通过“提前暴露”未完全初始化的对象引用,利用三级缓存来打破死锁循环。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:Spring 的循环依赖问题是什么?如何解决循环依赖问题?
💬 高手逐字稿:
关于 Spring 的循环依赖,简单来说就是 A 依赖 B,B 又反过来依赖 A。在 Spring 容器初始化时,如果没有处理机制,就会陷入死循环导致程序崩溃。
Spring 解决这个问题的核心思想是“提前暴露”,并为此设计了三级缓存机制:
第一级缓存是 singletonObjects,存的是完全体,也就是已经走完整个生命周期的 Bean。第二级缓存是 earlySingletonObjects,存的是半成品,即实例化了但还没属性填充的 Bean,用来保证在循环依赖时,大家拿到的都是同一个引用的副本。第三级缓存是 singletonFactories,它最关键,存的是一个工厂对象 ObjectFactory。
我详细梳理一下流程:当 Spring 创建 A 时,先通过构造函数实例化 A,然后把 A 包装成一个工厂放入三级缓存中。接着 A 发现自己依赖 B,就开始创建 B。B 实例化后也放入三级缓存,并发现自己依赖 A。这时候,B 会去一级、二级缓存找 A,没找到,最后在三级缓存里找到了 A 的工厂。B 通过工厂拿到 A 的引用(如果 A 有 AOP 增强,此时拿到的就是代理对象),并将 A 存入二级缓存。B 完成初始化后,把自己放入一级缓存。最后 A 拿到 B,也完成初始化。
这里有一个很经典的细节:为什么一定要用三级缓存,二级行不行? 如果我们的 Bean 只是简单的 POJO,确实二级缓存就够了。但 Spring 必须要考虑 AOP 代理。根据 Spring 的生命周期,代理对象的创建通常是在初始化之后的 AnnotationAwareAspectJAutoProxyCreator 中完成的。如果没有三级缓存的工厂,我们就必须在实例化后立即为所有 Bean 创建代理,这违背了 Spring 设计的初衷。通过三级缓存,只有在真正发生循环依赖时,才会通过工厂提前创建代理对象;如果没有循环依赖,代理依然在初始化后按原计划创建。
最后需要注意,Spring 并不是万能的。构造器注入的循环依赖它解决不了,因为对象还没实例化就卡住了。另外,原型模式(Prototype)的循环依赖也解决不了,因为 Spring 不会缓存原型 Bean。
在实际开发中,虽然 Spring 帮我们兜底了,但从架构设计角度看,出现循环依赖往往意味着代码耦合度过高。所以即便 Spring 能解决,我也建议尽量通过重构代码逻辑,比如引入中转层或使用 @Lazy 注解来规避。