Spring 3 级缓存解决循环依赖

一、3 级缓存的源码定义

// DefaultSingletonBeanRegistry(Spring 核心源码)

public class DefaultSingletonBeanRegistry {

/** 1 级缓存:存放完全初始化好的 Bean(成品) */

private final Map<String, Object> singletonObjects =

new ConcurrentHashMap<>(256);

/** 2 级缓存:存放早期暴露的 Bean(半成品,已实例化但未填充属性) */

private final Map<String, Object> earlySingletonObjects =

new ConcurrentHashMap<>(16);

/** 3 级缓存:存放 ObjectFactory(能产生早期 Bean 引用) */

private final Map<String, ObjectFactory<?>> singletonFactories =

new HashMap<>(16);

}

3 级缓存本质:3 个 Map,分别存不同状态的 Bean。

缓存 内容 状态
singletonObjects(一级) 完整 Bean 已实例化 + 已注入 + 已初始化
earlySingletonObjects(二级) 早期 Bean 已实例化 + 未注入 + 未初始化
singletonFactories(三级) ObjectFactory 工厂方法(产生早期 Bean)

二、3 级缓存解决循环依赖的完整流程

场景:A 和 B 互相依赖(字段注入或 setter 注入
复制代码
@Servicepublic class A {    @Autowired    private B b;  // A 依赖 B}@Servicepublic class B {    @Autowired    private A a;  // B 依赖 A}
2.1 完整流程┌─────────────────────────────────────────────────────────┐

│ 步骤 1:实例化 A │
│ new A() → 创建 A 原始对象(属性 b 还是 null) │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 步骤 2:暴露早期引用(核心!) │
│ A-ObjectFactory → singletonFactories(3 级缓存) │
│ ⚠️ 此时 A 还是半成品,b=null │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 步骤 3:给 A 注入依赖 │
│ 需要注入 B(@Autowired B b) │
│ ↓ │
│ 去容器找 B → B 不存在 → 开始创建 B │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 步骤 4:实例化 B │
│ new B() → 创建 B 原始对象(属性 a 还是 null) │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 步骤 5:从 3 级缓存获取 A(关键步骤!) │
│ B 需要注入 A → 容器找 A → A 在 singletonFactories 中 │
│ ↓ │
│ 调用 A-ObjectFactory.getObject() → 创建 A 早期引用(半成品)│
│ ↓ │
│ ⚠️ A 早期引用 → earlySingletonObjects(2 级缓存) │
│ 此时 A 还是半成品,b=null │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 步骤 6:将 A 注入给 B │
│ B.a = A 早期引用(半成品) │
│ ↓ │
│ B 创建成功(B.a 已注入) │
│ ↓ │
│ B → singletonObjects(1 级缓存,完整 Bean) │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 步骤 7:回到 A,注入 B │
│ 步骤 3 暂停的地方继续:把 B 注入给 A │
│ A.b = B 完整引用 │
│ ↓ │
│ A 创建成功(A.b 已注入) │
│ ↓ │
│ A → singletonObjects(1 级缓存,完整 Bean) │
└─────────────────────────────────────────────────────────┘

2.2 关键时序

步骤 1: 实例化 A → 原始对象 A

步骤 2: A-ObjectFactory → singletonFactories(3 级)

步骤 3: 需要注入 B

步骤 4: 实例化 B → 原始对象 B

步骤 5: 从 singletonFactories 获取 A-ObjectFactory

→ 创建 A 早期引用

→ 放入 earlySingletonObjects(2 级)

步骤 6: B.a = A 早期引用 → B 创建成功 → singletonObjects(1 级)

步骤 7: A.b = B 完整引用 → A 创建成功 → singletonObjects(1 级)

三、3 级缓存的 4 大核心要点

3.1 为什么需要 3 级缓存?1 级不行吗?

1 级缓存为什么不行: // 假设只有 1 级缓存

步骤 1: new A() → A 原始对象

步骤 2: 需要注入 B → B 不存在 → 创建 B

步骤 3: B 需要注入 A → A 还是 null(没创建完)→ ❌ 死循环

2 级缓存(早期引用)为什么不够:

// 假设只有 2 级缓存(早期引用)

步骤 1: new A() → A 原始对象

步骤 2: A 早期引用 → earlySingletonObjects(2 级)

步骤 3: 需要注入 B → 创建 B

步骤 4: B 需要注入 A → 从 earlySingletonObjects 取 A 早期引用 ✅

步骤 5: B 创建成功

步骤 6: A 注入 B 成功

2 级缓存理论上能解决! 但 Spring 还是要 3 级,原因是 AOP

3.2 3 级缓存 vs AOP(核心原因

@Service

public class A {

@Autowired

private B b;

@Transactional // ⚠️ A 需要 AOP 代理

public void doSomething() { ... }

}

问题: A 在步骤 1 实例化时还是原始对象 (没 AOP 代理),但 B 在步骤 4 需要注入 A,如果注入原始对象,后续 A 的 AOP 代理就废了

3 级缓存的解决:

// 3 级缓存存的不是原始对象,是 ObjectFactory

singletonFactories.put("A", () -> {

// 这个 lambda 在调用时才执行

// 此时可以决定返回原始对象还是 AOP 代理对象

return getEarlyBeanReference("A", mbd, A 原始对象);

});

// getEarlyBeanReference 会判断 A 是否需要 AOP 代理

// 需要:返回 AOP 代理对象

// 不需要:返回原始对象

关键事实:

  • 3 级缓存存的是 ObjectFactory(lambda / 工厂方法)
  • B 注入 A 时调用 ObjectFactory.getObject()
  • getObject() 内部判断是否需要 AOP 代理
  • 这样保证 B 拿到的 A 是"最终版本"(可能是代理)
3.3 完整时序图(含 AOP)

步骤 1: 实例化 A → 原始对象 A

步骤 2: A-ObjectFactory → singletonFactories(3 级)

ObjectFactory 内部: getEarlyBeanReference() → 返回 A 代理对象(@Transactional 触发 AOP)

步骤 3: 需要注入 B

步骤 4: 实例化 B → 原始对象 B

步骤 5: 从 singletonFactories 获取 A-ObjectFactory

→ getObject() → 返回 A 代理对象(AOP 已生成)

→ 放入 earlySingletonObjects(2 级)

步骤 6: B.a = A 代理对象 → B 创建成功

步骤 7: A.b = B 完整引用 → A 创建成功

⚠️ A 此时也会经过 BeanPostProcessor#after 生成 AOP 代理

⚠️ 但 A 早期引用已经是代理对象了,所以最终一致

3.4 为什么 3 级缓存能解决,构造注入不行?

关键差异:

注入方式 实例化 vs 注入 3 级缓存能否解决
字段注入 实例化(new A)→ 暴露 ObjectFactory → 注入(setter 注入 b) 能解决(因为 setter 注入是后置的)
setter 注入 同上 能解决
构造注入 实例化(new A(B b))→ 必须立刻拿到 b 不能解决(构造方法执行时 b 还不存在)

结论:

  • 字段/setter 注入:实例化(先 new 一个空对象)→ 暴露 ObjectFactory → 注入依赖
  • 构造注入 :实例化(必须立刻拿到所有依赖)→ 没法分两步

四、面试官追问应对

追问:Spring 怎么解决循环依赖?

"3 级缓存

1.singletonObjects(完整 Bean)

2.earlySingletonObjects(早期引用)

3.singletonFactories(ObjectFactory)

核心流程:

1.A 实例化 → 暴露 ObjectFactory(3 级)

2.A 注入 B → 创建 B

3.B 注入 A → 从 3 级缓存获取 A 早期引用 → B 创建成功

4.A 注入 B → A 创建成功

注意:构造注入的循环依赖 3 级缓存解不了,要用 @Lazy 或改成 setter/字段注入。"

追问 2:3 级缓存为什么能解决 AOP + 循环依赖?

"3 级缓存存的不是 Bean 本身,是 ObjectFactory(lambda)。

ObjectFactory.getObject() 内部会判断是否需要 AOP 代理

  • 需要:返回 AOP 代理对象
  • 不需要:返回原始对象

这样 B 注入的 A 始终是'最终版本'(可能是代理),保证 AOP 不会失效。"

追问 3:构造注入的循环依赖为什么 3 级缓存解不了?

"构造注入必须立刻拿到所有依赖不能分两步(实例化 → 注入)。

但 3 级缓存的核心是**'先实例化空对象,再注入依赖',这要求注入是后置的**。

  • 字段注入 ✅(后置,setter 注入)
  • setter 注入 ✅(后置)
  • 构造注入 ❌(前置,必须立刻拿到依赖)

解决:构造注入 + @Lazy(参数是代理对象,不真正创建)。"

追问 4:@Lazy 怎么解决循环依赖?

"@Lazy 注入的是代理对象(不是 Bean 本体),第一次调用时才真正创建。

流程:

1.A 构造注入 @Lazy B → 拿到 B 的代理对象

2.A 创建成功

3.真正用到 B 时才创建 B

4.B 创建时 A 已经存在 → 不会循环

@Lazy 是打破时序的关键。"

追问 5:3 级缓存能解决构造注入 + @Async 的循环依赖吗?

"不能

  • 字段/setter 注入 + @Async ✅(3 级缓存 + AOP 代理都能解决)
  • 构造注入 + @Async ❌(构造注入本身不能循环)

解决:构造注入 + @Lazy + @Async(两个都用)。"

五、记忆口诀

"3 级缓存:完整 → 早期 → ObjectFactory"

"ObjectFactory 内部判断 AOP,返回代理或原始对象"

"字段/setter 注入能解,构造注入不能解"

"构造 + @Lazy 解决循环,时序是关键"

"prototype 循环依赖 3 级缓存解不了"

六、一句话总结

"Spring 3 级缓存解决循环依赖

1.singletonObjects(完整 Bean,1 级)

2.earlySingletonObjects(早期引用,2 级)

3.singletonFactories(ObjectFactory,3 级)

核心:实例化 A → 暴露 ObjectFactory → 注入 B → B 从 ObjectFactory 拿 A 早期引用 → B 创建成功 → A 注入 B → A 创建成功。

关键限制:构造注入的循环依赖 3 级缓存解不了,要用 @Lazy 或改成 setter/字段注入。"

相关推荐
摇滚侠1 小时前
SpringMVC 入门到实战 获取请求参数 25-32
java·spring·intellij-idea
咖啡八杯1 小时前
【无标题】
java·后端·设计模式
mqiqe1 小时前
面试题-MyBatis 面试篇
java·面试·mybatis
摇滚侠1 小时前
SpringMVC 入门到实战 @RequestMapping 14-24
java·spring
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【80】可观测集成
java·人工智能·spring
TPBoreas1 小时前
spring3.5的本质改变
spring
Filwaod2 小时前
MCP 接入模式对比:Agent - Gateway - 业务项目 vs Agent - Adapter - 业务项目
java·agent·mcp
kuonyuma2 小时前
MyBatis入门·注解操作
java·spring boot·mysql·spring·mybatis
码界索隆2 小时前
Python转Java系列:面向对象基础
java·开发语言·python