📌 PDF :大白话说Java面试题 --- 06_Spring篇
第15题:Spring 如何解决循环依赖问题?
📚 回答:
- 核心考点 : Spring 循环依赖是面试中最经典、最深入的问题之一,大厂面试不会只问"三级缓存是什么",而是深入考察 三级缓存的源码级结构 (
singletonObjects/earlySingletonObjects/singletonFactories的字段定义与类型)、循环依赖的完整解决流程 (从getBean()到createBean()到populateBean()的源码链路)、为什么需要三级缓存而非二级 (AOP 代理对象的提前暴露问题)、构造器循环依赖为什么无法解决 (ConstructorResolver的创建时机)、以及@Lazy延迟注入和ObjectFactory的替代方案。面试官真正想判断的是:你是否能从源码层面理解 Spring 解决循环依赖的设计精妙之处,以及能否在生产中识别和解决循环依赖引发的启动失败、代理失效等问题。
1. 什么是循环依赖?
循环依赖是指两个或多个 Bean 之间相互持有对方的引用,形成闭环:
A → B → A(两两循环)
A → B → C → A(三角循环)
Spring 中的循环依赖类型:
| 循环依赖类型 | 示例 | Spring 是否支持 | 原因 |
|---|---|---|---|
| 构造器循环依赖 | A(B b) → B(A a) |
❌ 不支持 | 构造器注入时 Bean 尚未实例化,无法提前暴露 |
| Setter/字段循环依赖 | @Autowired B b → @Autowired A a |
✅ 支持 | 实例化后可提前暴露半成品,再注入依赖 |
| 多例(prototype)循环依赖 | @Scope("prototype") |
❌ 不支持 | 原型 Bean 不缓存,每次创建都是新实例 |
| @DependsOn 循环依赖 | @DependsOn("b") → @DependsOn("a") |
❌ 不支持 | @DependsOn 强制顺序,循环则报错 |
2. 三级缓存的源码级结构
Spring 的三级缓存在 DefaultSingletonBeanRegistry 中定义:
java
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// 一级缓存:成品单例池(完全初始化好的 Bean)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:提前暴露的对象(半成品,用于解决循环依赖)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:单例工厂(用于创建 Bean 或代理对象)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 正在创建中的 Bean 名称集合(用于检测循环依赖)
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}
| 缓存 | 字段名 | 类型 | 存放内容 | 作用 |
|---|---|---|---|---|
| 一级缓存 | singletonObjects |
ConcurrentHashMap<String, Object> |
完全初始化好的 Bean(成品) | 对外提供 Bean 实例 |
| 二级缓存 | earlySingletonObjects |
ConcurrentHashMap<String, Object> |
已实例化但未初始化的 Bean(半成品) | 解决循环依赖,避免重复创建代理 |
| 三级缓存 | singletonFactories |
HashMap<String, ObjectFactory<?>> |
ObjectFactory(可生成 Bean 或代理) |
延迟创建代理对象,支持 AOP |
3. 三级缓存解决循环依赖的完整流程
以经典的 A → B → A 字段注入循环依赖为例,完整梳理源码链路:
Step 1:获取 A,开始创建
java
// AbstractBeanFactory.doGetBean()
protected <T> T doGetBean(String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly) {
// 1. 从一级缓存获取
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// 2. 标记 A 正在创建
beforeSingletonCreation(beanName); // 加入 singletonsCurrentlyInCreation
// 3. 创建 Bean
singletonObject = createBean(beanName, mbd, args);
}
}
Step 2:实例化 A,放入三级缓存
java
// AbstractAutowireCapableBeanFactory.doCreateBean()
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化 A(调用构造方法,此时 A 中 B 为 null)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 2. 【关键】如果是单例、允许循环依赖、正在创建中,放入三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> {
// 如果有 AOP 代理,提前创建代理;否则返回原始对象
return getEarlyBeanReference(beanName, mbd, bean);
});
}
// 3. 属性填充(populateBean)------ 发现需要注入 B
populateBean(beanName, mbd, instanceWrapper);
// 4. 初始化(initializeBean)
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
Step 3:创建 B,发现需要 A
java
// 在 populateBean 中,发现 A 需要注入 B
// 调用 getBean("b") → doGetBean("b")
// B 的创建流程与 A 相同...
// B 实例化后,populateBean 发现需要注入 A
// 调用 getBean("a") → doGetBean("a")
Step 4:从三级缓存获取 A 的半成品
java
// DefaultSingletonBeanRegistry.getSingleton()
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 一级缓存没有,且正在创建中(说明有循环依赖)
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 3. 从三级缓存获取 ObjectFactory
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 4. 调用 getObject() 获取 Bean(或代理对象)
singletonObject = singletonFactory.getObject();
// 5. 提升到二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
return singletonObject;
}
Step 5:B 完成初始化,A 完成初始化
java
// B 获取到 A 的半成品后,完成 populateBean 和 initializeBean
// B 放入一级缓存
addSingleton("b", b);
// 回到 A 的创建流程,B 已可用,A 完成 populateBean 和 initializeBean
// A 放入一级缓存
addSingleton("a", a);
// addSingleton 方法:
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject); // 放入一级缓存
this.singletonFactories.remove(beanName); // 移除三级缓存
this.earlySingletonObjects.remove(beanName); // 移除二级缓存
}
}
完整流程图:
getBean("a")
↓
doGetBean("a")
↓
getSingleton("a") → 一级缓存无,开始创建
↓
createBean("a") → doCreateBean("a")
↓
实例化 A(构造方法)→ A 对象创建,B=null
↓
addSingletonFactory("a", ObjectFactory) → 放入三级缓存
↓
populateBean("a") → 发现需要注入 B
↓
getBean("b")
↓
doGetBean("b")
↓
getSingleton("b") → 一级缓存无,开始创建
↓
createBean("b") → doCreateBean("b")
↓
实例化 B(构造方法)→ B 对象创建,A=null
↓
addSingletonFactory("b", ObjectFactory) → 放入三级缓存
↓
populateBean("b") → 发现需要注入 A
↓
getBean("a")
↓
getSingleton("a", true)
↓
一级缓存无 → A 正在创建中
↓
earlySingletonObjects 无
↓
singletonFactories 获取 ObjectFactory
↓
getObject() → 返回 A 的半成品(或代理)
↓
放入二级缓存,移除三级缓存
↓
返回 A 的半成品
↓
B 完成 populateBean(A 已注入)
↓
initializeBean("b") → B 完成初始化
↓
addSingleton("b") → 放入一级缓存
↓
返回 B
↓
A 完成 populateBean(B 已注入)
↓
initializeBean("a") → A 完成初始化
↓
addSingleton("a") → 放入一级缓存
↓
返回 A
4. 为什么需要三级缓存?二级缓存不够吗?
这是面试中最核心的问题,需要从 AOP 代理 的角度理解:
-
4.1 如果只有两级缓存的问题
假设只有一级缓存(成品)和二级缓存(半成品):
- A 实例化后,如果需要 AOP 代理,必须立即创建代理对象放入二级缓存;
- 但 AOP 代理的创建时机是在 初始化后 (
postProcessAfterInitialization),此时 A 尚未完成依赖注入和初始化; - 如果在实例化后立即创建代理,后续的
@PostConstruct、InitializingBean等初始化逻辑会在代理对象上执行,可能导致代理逻辑被覆盖或注入失败。
-
4.2 三级缓存的精妙设计
三级缓存存放的是
ObjectFactory(函数式接口),而非直接的 Bean 实例:javaaddSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));延迟创建代理 :只有当循环依赖发生、需要从三级缓存获取 Bean 时,才调用
ObjectFactory.getObject()。此时:- 如果 A 需要 AOP 代理,
getEarlyBeanReference()会提前创建代理对象; - 如果 A 不需要代理,直接返回原始对象;
- 代理对象创建后提升到二级缓存,避免重复创建。
为什么提升到二级缓存?
如果循环依赖中有多个 Bean 依赖 A(如
B → A且C → A),第一次从三级缓存获取 A 时创建代理并放入二级缓存,后续再获取 A 时直接从二级缓存拿,避免重复创建代理。 - 如果 A 需要 AOP 代理,
-
4.3 三级 vs 二级缓存的本质区别
维度 二级缓存方案 三级缓存方案 存储内容 直接存放 Bean 实例 存放 ObjectFactory代理创建时机 实例化后立即创建 按需延迟创建(发生循环依赖时) 初始化顺序 代理对象先创建,初始化逻辑后执行 原始对象先初始化,代理按需创建 灵活性 低,所有 Bean 都提前创建代理 高,只有循环依赖的 Bean 才提前创建代理 性能 差,所有 Bean 都提前创建代理 好,按需创建代理
5. 构造器循环依赖为什么无法解决?
构造器注入的循环依赖无法通过三级缓存解决,根本原因在于创建时机的差异:
java
@Service
public class A {
private final B b;
public A(B b) { this.b = b; } // 构造器注入
}
@Service
public class B {
private final A a;
public B(A a) { this.a = a; } // 构造器注入
}
问题分析:
| 阶段 | Setter/字段注入 | 构造器注入 |
|---|---|---|
| 实例化 | 无参构造创建对象 | 有参构造创建对象,参数必须已存在 |
| 提前暴露 | 实例化后即可暴露 | 构造器执行时就需要 B,B 尚未创建 |
| 循环依赖解决 | ✅ 可以 | ❌ 无法提前暴露 |
具体流程:
创建 A → 调用构造方法 A(B b) → 需要 B
↓
创建 B → 调用构造方法 B(A a) → 需要 A
↓
A 尚未实例化完成,无法从三级缓存获取 → 死循环
解决方案:
| 方案 | 实现方式 | 说明 |
|---|---|---|
| 改用 Setter/字段注入 | 将 final 和构造器去掉,改用 @Autowired |
最简单,但牺牲不可变性 |
| @Lazy 延迟注入 | @Lazy @Autowired private B b; |
注入代理对象,延迟真正创建 |
| ObjectFactory 延迟获取 | @Autowired private ObjectFactory<B> bFactory; |
使用时才获取 |
| @Lookup 方法 | @Lookup protected abstract B getB(); |
Spring 动态生成子类实现 |
| 重构代码 | 消除循环依赖 | 最佳方案,但成本最高 |
@Lazy 解决构造器循环依赖的原理:
java
@Service
public class A {
private final B b;
public A(@Lazy B b) { // 注入的是 B 的代理对象
this.b = b;
}
}
Spring 为 B 创建 CGLIB 代理对象,A 的构造器接收的是代理而非真实 B。代理对象可以立即创建,不需要 B 的真实实例。当 A 初始化完成后,B 再创建并注入到 A 中。
6. 多例(prototype)循环依赖
原型 Bean 不缓存,每次 getBean() 都创建新实例,因此无法通过三级缓存解决循环依赖:
java
@Scope("prototype")
@Component
public class A {
@Autowired
private B b;
}
@Scope("prototype")
@Component
public class B {
@Autowired
private A a;
}
报错 :BeanCurrentlyInCreationException
解决方案 :使用 ObjectFactory 或 @Lookup 延迟获取。
7. 生产环境避坑指南
-
7.1 启动时报
BeanCurrentlyInCreationException检查是否是构造器循环依赖或多例循环依赖。查看异常堆栈中的 Bean 名称,定位循环链。
-
7.2 @Async 与循环依赖
@Async代理的创建可能导致循环依赖检测失败。确保@EnableAsync在@EnableAspectJAutoProxy之后。 -
7.3 AOP 代理与循环依赖的初始化顺序
循环依赖中提前创建的代理对象,其
TargetSource指向的是半成品 Bean。初始化完成后,代理对象的目标引用需要更新为完整的 Bean。 -
7.4 使用
@DependsOn导致循环@DependsOn("b")强制在创建 A 之前先创建 B,如果 B 也@DependsOn("a"),会直接报错,无法通过三级缓存解决。 -
7.5 循环依赖是设计问题
虽然 Spring 能解决 Setter 循环依赖,但循环依赖通常意味着职责划分不清。最佳实践是通过重构消除循环依赖。
8. 面试官追问与高分回答模板
-
追问 1:"Spring 是如何解决循环依赖的?"
低分回答:"通过三级缓存,在对象尚未完全初始化时提前暴露引用。"(没有触及源码和 AOP 代理)
高分回答:
"Spring 通过三级缓存解决单例 Bean 的 Setter/字段注入循环依赖:
缓存 字段 内容 作用 一级缓存 singletonObjects成品 Bean 对外提供 二级缓存 earlySingletonObjects半成品 Bean 解决循环依赖,避免重复创建代理 三级缓存 singletonFactoriesObjectFactory延迟创建代理对象 完整流程:
- A 实例化后(构造方法执行完,尚未注入依赖),将
ObjectFactory放入三级缓存; - A 属性填充时发现需要 B,开始创建 B;
- B 实例化后放入三级缓存,属性填充时发现需要 A;
- 从三级缓存获取 A 的
ObjectFactory,调用getObject()获取 A 的半成品(或代理对象),提升到二级缓存; - B 完成初始化,放入一级缓存;
- A 继续初始化,放入一级缓存。
关键是三级缓存存放
ObjectFactory而非直接实例,实现了代理对象的延迟创建------只有发生循环依赖时才提前创建代理,避免所有 Bean 都提前代理的性能损耗。" - A 实例化后(构造方法执行完,尚未注入依赖),将
-
追问 2:"为什么需要三级缓存?二级缓存不够吗?"
高分回答:
"二级缓存不够,核心原因是 AOP 代理的创建时机问题。
如果只有两级缓存(成品 + 半成品),A 实例化后必须立即创建代理对象放入二级缓存。但 AOP 代理的正常创建时机是在初始化后 (
postProcessAfterInitialization),此时@PostConstruct、InitializingBean等初始化逻辑尚未执行。如果提前创建代理:- 初始化逻辑会在代理对象上执行,可能覆盖代理逻辑;
- 所有 Bean 都提前创建代理,性能损耗大。
三级缓存存放
ObjectFactory,实现了按需延迟创建 :只有当发生循环依赖、需要从缓存获取 Bean 时,才调用getObject()创建代理。这样既保证了循环依赖时能获取到代理对象,又避免了不必要的提前代理。二级缓存(
earlySingletonObjects)的作用是避免重复创建代理:如果多个 Bean 循环依赖 A,第一次从三级缓存创建代理后放入二级缓存,后续直接从二级缓存获取。" -
追问 3:"构造器循环依赖为什么无法解决?"
高分回答:
"构造器循环依赖无法解决的根本原因是实例化和依赖注入的时机重叠。
Setter/字段注入的流程是:先无参构造实例化 → 提前暴露到三级缓存 → 再注入依赖。此时 Bean 已经存在,可以被其他 Bean 引用。
构造器注入的流程是:调用构造方法时就需要传入依赖对象。创建 A 的构造方法
A(B b)时,B 尚未创建;创建 B 的构造方法B(A a)时,A 尚未实例化完成。两者互相等待,形成死锁。解决方案:
- 改用 Setter/字段注入(最简单,但牺牲不可变性);
@Lazy延迟注入 :public A(@Lazy B b),注入 B 的代理对象,延迟真正创建;ObjectFactory延迟获取 :使用时才getObject();- 重构代码,消除循环依赖(最佳方案)。"
-
追问 4:"@Lazy 是怎么解决循环依赖的?"
高分回答:
"
@Lazy解决循环依赖的原理是延迟注入代理对象。以构造器循环依赖
A(B)→B(A)为例:javapublic A(@Lazy B b) { this.b = b; }- Spring 创建 A 时,发现构造器参数 B 标注了
@Lazy; - Spring 不为 B 创建真实实例,而是创建 B 的 CGLIB 代理对象;
- A 的构造器接收代理对象,A 实例化完成并提前暴露到三级缓存;
- 创建 B 时,从三级缓存获取 A 的半成品注入;
- B 初始化完成;
- A 初始化完成,代理对象中的目标引用更新为完整的 B。
关键点:代理对象可以立即创建,不需要依赖的真实实例。
@Lazy本质上是用"空间换时间",用代理对象打破循环等待。" - Spring 创建 A 时,发现构造器参数 B 标注了
-
追问 5:"Spring Boot 中怎么检测和避免循环依赖?"
高分回答:
"Spring Boot 2.6+ 默认禁止循环依赖 (
spring.main.allow-circular-references=false),启动时检测到循环依赖会直接报错。检测方法:
- 查看启动异常
BeanCurrentlyInCreationException,堆栈会显示循环链; - 使用 IDE 插件(如 IntelliJ 的 Spring 依赖分析)可视化 Bean 依赖图;
- 代码审查:检查
@Autowired和构造器注入的依赖关系。
避免策略:
- 分层架构:Controller → Service → DAO,同层不依赖,避免循环;
- 事件驱动 :使用
ApplicationEventPublisher解耦依赖; - 抽象公共逻辑:将双向依赖的公共部分抽取到第三个 Bean;
- 接口隔离:通过接口降低耦合,但注意接口之间也可能循环。"
- 查看启动异常
-
追问 6:"如果 A 需要 AOP 代理,循环依赖中提前暴露的是代理对象还是原始对象?"
高分回答:
"循环依赖中提前暴露的是代理对象(如果 A 需要 AOP 增强)。
具体流程:
- A 实例化后,三级缓存中放入
ObjectFactory; - B 创建时需要 A,从三级缓存调用
ObjectFactory.getObject(); getObject()内部调用getEarlyBeanReference(beanName, mbd, bean);getEarlyBeanReference()遍历SmartInstantiationAwareBeanPostProcessor,AbstractAutoProxyCreator判断 A 需要代理,提前创建代理对象;- 代理对象返回给 B,B 注入的是 A 的代理;
- A 初始化完成后,代理对象的
TargetSource指向完整的 A。
这意味着:B 中注入的 A 从一开始就是代理对象,而非先注入原始对象再替换。这是三级缓存设计的关键------确保循环依赖中注入的是一致的代理对象。"
- A 实例化后,三级缓存中放入
9. 方案选型速查表
| 循环依赖场景 | 是否支持 | 解决方案 | 推荐度 |
|---|---|---|---|
| 单例 + Setter/字段注入 | ✅ | 三级缓存自动解决 | ⭐⭐⭐⭐⭐ |
| 单例 + 构造器注入 | ❌ | 改用 Setter、@Lazy、ObjectFactory、重构 | ⭐⭐⭐⭐ |
| 单例 + @DependsOn | ❌ | 消除 @DependsOn 循环 | ⭐⭐⭐⭐⭐ |
| 原型(prototype) | ❌ | ObjectFactory、@Lookup、重构 | ⭐⭐⭐ |
| 多例 + 构造器 | ❌ | 重构消除循环 | ⭐⭐⭐⭐⭐ |
| AOP 代理 + 循环依赖 | ✅ | 三级缓存自动处理代理提前暴露 | ⭐⭐⭐⭐⭐ |
💡 面试官想要的满分总结:
Spring 解决循环依赖的核心设计是三级缓存 + 提前暴露 ,但这只适用于单例 + Setter/字段注入的场景。理解三级缓存必须抓住三个关键点:
- 三级缓存存放
ObjectFactory而非实例 :实现了 AOP 代理的延迟创建 。只有发生循环依赖时才提前创建代理,避免所有 Bean 都提前代理的性能损耗。二级缓存(earlySingletonObjects)则避免重复创建代理。- 构造器循环依赖无法解决 :因为构造器执行时 Bean 尚未实例化,无法提前暴露。解决方案包括
@Lazy(注入代理对象)、ObjectFactory(延迟获取)、或重构消除循环。- 循环依赖是设计问题:虽然 Spring 能解决 Setter 循环依赖,但通常意味着职责划分不清。Spring Boot 2.6+ 默认禁止循环依赖,倒逼更好的设计。
生产中最容易踩的坑是构造器注入的循环依赖启动失败 和忘记
@Lazy导致的问题。记住:三级缓存是 Spring 的"兜底"机制,不是鼓励循环依赖的"许可证"。优秀的架构应该通过分层设计和事件驱动消除循环依赖,而非依赖框架的容错能力。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯