专栏文章
专栏文章
Spring Framework 系列
1. Spring Framework 系列 #01:Spring 容器启动流程源码分析 2. Spring Framework 系列 #02:Spring 三级缓存与循环依赖 3. Spring Framework 系列 #03:Spring 事务管理 4. Spring Framework 系列 #04:Spring IoC 与 Bean 生命周期 5. Spring Framework 系列 #05:Spring AOP

Spring Framework 系列 #02:Spring 三级缓存与循环依赖

发布于 2026-05-26 09:43 👁 11 次阅读
#源码解析#spring#ioc#interview

Spring 用三级缓存解决 setter/field 注入的循环依赖。本文从"为什么需要三级"出发,逐步拆解每一级的职责、对象创建的完整时序,以及 AOP 代理场景下的特殊处理。面试高频题。

缩写速查Spring 术语与缩写速查 · 相关笔记Spring 容器启动流程源码分析 · SpringBoot 启动流程源码分析


目录

章节 说明
什么是循环依赖 问题定义与分类
三级缓存结构 三个 Map 的职责
解决流程详解 A↔B 循环依赖的完整时序
为什么需要三级而不是两级 核心问题:AOP 代理的时机
无法解决的场景 构造器注入、prototype、@Async
面试常见追问 高频变体问题

什么是循环依赖

@Component
class A {
    @Autowired B b;   // A 依赖 B
}

@Component
class B {
    @Autowired A a;   // B 依赖 A
}

Spring 创建 A 时需要注入 B,创建 B 时又需要注入 A,形成互相等待的死锁。

循环依赖的分类

类型 示例 Spring 能否解决 原因
setter / field 注入(单例) @Autowired 字段注入 ✅ 三级缓存解决 先实例化再注入,实例化后即可将 ObjectFactory 放入三级缓存,打破死锁
构造器注入 @Autowired 构造器参数 ❌ 无法解决 实例化时就需要依赖,构造器未执行完则无法放入缓存,死锁无法打破
prototype 作用域 @Scope("prototype") ❌ 无法解决 Spring 不缓存 prototype Bean,三级缓存依赖缓存机制,对 prototype 完全失效
@Async + 循环依赖 异步代理与循环依赖同时存在 ❌ 抛出异常 @Async 代理在 postProcessAfterInitialization 生成,晚于三级缓存暴露早期引用的时机,导致早期引用与最终代理不一致

三级缓存结构

Spring 在 DefaultSingletonBeanRegistry 中维护三个 Map:

// 一级缓存:完整的、可直接使用的 Bean
Map<String, Object> singletonObjects;

// 二级缓存:早期暴露的 Bean(已实例化,但属性注入/初始化未完成)
Map<String, Object> earlySingletonObjects;

// 三级缓存:Bean 工厂,调用时生成早期引用(可触发 AOP 代理)
Map<String, ObjectFactory<?>> singletonFactories;
缓存 Key Value 何时写入 何时移除
三级 singletonFactories beanName ObjectFactory<?> createBeanInstance 后,属性注入前 三级引用被取走时
二级 earlySingletonObjects beanName 早期 Bean 引用(可能是代理) 从三级取出时自动写入 Bean 完全初始化后
一级 singletonObjects beanName 完整 Bean initializeBean 全部完成后 容器关闭时

解决流程详解

spring three level cache

A 依赖 B,B 依赖 A 为例,完整时序与每步后的缓存状态:

步骤 主要操作 三级 singletonFactories 二级 earlySingletonObjects 一级 singletonObjects
① 实例化 A createBeanInstance(A)addSingletonFactory("A", factory) A: factory
② 注入 A→B,实例化 B createBeanInstance(B)addSingletonFactory("B", factory) A: factory
B: factory
③ 注入 B→A,命中三级 factory.getObject() 生成 earlyA(若需 AOP 则生成代理)
earlyA 写入二级,A 从三级移除
~~A 已移除~~
B: factory
A: earlyA
④ B 初始化完成 initializeBean(B) 完成
B 写入一级,B 从三级移除
~~B 已移除~~ A: earlyA B: 完整 Bean
⑤ A 初始化完成 initializeBean(A) 完成
A 写入一级,A 从二级移除
~~A 已移除~~ A: 完整 Bean
B: 完整 Bean

为什么需要三级而不是两级

如果去掉三级缓存,只用两级

假设实例化后直接把原始 Bean 放入二级缓存,时间线如下:

时刻 操作 一级缓存 二级缓存 B 持有的 A
T1 A 实例化(new A()
T2 原始 A 放入二级缓存 原始 A
T3 创建 B,B 需要注入 A 原始 A
T4 从二级缓存取 A 注入给 B 原始 A 原始 A
T5 B 创建完成 原始 A 原始 A
T6 A 初始化完成,AOP 生成代理 A 代理 A 原始 A
最终 容器就绪 代理 A 原始 A ≠ 代理 A

B 持有原始 A,而容器中流通的是代理 A,两者不是同一对象,B 调用 a.method() 不走代理,事务/AOP 全部失效

为什么不能事后把 B 持有的引用更新为代理 A?

虽然 Spring 知道哪些 Bean 依赖了 A(BeanDefinition 图完整),可以反射更新 b.a 字段,但有一个根本问题无法解决:

@Service
class B {
    @Autowired
    private A a;

    @PostConstruct
    public void init() {
        this.localRef = a;   // 初始化时把引用缓存到本地变量
    }

    private A localRef;

    public void doSomething() {
        localRef.method();   // 用的是 localRef,不是字段 a
    }
}

Spring 只能更新 b.a 字段,但 localRef 已经是原始 A 的副本,追踪不到,更新不了。事后修补永远存在遗漏风险,不如从源头保证正确。

三级缓存的解法

三级缓存存的是 ObjectFactory<?> 而不是对象本身,在 B 取 A 的那一刻就提前生成代理

// Spring 源码:addSingletonFactory 时注册的工厂
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// getEarlyBeanReference → 触发 AbstractAutoProxyCreator
// → 如果 A 需要 AOP,此时提前生成代理 A 返回给 B

时间线对比:

时刻 操作 一级缓存 二级缓存 三级缓存 B 持有的 A
T1 A 实例化
T2 ObjectFactory 放入三级缓存 () → 代理A
T3 创建 B,B 需要注入 A () → 代理A
T4 调用工厂,提前生成代理 A,移入二级缓存 代理 A
T5 代理 A 注入给 B 代理 A 代理 A
T6 B 创建完成 代理 A 代理 A
T7 A 初始化完成,二级缓存代理 A 晋升一级 代理 A 代理 A
最终 容器就绪 代理 A 代理 A = 一级缓存

两种方案关键差异对比:

T4 时 B 拿到的 A 最终一级缓存的 A 是否一致
两级缓存 原始 A 代理 A ❌ 不一致,AOP 失效
三级缓存 代理 A 代理 A ✅ 一致

一句话总结

二级缓存能解决循环依赖,但无法保证 AOP 代理的唯一性。三级缓存通过 ObjectFactory 在 B 取 A 的那一刻就提前生成代理,从源头保证全程只有一个版本流通,而非事后修补。


无法解决的场景

1. 构造器注入

@Component
class A {
    A(B b) { }   // 构造器注入
}

实例化时就需要 B,此时 A 还没有任何引用可以放入缓存,死锁无法打破。

解法@Lazy 让其中一个延迟注入(注入代理,首次调用时才真正初始化):

@Component
class A {
    A(@Lazy B b) { }
}

2. prototype 作用域

Spring 不缓存 prototype Bean(每次 getBean 创建新实例),三级缓存机制不适用。

3. @Async + 循环依赖

@Async 方法由 AsyncAnnotationBeanPostProcessorpostProcessAfterInitialization 阶段生成代理,晚于三级缓存暴露早期引用的时机,导致早期暴露的引用与最终代理不一致,Spring 检测到后直接抛出:

BeanCurrentlyInCreationException: 
  Bean with name 'xxx' has been injected into other beans in its raw version 
  as part of a circular reference, but has eventually been wrapped.

解法:重构设计消除循环依赖,或将 @Async 方法提取到独立的 Bean。


面试常见追问

Q:Spring 三级缓存中,二级缓存的作用是什么?

性能优化。如果多个 Bean 同时依赖 A,第一次从三级缓存取出后放入二级缓存,后续直接从二级缓存取,不需要重复调用 ObjectFactory.getObject()(避免重复创建代理)。

Q:为什么构造器注入不能解决循环依赖?

实例化 Bean 必须先调用构造器,调用构造器前无法将任何引用放入缓存。三级缓存的机制依赖"先实例化、再注入"的顺序,构造器注入打破了这个顺序。

Q:Spring Boot 默认允许循环依赖吗?

Spring Boot 2.6+ 默认禁止循环依赖,启动时报 BeanCurrentlyInCreationException。可通过 spring.main.allow-circular-references=true 开启,但官方不推荐——循环依赖通常意味着设计问题。

Q:@Lazy 是如何解决构造器循环依赖的?

@Lazy 让 Spring 注入一个 CGLIB 代理对象(而非真实 Bean),代理对象在首次方法调用时才触发真正的 Bean 初始化。这样构造器能顺利完成,打破了死锁。

Q:循环依赖一定是坏的设计吗?

是的。循环依赖说明两个类互相耦合,职责边界模糊。解法通常是:提取公共依赖到第三个类、使用事件解耦、重新划分职责。


参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论