文章目录
- [一、引言:Spring 为什么要解决循环依赖?](#一、引言:Spring 为什么要解决循环依赖?)
- [二、Spring 能解决/不能解决哪些循环依赖?](#二、Spring 能解决/不能解决哪些循环依赖?)
-
- [1、Spring 能自动解决的](#1、Spring 能自动解决的)
- [2、Spring 无法自动解决的](#2、Spring 无法自动解决的)
-
- (1)构造器注入循环依赖
- [(2)prototype 作用域循环依赖](#(2)prototype 作用域循环依赖)
- [三、Bean 创建的大致流程](#三、Bean 创建的大致流程)
-
- [1、入口:getBean() → doGetBean()](#1、入口:getBean() → doGetBean())
- [2、创建单例:getSingleton(name, ObjectFactory)(外层壳)](#2、创建单例:getSingleton(name, ObjectFactory)(外层壳))
- [四、三级缓存详解:Spring 到底在内存里放了什么?](#四、三级缓存详解:Spring 到底在内存里放了什么?)
-
- [1、一级缓存:singletonObjects ------ 成品 Bean](#1、一级缓存:singletonObjects —— 成品 Bean)
- [2、二级缓存:earlySingletonObjects ------ 提前暴露的早期引用](#2、二级缓存:earlySingletonObjects —— 提前暴露的早期引用)
- [3、三级缓存:singletonFactories ------ 生成早期引用的工厂](#3、三级缓存:singletonFactories —— 生成早期引用的工厂)
- 4、三层缓存的流转顺序
- [五、关键方法:getSingleton(name) 如何用三层缓存拿 Bean?](#五、关键方法:getSingleton(name) 如何用三层缓存拿 Bean?)
- [六、doCreateBean:在哪一步把 Bean 提前暴露出去?](#六、doCreateBean:在哪一步把 Bean 提前暴露出去?)
- [七、A ↔ B 字段注入循环依赖:源码级时序走一遍](#七、A ↔ B 字段注入循环依赖:源码级时序走一遍)
-
- [1、容器开始创建 A:getBean("a")](#1、容器开始创建 A:getBean("a"))
- [2、doCreateBean("a") 内部](#2、doCreateBean("a") 内部)
- [3、开始创建 B:getBean("b")](#3、开始创建 B:getBean("b"))
- [4、创建 B 的过程中再次请求 A:getBean("a") → getSingleton("a")](#4、创建 B 的过程中再次请求 A:getBean("a") → getSingleton("a"))
- [5、B 完成创建 → 放入一级缓存](#5、B 完成创建 → 放入一级缓存)
- [6、回到 A 的创建现场 → 完成 A](#6、回到 A 的创建现场 → 完成 A)
- 八、为什么必须是"三级缓存"?两级缓存为什么不够?
- [九、为什么构造器 / prototype 循环依赖解决不了?](#九、为什么构造器 / prototype 循环依赖解决不了?)
-
- [1、构造器循环依赖:实例都 new 不出来,谈什么提前暴露](#1、构造器循环依赖:实例都 new 不出来,谈什么提前暴露)
- [2、prototype 不走单例缓存那一套](#2、prototype 不走单例缓存那一套)
- [十、AOP 场景下的 rawA / proxyA:为什么 Spring 拼命想让大家都拿到 proxyA?](#十、AOP 场景下的 rawA / proxyA:为什么 Spring 拼命想让大家都拿到 proxyA?)
- 十一、实战建议
一、引言:Spring 为什么要解决循环依赖?
在 Spring 中,Bean 之间通过依赖注入(DI)彼此引用是非常常见的事情。但当出现这种情况:
- A 依赖 B
- B 又依赖 A
就构成了 循环依赖(Circular Dependency)。
如果容器没能力处理这种情况,稍微复杂一点的业务系统就会在启动时疯狂报错。
所以 Spring 专门设计了一套 "三级缓存 + 提前暴露 Bean 引用" 的机制,来解决一部分循环依赖问题。
二、Spring 能解决/不能解决哪些循环依赖?
1、Spring 能自动解决的
仅限下面这一类:
- 作用域:singleton(单例)
- 注入方式:字段注入 / Setter 注入(非构造器)
- 容器允许循环依赖:
allowCircularReferences = true(Spring Boot 3 之后默认为 false,需要手动打开)
典型例子:
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
这种 单例 + 属性注入 的循环依赖,Spring 能自动帮你解开。
2、Spring 无法自动解决的
(1)构造器注入循环依赖
java
@Component
public class A {
private final B b;
public A(B b) { this.b = b; }
}
@Component
public class B {
private final A a;
public B(A a) { this.a = a; }
}
创建 A 需要先有 B,创建 B 又需要先有 A,实例都 new 不出来,Spring 没法"提前暴露半成品",只能抛异常:
text
BeanCurrentlyInCreationException
(2)prototype 作用域循环依赖
java
@Scope("prototype")
@Component
public class A {
@Autowired
private B b;
}
@Scope("prototype")
@Component
public class B {
@Autowired
private A a;
}
prototype 每次 getBean() 都新建一个对象,Spring 不会对 prototype 用单例那套缓存标记/提前暴露机制,所以也没法解决循环依赖。
三、Bean 创建的大致流程
1、入口:getBean() → doGetBean()
精简后的伪代码(非完整源码):
java
protected Object doGetBean(String name) {
// 1. 尝试从单例缓存中获取(包括早期引用)
Object sharedInstance = getSingleton(name);
if (sharedInstance != null) {
return sharedInstance;
}
// 2. 解析 BeanDefinition
RootBeanDefinition mbd = getMergedBeanDefinition(name);
// 3. 如果是单例,则通过 getSingleton(name, ObjectFactory) 创建
if (mbd.isSingleton()) {
Object bean = getSingleton(name, () -> createBean(name, mbd));
return bean;
}
// 其他 scope 略
}
这里有两个关键的 getSingleton:
getSingleton(String name):只负责从缓存中拿getSingleton(String name, ObjectFactory):不存在就创建 (里面会调用ObjectFactory.getObject(),实际走到createBean()→doCreateBean())
2、创建单例:getSingleton(name, ObjectFactory)(外层壳)
java
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Object singletonObject = singletonObjects.get(beanName);
if (singletonObject == null) {
// 标记这个 Bean 正在创建中
beforeSingletonCreation(beanName);
try {
// ✨ 真正创建 Bean(调用 createBean → doCreateBean)
singletonObject = singletonFactory.getObject();
// 创建完成后放入一级缓存
addSingleton(beanName, singletonObject);
} finally {
afterSingletonCreation(beanName);
}
}
return singletonObject;
}
真正的重头戏 在 createBean() → doCreateBean(),以及我们接下来要讲的三级缓存。
四、三级缓存详解:Spring 到底在内存里放了什么?
Spring 为了管理单例 Bean,维护了 3 个重要的 Map(都在 DefaultSingletonBeanRegistry 中):
1、一级缓存:singletonObjects ------ 成品 Bean
java
/** beanName -> 完全初始化好的 Bean 实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
特点:
-
存放的是完全初始化完成的 Bean:
- 构造器调用完成
- 依赖注入完成
- 各种初始化回调(
afterPropertiesSet、init-method等)都执行完 - 如果有 AOP,通常已经是代理对象
proxyA
-
你平时
applicationContext.getBean("a")最终拿到的就是这里面的对象
2、二级缓存:earlySingletonObjects ------ 提前暴露的早期引用
java
/** beanName -> 早期曝光的 Bean 引用(可能是半成品) */
private final Map<String, Object> earlySingletonObjects = new HashMap<>();
特点:
- 存放 "早期曝光的 Bean 引用",也就是半成品 Bean
- 用于在循环依赖场景下,让另一个 Bean 先用上这个"正在创建中的 Bean"
3、三级缓存:singletonFactories ------ 生成早期引用的工厂
java
/** beanName -> ObjectFactory,用于生成该 Bean 的早期引用 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
特点:
-
存的不是 Bean 对象,而是一个
ObjectFactory工厂 -
这个工厂的典型形式:
java() -> getEarlyBeanReference(beanName, rawBeanInstance); -
真正发生循环依赖,需要提前引用这个 Bean 时,才会调用工厂
getObject():- 无 AOP:返回
rawA - 有 AOP:返回
proxyA(代理对象)
- 无 AOP:返回
4、三层缓存的流转顺序
一句话:
三级缓存存工厂 → 需要时工厂生成"早期引用"放入二级缓存 → Bean 完成创建后进入一级缓存。
五、关键方法:getSingleton(name) 如何用三层缓存拿 Bean?
getSingleton(String beanName) 专门负责从缓存里取 Bean,是循环依赖时的关键。
简化伪代码:
java
public Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先从一级缓存拿(成品 Bean)
Object singletonObject = singletonObjects.get(beanName);
// 2. 一级没有,且该 Bean 正在创建中,才考虑早期引用
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (singletonObjects) {
// 2.1 再看二级缓存(早期引用)
singletonObject = earlySingletonObjects.get(beanName);
// 2.2 二级也没有,而且允许提前引用,才去动用三级缓存
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
if (singletonFactory != null) {
// 通过工厂生成早期引用(可能是代理对象)
singletonObject = singletonFactory.getObject();
// 缓存到二级
earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存中移除工厂
singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
关键点:
- 正常情况下:直接从一级缓存拿成品 Bean
- 正在创建中:才会考虑去二级/三级找早期引用
- 第一次用到三级缓存时,会立刻"下沉"为二级缓存(工厂用完就删)
六、doCreateBean:在哪一步把 Bean 提前暴露出去?
Spring 真正创建 Bean 的逻辑在 AbstractAutowireCapableBeanFactory#doCreateBean。
简化伪代码:
java
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化(只调用构造器,不注入属性)
Object beanInstance = createBeanInstance(beanName, mbd, args);
// 2. 是否需要提前暴露(单例 & 允许循环依赖)
boolean earlySingletonExposure = mbd.isSingleton() && allowCircularReferences && isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
// ✨ 关键:往三级缓存注册一个 ObjectFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, beanInstance));
}
// 3. 属性填充(@Autowired 等)
populateBean(beanName, mbd, beanInstance);
// 4. 初始化(调用各种 BeanPostProcessor、init-method等)
Object exposedObject = initializeBean(beanName, beanInstance, mbd);
return exposedObject;
}
注意:
addSingletonFactory放的是"将来如何拿早期引用"的工厂,而不是直接把 Bean 丢进二级缓存- 真正发生循环依赖时,才会触发三级缓存 → 工厂 → 二级缓存的流程
七、A ↔ B 字段注入循环依赖:源码级时序走一遍
假设:
java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
我们按调用顺序看一遍:
1、容器开始创建 A:getBean("a")
- 调用
doGetBean("a") - 单例 → 调用两参版
getSingleton("a", ObjectFactory) - 一级缓存中没有 → 调用
singletonFactory.getObject()→ 进入createBean("a")→doCreateBean("a")
2、doCreateBean("a") 内部
-
createBeanInstance("a"):javarawA = new A(); // 只构造,不注入 B -
addSingletonFactory("a", () -> getEarlyBeanReference("a", rawA));👉 A 的早期工厂进入 三级缓存
-
populateBean("a"):给 A 注入依赖- 发现需要 B → 调用
getBean("b")
- 发现需要 B → 调用
3、开始创建 B:getBean("b")
- 同样走到
getSingleton("b", ObjectFactory)→doCreateBean("b")
在 doCreateBean("b") 里:
-
createBeanInstance("b"):javarawB = new B(); -
addSingletonFactory("b", () -> getEarlyBeanReference("b", rawB)); -
populateBean("b"):- 发现字段
A a,需要getBean("a")
- 发现字段
4、创建 B 的过程中再次请求 A:getBean("a") → getSingleton("a")
现在 A 已经在"创建中"了,getSingleton("a") 的行为是:
-
一级缓存:
singletonObjects["a"]→ 没有(A 未完成) -
检测到 A 正在创建中 → 可以尝试早期引用
-
二级缓存:
earlySingletonObjects["a"]→ 还没有 -
三级缓存:
-
取出
singletonFactories["a"]中的ObjectFactory -
调用
getObject()→ 内部执行:javaearlyA = getEarlyBeanReference("a", rawA);- 如果无 AOP:通常是
return rawA; - 如果有
@Transactional/ 切面:这里可能返回proxyA
- 如果无 AOP:通常是
-
把
earlyA放进二级缓存:javaearlySingletonObjects["a"] = earlyA; singletonFactories.remove("a");
-
于是:
- B 在属性注入时,拿到的是 earlyA,也就是 A 的早期引用(可能已是代理)
5、B 完成创建 → 放入一级缓存
B 剩余流程:
-
继续
populateBean("b"),注入完成 -
调用
initializeBean("b")(执行初始化回调、BeanPostProcessor) -
得到完整体
fullB,放入一级缓存:javasingletonObjects["b"] = fullB;
6、回到 A 的创建现场 → 完成 A
-
控制流回到
populateBean("a")后面 -
给 A 注入 B 时调用
getBean("b"):- 此时 B 已在一级缓存中,直接拿到 fullB
-
执行
initializeBean("a") -
得到
fullA,加入一级缓存:javasingletonObjects["a"] = fullA; earlySingletonObjects.remove("a");
循环依赖成功解除。
八、为什么必须是"三级缓存"?两级缓存为什么不够?
要点只有一个:AOP / 事务代理造成的"rawA vs proxyA 区别"。
1、只有两级缓存会怎样?
假设只有:
- 一级:
singletonObjects(成品 Bean) - 二级:
earlySingletonObjects(早期 Bean)
那 doCreateBean("a") 时,只能这么做:
java
rawA = new A();
earlySingletonObjects["a"] = rawA; // 为了解决循环依赖
然后再执行 AOP / BeanPostProcessor:
java
proxyA = wrapIfNecessary(rawA);
singletonObjects["a"] = proxyA;
结果:
- B 在创建过程中注入的是 rawA(没事务、没切面)
- 其他 Bean 以后从容器拿的是 proxyA(有事务、切面)
👉 同一个 Bean,在容器里出现两种不同形态:
- 有的地方事务生效
- 有的地方事务不生效
严重违背了容器的一致性原则。
2、三级缓存的解决方案
三级缓存存的是一个"工厂":
java
addSingletonFactory("a", () -> getEarlyBeanReference("a", rawA));
- 此时 并不立即暴露 rawA
- 只是先把"以后怎么拿早期引用"的能力存起来
当真正发生循环依赖,需要提前暴露 A 时:
java
ObjectFactory<?> factory = singletonFactories["a"];
earlyA = factory.getObject(); // 内部可能返回 proxyA
earlySingletonObjects["a"] = earlyA;
singletonFactories.remove("a");
getEarlyBeanReference() 中可以:
- 根据是否有 AOP / 事务,决定是否生成代理
- 保证所有地方看到的早期引用都是同一份对象 (通常是
proxyA)
一句话总结这一节:
二级缓存只能存"对象结果",三级缓存存的是"生成对象的工厂",可以延迟到真正需要提前暴露时再做"要不要代理"的决策,从而保证容器内对同一 Bean 的引用形态一致。
九、为什么构造器 / prototype 循环依赖解决不了?
1、构造器循环依赖:实例都 new 不出来,谈什么提前暴露
构造器注入要求:
java
new A(b);
new B(a);
但刚才我们看到:
-
只有
createBeanInstance(beanName)之后,Spring 才有rawA -
然后才会:
javaaddSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, rawA));
构造器循环依赖中:
- 为了实例化 A,需要先准备 B
- 为了实例化 B,需要先准备 A
- A / B 都还没 new 出来,更别说注册三级缓存工厂
👉 所以构造器循环依赖在时序上 根本没有"半成品可以提前暴露"这一刻,Spring 只能报错。
2、prototype 不走单例缓存那一套
DefaultSingletonBeanRegistry 这套:
singletonObjectsearlySingletonObjectssingletonFactoriesisSingletonCurrentlyInCreation
都是服务于单例 Bean 的。
prototype 每次 getBean() 都新建:
- 不会进入这三层缓存
- 不会被标记为"currently in creation"
- 不会注册
ObjectFactory用于提前暴露
所以:
prototype Bean 一旦循环依赖,没有任何"提前暴露半成品"的机会,Spring 自然也就解不了环。
十、AOP 场景下的 rawA / proxyA:为什么 Spring 拼命想让大家都拿到 proxyA?
再用一个例子感受下 rawA vs proxyA:
java
@Service
public class UserService {
@Transactional
public void createUser() {
// 插入数据库
}
}
Spring 实际上做了两件事:
new UserService()→ 得到 rawUserService(rawA)- 判断有
@Transactional→ 用代理包装成 proxyUserService(proxyA)
调用:
-
proxyUserService.createUser()→ 会先进入代理逻辑:- 开启事务
- 调用
rawUserService.createUser() - 提交/回滚事务
-
若绕过代理直接调
rawUserService.createUser()→ 没有事务!
👉 所以容器必须保证:
能被外面拿到并使用的,尽量都是
proxyA,而不是rawA。
而三级缓存中的 getEarlyBeanReference 和 ObjectFactory,就是为了在循环依赖场景下,也尽可能把 proxyA(代理对象) 暴露给其他 Bean,而不是原始的 rawA。
十一、实战建议
1、实战建议
-
优先从设计上避免循环依赖
- 抽公共 Service / Manager
- 使用事件、回调、观察者等模式解耦
-
确实改不动时:
- 保证两端都是
singleton - 使用字段 / Setter 注入,而非构造器注入
- 必要时使用
@Lazy打破强实时依赖
- 保证两端都是
-
Spring Boot 3 / Spring 6 以后:
yamlspring: main: allow-circular-references: true如果你需要沿用 Spring 的自动解环能力,需要显式开启。
2、总结
Spring 只能自动解决 singleton + 字段/Setter 注入 场景下的循环依赖,核心是
DefaultSingletonBeanRegistry中的三级缓存机制。
- 一级缓存
singletonObjects存放完全初始化好的单例 Bean;- 二级缓存
earlySingletonObjects存放提前暴露的早期引用;- 三级缓存
singletonFactories存放一个ObjectFactory,用于在真正发生循环依赖时,通过getEarlyBeanReference生成早期引用(可能是 AOP 代理proxyA)。在
doCreateBean中,Spring 会在实例化 Bean 后、属性注入前,把这个工厂放进三级缓存;当另一个 Bean 注入它时,通过getSingleton从三级缓存得到早期引用并放入二级缓存,从而打破 A ↔ B 的循环。由于构造器注入在实例化前就需要依赖对象,prototype 又不使用这套单例缓存机制,所以 构造器循环依赖和 prototype 循环依赖是无法由 Spring 自动解决的。