2026年4月9日 发布于佛山
首段
在Java后端面试与技术进阶中,Spring循环依赖(Circular Dependency) 几乎是绕不开的核心考点,但很多开发者仅停留在“三级缓存能解决循环依赖”的模糊印象层面,遇到“为什么需要三级而不是二级”“构造器注入为什么不行”等追问时往往答不上来。本文通过考证AI助手辅助完成资料梳理与内容构建,从零开始系统拆解Spring如何通过三级缓存机制优雅破解单例Bean的循环依赖难题,涵盖定义、痛点、原理、代码示例、底层支撑和高频面试题,帮助读者建立完整知识链路。
一、痛点切入:传统方式下的“鸡生蛋”死锁

1.1 一个最直观的循环依赖示例
在日常开发中,最典型的循环依赖场景是这样的:
@Service public class OrderService { @Autowired private UserService userService; // 业务方法... } @Service public class UserService { @Autowired private OrderService orderService; // 业务方法... }
这是最常见的字段注入(Field Injection)场景。OrderService依赖UserService,UserService又依赖OrderService,形成了“你中有我、我中有你”的依赖闭环-2。如果在传统对象管理体系中,创建OrderService需要先有UserService,创建UserService又需要先有OrderService,两者互相等待,陷入死锁。Spring启动时会抛出BeanCurrentlyInCreationException异常-2。
1.2 传统方式的缺陷分析
这种循环依赖问题在Spring出现早期,开发者只能通过手动调整Bean加载顺序或重构代码来规避-2。随着现代企业应用平均依赖层级已达5-7层,手动解耦越来越难以满足需求-2。据2024年Java生态调研报告显示,约23%的Spring应用开发者曾遇到过循环依赖问题-2。
核心痛点总结:
代码耦合高:Bean之间的双向依赖本身就是设计缺陷的信号
启动失败风险:循环依赖未被妥善处理时直接抛出异常,可能引发微服务雪崩效应-2
调试困难:深层循环依赖往往掩盖设计缺陷,维护成本指数级上升-2
构造器注入场景完全无法解决:这恰恰是面试中的高频陷阱
正是在这样的背景下,Spring框架设计出三级缓存机制,成为解决单例Bean循环依赖问题的精妙方案。
二、核心概念讲解:什么是循环依赖
2.1 标准定义
循环依赖(Circular Dependency) ,指的是两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系。最典型的场景是Bean A依赖Bean B,同时Bean B又依赖Bean A-1。
2.2 生活化类比
可以把Bean创建过程类比为“拼乐高”:每个Bean的完整构建分为三个步骤——实例化(拿出零件)、属性填充(拼上配件)、初始化(完成质检)。当A拼到一半发现需要B的某个配件才能继续,于是去找B;结果B也拼到一半,发现需要A的配件。两人互相等对方先拼完,都卡住了。
循环依赖的三种形态:
| 依赖类型 | 注入方式 | Spring是否支持 | 原因简析 |
|---|---|---|---|
| 构造器循环依赖 | 通过构造函数注入 | ❌ 不支持 | 实例化时就必须提供依赖,无法提前暴露 |
| Setter循环依赖 | 通过setter方法注入 | ✅ 支持 | 实例化后可先暴露半成品 |
| 字段注入循环依赖 | 通过@Autowired字段注入 | ✅ 支持 | 本质上是Setter注入的变体 |
-3
三、关联概念讲解:三级缓存的定义与职责
3.1 标准定义
三级缓存是Spring在DefaultSingletonBeanRegistry类中维护的三个Map集合,用于分层存储不同状态的单例Bean实例-2:
| 缓存级别 | 源码变量名 | 核心存储内容 | 核心作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的单例Bean | 供业务直接获取使用,是最终的“成品缓存” |
| 二级缓存 | earlySingletonObjects | 提前暴露的半成品Bean | 避免同一Bean在循环依赖中被反复创建,起“临时占位”作用 |
| 三级缓存 | singletonFactories | ObjectFactory工厂对象 | 存储“生成Bean实例的逻辑”,能在需要时动态生成代理对象 |
-6
3.2 类比理解:食堂打饭
一级缓存好比食堂出餐窗口——菜已做好,直接端走;二级缓存好比备餐台——菜做到一半,但已拿出来让大家知道“这道菜在做着”;三级缓存好比后厨的备料方案——暂时不决定做红烧还是清蒸,等人来点单时再决定做法。
3.3 底层原理与技术支撑
三级缓存机制底层依赖以下关键技术:
反射机制(Reflection) :Spring通过反射调用构造器完成Bean的实例化
动态代理(Dynamic Proxy) :为支持AOP场景(如
@Transactional),三级缓存中的ObjectFactory能在需要时动态生成代理对象-8ObjectFactory函数式接口:三级缓存存储的是() -> getEarlyBeanReference(...)形式的lambda表达式,实现“延迟创建代理”
四、概念关系与区别总结
4.1 三级缓存之间的协作关系
一句话概括: 一级存成品,二级存已确定的早期引用,三级存“将来可能生成代理”的工厂-8。
一级缓存是最终结果存放处,不参与循环依赖“破局”
二级缓存是临时存储提前暴露的Bean引用,防止重复生成
三级缓存是“工厂层”,在第一次被依赖时才动态决定——是否要生成代理对象
4.2 为什么需要三级缓存?二级不够吗?
这是面试中的核心追问,答案很关键:二级缓存无法解决“循环依赖 + AOP代理”的组合场景-6。
问题本质: 如果只有二级缓存,A在实例化后就必须立刻决定是否生成代理对象,但此时A的生命周期还没走到“初始化”阶段,无法判断它是否需要AOP增强(如@Transactional的切面信息)。如果先放原始Bean进二级缓存,B拿到的是原始对象;等到A后续初始化时才生成代理对象并放入一级缓存,此时B中注入的A和最终一级缓存中的A不是同一个对象,导致“代理对象不一致”问题-6。
三级缓存的解决之道: 将“要不要代理”的判断延迟到第一次被其他Bean引用时才计算。三级缓存存储的不是Bean对象本身,而是一个工厂逻辑() -> getEarlyBeanReference(A),当B需要A时,执行该工厂,此时A的生命周期已足够判断是否需要代理,然后生成正确的对象放入二级缓存供B使用-6。既按需代理,又不破坏Bean的生命周期顺序-8。
五、代码/流程示例演示
5.1 场景搭建:字段注入循环依赖
// ServiceA.java @Component public class ServiceA { @Autowired private ServiceB serviceB; public void doSomething() { System.out.println("ServiceA is working"); } } // ServiceB.java @Component public class ServiceB { @Autowired private ServiceA serviceA; public void doSomething() { System.out.println("ServiceB is working"); } }
5.2 执行流程详解
核心流程三步走:
创建ServiceA:实例化 → 放入三级缓存(存
ObjectFactory)→ 属性填充时发现依赖ServiceB创建ServiceB:实例化 → 放入三级缓存 → 属性填充时发现依赖ServiceA
破解循环:从三级缓存取ServiceA的工厂 → 执行工厂生成半成品 → 放入二级缓存 → 注入ServiceB → ServiceB完成初始化 → ServiceA接着完成
-1-32
5.3 对比新旧实现
| 维度 | 无三级缓存(传统方式) | 有三级缓存(Spring机制) |
|---|---|---|
| 构造器循环依赖 | ❌ 无法解决,启动抛异常 | ❌ 仍无法解决 |
| Setter/字段循环依赖 | ❌ 需手动调整加载顺序 | ✅ 自动解决 |
| AOP代理场景 | ❌ 代理对象不一致 | ✅ 注入正确的代理对象 |
| 开发者感知 | 需要大量手动配置 | 对开发者透明 |
六、底层原理/技术支撑点
6.1 源码定位
三级缓存机制的核心实现在DefaultSingletonBeanRegistry类中,关键源码如下-1:
// 一级缓存:存放完全初始化好的单例Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 三级缓存:存放Bean的工厂对象 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 二级缓存:存放提前暴露的半成品Bean实例 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 记录当前正在创建的Bean名称 private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
6.2 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<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } return singletonObject; }
-1
6.3 关键技术依赖
反射(Reflection) :通过反射调用构造器完成Bean实例化
动态代理(Dynamic Proxy) :
ObjectFactory.getObject()内部调用getEarlyBeanReference(),根据是否需要AOP决定返回原始Bean还是代理对象ObjectFactory函数式接口:将代理对象的创建延迟到第一次被引用时
七、高频面试题与参考答案
Q1:Spring是如何解决循环依赖的?
参考答案: Spring通过三级缓存机制解决单例Bean的Setter/字段注入场景下的循环依赖。三级缓存包括:一级缓存singletonObjects存成品Bean,二级缓存earlySingletonObjects存半成品Bean,三级缓存singletonFactories存ObjectFactory工厂。当A依赖B、B依赖A时,A实例化后将自己的工厂放入三级缓存;B创建时从三级缓存获取A的工厂并生成早期引用,完成B的初始化;B完成后A再继续完成初始化,从而打破循环-12。
Q2:为什么构造器注入的循环依赖无法解决?
参考答案: 因为构造器注入要求在实例化时就完成所有依赖的注入,而此时Bean还未创建完成,无法提前暴露。三级缓存机制依赖于“先实例化、再暴露半成品、后填充属性”的流程,而构造器注入将“实例化”和“依赖注入”绑定在一起,Spring没有机会将半成品提前暴露出去-3-49。
Q3:为什么需要三级缓存,二级不够吗?
参考答案: 二级缓存不够。原因是AOP代理场景:如果只有二级缓存,A实例化后就必须立即决定是否生成代理对象,但此时A的生命周期还未到初始化阶段,无法判断是否需要AOP增强(如@Transactional)。三级缓存存储的是ObjectFactory工厂,将“是否生成代理”的决策延迟到第一次被其他Bean引用时才执行,此时可以正确判断并生成代理对象,保证注入的对象和最终成品一致-6。
Q4:Spring能解决所有循环依赖吗?有哪些限制?
参考答案: 不能。Spring仅能解决单例作用域 + Setter注入/字段注入场景的循环依赖。以下场景无法解决:①构造器注入循环依赖;②多例(Prototype)Bean的循环依赖;③通过@Lazy注解虽可规避但并非真正解决-12-54。另外,从Spring Framework 5.3/Spring Boot 2.6开始,默认已禁用循环依赖,需要显式配置spring.main.allow-circular-references=true才能开启-12。
Q5:如何在设计层面避免循环依赖?(加分项)
参考答案: 虽然Spring提供了技术解决方案,但循环依赖通常是代码设计有问题的信号。可以从设计层面解决:①提取公共接口,让双方依赖抽象而非具体实现;②使用@Lazy延迟加载作为临时方案;③分析职责边界,将双向依赖拆分为三个类,引入中间层;④使用事件驱动(ApplicationEvent)将直接调用改为发布订阅模式-12。
八、结尾总结
8.1 核心知识点回顾
| 核心要点 | 一句话总结 |
|---|---|
| 什么是循环依赖 | Bean之间互相持有对方引用形成的闭环 |
| 三级缓存构成 | singletonObjects(成品) + earlySingletonObjects(半成品) + singletonFactories(工厂) |
| 为什么是三级 | 解决AOP代理场景下对象一致性问题,实现“延迟决定是否代理” |
| 适用条件 | 单例Bean + Setter/字段注入 |
| 不适用场景 | 构造器注入、多例Bean |
| 底层依赖 | 反射、动态代理、ObjectFactory |
8.2 重点与易错点强调
记忆口诀:一级存成品,二级存已确定,三级存工厂——等被依赖时才决定-8
常见误解:❌“三级缓存是为了性能”→ ❌错误,核心是为了解决AOP代理提前暴露问题-11
版本注意:Spring Boot 2.6+默认禁用循环依赖,面试时可主动提及,体现技术关注度
8.3 进阶预告
下一篇将深入Spring Bean生命周期与AOP代理的源码实现,从doCreateBean到populateBean到initializeBean,彻底打通Spring IoC的核心链路,敬请期待。
