循环依赖是Spring面试中的经典问题,也是实际开发中容易踩的坑。比如,Bean A依赖Bean B,而Bean B又依赖Bean A,Spring是如何解决这种“鸡生蛋还是蛋生鸡”的问题的?有人说必须用三级缓存,但真的没有其他办法吗?本文用大白话+代码示例,帮你彻底搞懂!
目录
一、什么是循环依赖?
假设有两个类互相依赖,Spring在创建Bean时会陷入死循环:
@Component
public class A {
@Autowired
private B b; // A依赖B
}
@Component
public class B {
@Autowired
private A a; // B依赖A
}
启动时会报错:BeanCurrentlyInCreationException。但实际开发中,Spring却成功解决了这个问题!它的秘密武器就是三级缓存。
二、Spring的三级缓存是什么?
Spring内部通过三个Map(俗称三级缓存)管理Bean的创建过程:
| 缓存名称 | 作用 |
|---|---|
| singletonObjects | 存放完全初始化好的单例Bean(成品) |
| earlySingletonObjects | 存放提前暴露的Bean(半成品,未填充属性) |
| singletonFactories | 存放Bean的工厂对象,用于生成半成品Bean |
核心思想:在Bean未完全初始化前,提前暴露它的引用,打破循环依赖。
三、三级缓存如何解决循环依赖?
以A和B的循环依赖为例,Spring创建Bean的流程如下:
-
创建A实例
- 实例化A(调用构造方法,得到一个“原始对象”)。
- 将A的工厂对象(用于生成半成品A)放入三级缓存
singletonFactories。
-
填充A的属性
- 发现A依赖B,于是开始创建B。
-
创建B实例
- 实例化B(得到原始对象)。
- 将B的工厂对象放入三级缓存。
-
填充B的属性
- 发现B依赖A,于是从三级缓存中找到A的工厂,生成半成品A,并放入二级缓存
earlySingletonObjects。 - 将半成品A注入到B中,完成B的初始化。
- 将初始化好的B放入一级缓存
singletonObjects。
- 发现B依赖A,于是从三级缓存中找到A的工厂,生成半成品A,并放入二级缓存
-
完成A的初始化
- 将初始化好的B注入到A中,完成A的初始化。
- 将A从二级缓存移到一级缓存。
整个过程通过提前暴露半成品对象,避免了死锁。
四、为什么必须用三级缓存?
关键问题:如果只有两级缓存(一级成品+二级半成品),能不能解决问题?
答案是:能,但前提是没有AOP代理!
如果Bean需要被代理(比如用了@Transactional或@Async),Spring会在初始化阶段生成代理对象。此时,如果只有两级缓存:
- 在填充属性时,注入的是原始对象,而不是代理对象。
- 最终放入一级缓存的代理对象和已注入的原始对象不一致,导致严重问题!
三级缓存的作用:
通过singletonFactories存储的工厂对象,可以动态决定返回原始对象还是代理对象。例如,当A被AOP增强时,工厂会生成代理对象并返回,确保全局唯一性。
五、不用三级缓存的替代方案?
假设没有AOP,可以简化流程,仅用两级缓存。但实际开发中,Spring的AOP功能被广泛使用,因此三级缓存是必要的。
结论:
- 如果项目完全不用AOP,理论上可以去掉三级缓存。
- 但Spring设计时考虑了通用性,因此必须保留三级缓存。
六、哪些循环依赖无法解决?
Spring只能解决单例Bean通过Setter/字段注入的循环依赖。以下情况会失败:
-
构造器注入的循环依赖
@Component public class A { private B b; public A(B b) { this.b = b; } // 构造器注入 } @Component public class B { private A a; public B(A a) { this.a = a; } // 构造器注入 }原因:构造器注入需要先完成Bean的实例化,无法提前暴露半成品。
-
多例Bean(@Scope("prototype"))
Spring不缓存多例Bean,因此无法解决循环依赖。
七、实际开发中的建议
-
尽量避免循环依赖:代码结构不合理时容易引发循环依赖,建议通过重构解决。
-
优先使用Setter/字段注入:构造器注入虽然安全,但无法解决循环依赖。
-
利用@Lazy延迟加载:对某个Bean添加
@Lazy,让Spring延迟注入,打破循环。@Component public class A { @Lazy // 延迟注入B @Autowired private B b; }
总结
Spring通过三级缓存+提前暴露半成品对象解决循环依赖问题,核心目的是处理AOP代理对象的唯一性。虽然理论上两级缓存可以解决部分场景,但三级缓存是Spring设计上的必要选择。理解这一机制,不仅能应对面试,还能在遇到相关Bug时快速定位原因!