第80篇:Spring循环依赖解决(2026版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第79篇:Spring源码阅读技巧 |
➡️ 下一篇:第81篇:Spring事件机制(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第79篇搞定了Spring源码阅读方法论,本篇深入Spring循环依赖解决。
BeanCurrentlyInCreationException是Spring开发者最常遇到的启动异常之一——两个Bean互相依赖,项目起不来。很多人背熟了“三级缓存”四个字,但一问“为什么需要三级而不是二级”“构造器注入为什么不行”就卡壳了。搞懂循环依赖,就是搞懂了Spring IoC容器最精妙的设计之一:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| 循环依赖是什么 | 两个Bean互相依赖会怎样? | A依赖B、B依赖A,形成“先有鸡还是先有蛋”的死锁 |
| 三级缓存是什么 | Spring靠什么解决循环依赖? | 三个Map——一级存成品、二级存半成品、三级存工厂 |
| 为什么能解决 | 三级缓存的原理是什么? | 提前暴露半成品——对象实例化后立即暴露引用,不等初始化完成 |
| 为什么需要三级 | 二级缓存不够吗? | 三级缓存专为AOP而生——代理对象需要在循环依赖时提前创建 |
| 什么情况解决不了 | Spring能解决所有循环依赖吗? | 构造器注入、原型Bean、多例模式——这三种不行 |
| @Lazy怎么用 | 不想改代码结构怎么办? | 用@Lazy延迟加载,让Spring注入代理对象 |
| 面试最爱问 | 高频考点有哪些? | 见文末 🎤 小节 |
一、核心知识点
1. 什么是循环依赖?
循环依赖是指两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系。最典型的场景是:
@ComponentpublicclassA{@AutowiredprivateBb;// A依赖B}@ComponentpublicclassB{@AutowiredprivateAa;// B依赖A,形成循环}如果不做特殊处理,Spring在创建A时需要注入B,创建B时需要注入A,陷入无限递归,最终抛出BeanCurrentlyInCreationException异常。
💡本质理解:循环依赖就是**“先有鸡还是先有蛋”**的问题——A的创建依赖B,B的创建依赖A,谁都没法先完成。
2. 循环依赖的产生时机
循环依赖发生在Bean生命周期的属性填充(populateBean)阶段:
| 阶段 | 说明 |
|---|---|
| ① 实例化 | 调用构造方法创建对象(分配内存) |
| ②属性填充 | 注入依赖的其他Bean——循环依赖发生在此 |
| ③ 初始化 | 执行自定义初始化逻辑 |
| ④ 完成 | 放入一级缓存 |
💡关键认知:实例化和属性填充是两个独立的阶段——对象实例化后、属性填充前,已经是一个“合法对象”(有内存地址、可被引用),只是属性还是空的。这是Spring能解决循环依赖的根本前提。
二、通俗讲解(1分钟开心学)
把循环依赖想象成“两个人互相等对方先开口”
- Bean A:我要等B先说话,我才说。
- Bean B:我要等A先说话,我才说。
- 结果:两个人就这么干等着,谁都不开口——死锁。
Spring的三级缓存就是“中间人”:
- 一级缓存(singletonObjects):已经聊完天、握过手的正式朋友(成品Bean)。
- 二级缓存(earlySingletonObjects):刚见面、还没深聊的半熟人(半成品Bean)。
- 三级缓存(singletonFactories):名片盒——里面放着每个人的联系方式(ObjectFactory),需要的时候随时打电话叫人。
解决过程:
- A来了,先发一张名片放进三级缓存(名片盒)。
- A要认识B,但B还没来,于是去创建B。
- B来了,也发一张名片放进三级缓存。
- B要认识A,去三级缓存翻A的名片,打电话把A叫来(提前暴露)。
- B认识了A(半成品),B完成创建,变成正式朋友进入一级缓存。
- A继续完成创建,也进入一级缓存。
💡关键洞察:提前暴露(Early Exposure)是核心——不等Bean完全初始化,先把“半成品”拿出来用。
三、三级缓存详解
3.1 三级缓存的定义
Spring在DefaultSingletonBeanRegistry类中定义了三个缓存:
| 缓存级别 | 缓存名称 | 存储内容 | 状态 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的成品Bean | 可用状态 |
| 二级缓存 | earlySingletonObjects | 提前暴露的半成品Bean(已实例化,未完成属性填充和初始化) | 半成品 |
| 三级缓存 | singletonFactories | ObjectFactory对象工厂——仅在调用getObject()时才会创建Bean实例 | 工厂 |
💡互斥规则:三个缓存是互斥的,同一个BeanName不会同时存在于多个缓存中。
3.2 三级缓存的分工
| 缓存 | 职责 | 关键特性 |
|---|---|---|
| 一级缓存 | 全局唯一对外提供可用的单例Bean | 用户最终获取的Bean均来自这里 |
| 二级缓存 | 缓存已通过三级工厂生成的早期对象,避免重复创建 | 提升性能,防止重复调用ObjectFactory.getObject() |
| 三级缓存 | 存放ObjectFactory,封装getEarlyBeanReference早期代理创建逻辑 | 无循环依赖时工厂永久不执行;仅发生循环依赖时才调用getObject() |
四、三级缓存解决循环依赖的完整流程
以A依赖B,B依赖A的经典场景为例:
4.1 关键步骤详解
| 步骤 | 操作 | 缓存变化 |
|---|---|---|
| ① | 实例化A | 无 |
| ② | A的工厂放入三级缓存 | 三级缓存:A的ObjectFactory |
| ③ | A填充属性,发现依赖B | 触发B的创建 |
| ④ | 实例化B | 无 |
| ⑤ | B的工厂放入三级缓存 | 三级缓存:A的工厂 + B的工厂 |
| ⑥ | B填充属性,发现依赖A | 从三级缓存取A的工厂→生成A的早期对象 |
| ⑦ | A的早期对象放入二级缓存 | 二级缓存:A(半成品) |
| ⑧ | B拿到A,完成创建 | 一级缓存:B(成品) |
| ⑨ | A拿到B,完成创建 | 一级缓存:A(成品)+ B(成品) |
五、核心源码解析
5.1 getSingleton() —— 三级缓存的核心入口
DefaultSingletonBeanRegistry.getSingleton()是解决循环依赖的核心方法:
@NullablepublicObjectgetSingleton(StringbeanName,booleanallowEarlyReference){// 第一步:从一级缓存获取成品BeanObjectsingletonObject=this.singletonObjects.get(beanName);// 如果一级缓存没有,且当前Bean正在创建中(循环依赖的核心判断条件)if(singletonObject==null&&isSingletonCurrentlyInCreation(beanName)){// 第二步:从二级缓存获取提前暴露的半成品BeansingletonObject=this.earlySingletonObjects.get(beanName);// 如果二级缓存也没有,且允许提前引用if(singletonObject==null&&allowEarlyReference){synchronized(this.singletonObjects){// 双重检查singletonObject=this.singletonObjects.get(beanName);if(singletonObject==null){singletonObject=this.earlySingletonObjects.get(beanName);if(singletonObject==null){// 第三步:从三级缓存获取ObjectFactoryObjectFactory<?>singletonFactory=this.singletonFactories.get(beanName);if(singletonFactory!=null){// 调用工厂创建早期对象singletonObject=singletonFactory.getObject();// 升级到二级缓存this.earlySingletonObjects.put(beanName,singletonObject);// 从三级缓存移除this.singletonFactories.remove(beanName);}}}}}}returnsingletonObject;}💡核心逻辑:一级→二级→三级,逐级查找。三级缓存找到了就“升级”到二级缓存,避免重复创建。
5.2 为什么需要三级缓存?(AOP场景)
很多人问:二级缓存不就能解决循环依赖了吗?为什么需要三级?
关键在AOP代理。
如果没有三级缓存(只有二级),在循环依赖发生时,直接暴露原始对象到二级缓存——但如果这个Bean需要AOP代理(比如有@Transactional),代理对象必须在初始化后才创建。
三级缓存的精妙设计:
| 场景 | 处理方式 |
|---|---|
| 无循环依赖 | 代理在postProcessAfterInitialization中统一创建 |
| 有循环依赖 | 代理提前通过三级缓存工厂创建,后置处理器不再重复生成 |
三级缓存存的不是Bean实例,而是**ObjectFactory**——一个函数式接口,只有在getObject()被调用时才会真正创建对象。getEarlyBeanReference()方法会检查是否需要AOP代理,如果需要就返回代理对象,否则返回原始对象。
💡一句话总结:三级缓存的存在,主要是为了解决AOP代理在循环依赖场景下的提前创建问题。
六、什么情况下Spring无法解决循环依赖?
6.1 三种无法解决的场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 构造器注入 | 构造器在实例化阶段执行,此时对象尚未创建,无法提前暴露引用 | 改用字段/Setter注入,或使用@Lazy |
| 原型(Prototype)Bean | 每次获取都新建,不进入缓存 | 改用单例,或手动管理 |
| 多例模式下的Setter注入 | 每次创建新实例,无限递归导致OOM | 避免在多例Bean之间循环依赖 |
6.2 构造器注入为什么不行?
构造器注入的本质问题在于JVM对象创建的两个阶段不可颠倒:
| 阶段 | 说明 |
|---|---|
| 阶段一:内存分配 | JVM在堆内存中开辟空间,所有属性赋默认值(null)——半成品 |
| 阶段二:初始化赋值 | 执行构造方法、成员变量赋值——成品 |
构造器注入将两个阶段原子绑定——必须在构造期间完成依赖注入,此时对象还没有分配内存地址,无法被外部引用。而Setter/字段注入允许先完成实例化(阶段一),暴露半成品引用,再完成属性填充(阶段二)。
💡记忆口诀:构造器注入“边盖房子边装修”,房子没盖好别人进不来;Setter注入“先盖好毛坯再装修”,毛坯房就能让人进去等了。
七、实战解决方案
7.1 方案一:改用字段/Setter注入(推荐)
问题代码:
@ComponentpublicclassA{privatefinalBb;// ❌ 构造器注入publicA(Bb){this.b=b;}}@ComponentpublicclassB{privatefinalAa;// ❌ 构造器注入publicB(Aa){this.a=a;}}解决方案:
@ComponentpublicclassA{@AutowiredprivateBb;// ✅ 字段注入}@ComponentpublicclassB{@AutowiredprivateAa;// ✅ 字段注入}7.2 方案二:使用@Lazy延迟加载
如果出于不可变性的考虑必须使用构造器注入,可以用@Lazy打破循环:
@ComponentpublicclassA{privatefinalBb;publicA(@LazyBb){// ✅ B被延迟加载this.b=b;}}@ComponentpublicclassB{privatefinalAa;publicB(Aa){this.a=a;}}💡原理:
@Lazy会让Spring注入一个代理对象,只有在第一次使用时才真正创建目标Bean。
7.3 方案三:重新设计(最彻底)
如果循环依赖频繁出现,说明模块边界可能不清晰。考虑:
- 提取公共依赖到第三个类
- 使用回调接口或事件发布解耦
- 重新审视职责划分
八、避坑要点
| 错误/误区 | 后果 | 正确做法 |
|---|---|---|
| 以为@Lazy能解决所有循环依赖 | 代理对象使用时可能NPE | @Lazy只适用于“非必须”的依赖 |
| 在原型Bean之间循环依赖 | 启动后无限递归导致OOM | 避免在多例Bean之间互相引用 |
| 以为Spring能解决所有循环依赖 | 构造器注入启动失败 | 构造器注入遇到循环依赖时用@Lazy |
在@PostConstruct中调用依赖的Bean | 可能拿到未初始化的半成品 | 在@PostConstruct中谨慎操作 |
九、面试高频考点
Q1:Spring如何解决循环依赖?
Spring通过三级缓存机制解决单例Bean的循环依赖。三级缓存分别是:一级缓存
singletonObjects(存放成品Bean)、二级缓存earlySingletonObjects(存放提前暴露的半成品Bean)、三级缓存singletonFactories(存放ObjectFactory对象工厂)。核心原理是提前暴露——Bean实例化后立即将ObjectFactory放入三级缓存,当发生循环依赖时,其他Bean可以从三级缓存获取ObjectFactory并调用getObject()得到早期引用,从而打破循环。
Q2:为什么需要三级缓存?二级缓存不够吗?
二级缓存只能解决普通对象的循环依赖。但如果Bean需要AOP代理(如
@Transactional),代理对象在初始化后才创建。三级缓存存的ObjectFactory可以在循环依赖发生时提前决定是返回原始对象还是代理对象。如果没有三级缓存,AOP代理在循环依赖场景下无法提前创建。
Q3:为什么构造器注入的循环依赖无法解决?
构造器注入在实例化阶段就必须完成依赖注入,而此时对象还没有分配内存地址,无法被外部引用。Setter/字段注入允许先实例化(分配内存、暴露引用),再填充属性。简单说:构造器注入“边盖房子边装修”,房子没盖好别人进不来;Setter注入“先盖好毛坯再装修”,毛坯房就能让人进去等了。
Q4:Spring能解决所有类型的循环依赖吗?
不能。Spring的三级缓存只能解决:单例Bean + Setter/字段注入的循环依赖。以下场景无法解决:①构造器注入;②原型(Prototype)Bean;③多例模式下的Setter注入。
Q5:@Lazy如何解决循环依赖?
@Lazy让Spring在注入依赖时不立即创建目标Bean,而是注入一个代理对象。当第一次使用该依赖时,代理对象才会触发真正的Bean创建。这样打破了“互相等待对方先创建”的死锁。
🎤 面试官追问陷阱(加分题)
追问1:“三级缓存中存的是ObjectFactory,那它什么时候会执行getObject()?”
👉只有发生循环依赖时才会执行。如果Bean没有发生循环依赖,三级缓存中的ObjectFactory永远不会被调用,Bean会在正常流程中完成初始化后直接放入一级缓存。当其他Bean在创建过程中发现依赖当前Bean,且当前Bean还在创建中(
isSingletonCurrentlyInCreation返回true),才会从三级缓存获取ObjectFactory并执行getObject()。
追问2:“Spring默认是开启循环依赖支持的,那能不能关闭?关闭了会怎样?”
👉能。Spring提供了一个开关
allowCircularReferences,默认为true。可以通过设置AbstractRefreshableApplicationContext.setAllowCircularReferences(false)关闭。关闭后,任何循环依赖都会直接抛出BeanCurrentlyInCreationException。生产环境不建议关闭——除非你100%确定项目中没有循环依赖。但大多数Spring Boot项目都有循环依赖(尤其是事务和AOP相关),关闭会导致启动失败。
追问3:“实际开发中,是应该让Spring解决循环依赖,还是主动避免?”
👉应该主动避免。三级缓存只是“救火”机制,不是“设计范式”。循环依赖本质上说明模块边界不清晰、职责划分不合理——A和B互相依赖,说明它们应该被合并或提取公共部分。主动避免的方式:①重新设计,提取公共接口;②使用
@Lazy延迟加载非核心依赖;③通过事件发布解耦。把循环依赖当成代码坏味道,而不是让Spring帮你兜底。
十、练习题
简答题:Spring的三级缓存分别是什么?各自的作用是什么?
代码题:写一个构造器注入导致循环依赖的示例,然后用
@Lazy解决它。分析题:某项目启动时报
BeanCurrentlyInCreationException,日志显示AService和BService互相依赖。两个Bean都使用了构造器注入。请给出两种解决方案,并说明各自的优缺点。
📊 你的学习进度
- 当前:第80篇 / 共108篇 ·进阶篇:Spring全家桶(第73~82篇)
- ✅ 已完成:基础篇44篇 + 第45~80篇
- 📖 正在学:第80篇
- ⏳ 待学习:第81~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第81篇:Spring事件机制》
内容简介:Spring事件驱动模型——
ApplicationEvent、ApplicationListener、@EventListener注解、异步事件、事务绑定事件。
👉Spring全家桶专题持续推进,拿下事件机制!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!