Spring是如何解决循环依赖

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

一、循环依赖基础认知

循环依赖是指两个或多个Bean之间形成相互依赖的闭环,比如Bean A依赖Bean B,Bean B同时依赖Bean A,若没有特殊处理,容器会陷入无限创建Bean的死循环,最终抛出创建异常。Spring针对循环依赖做了针对性适配,但并非所有循环依赖都能解决,核心仅支持单例Bean的setter/属性注入循环依赖

1.1 循环依赖常见分类

  • 构造器循环依赖:通过构造方法注入依赖,Bean实例化阶段就需要依赖对象,无法打破闭环,Spring无法解决
  • setter/属性注入循环依赖:Bean先完成实例化,再通过set方法/字段注入依赖,Spring可通过三级缓存解决
  • 原型作用域循环依赖:每次获取原型Bean都会创建新实例,无缓存机制,Spring无法解决

Spring解决循环依赖的核心思路:提前暴露未完全初始化的Bean早期引用,让依赖方先获取引用完成注入,后续再补全Bean的初始化逻辑,打破闭环死锁。

二、Spring解决循环依赖的核心:三级缓存机制

Spring容器在DefaultSingletonBeanRegistry类中维护了三级缓存(三个ConcurrentHashMap),分层存储不同生命周期阶段的Bean,通过缓存协同工作完成依赖注入,三级缓存各司其职、逐级降级获取Bean实例。

2.1 三级缓存详细说明

缓存级别

源码变量名

存储内容

核心作用

一级缓存(成品缓存)

singletonObjects

完全初始化完毕、可直接使用的单例Bean

存放最终成品Bean,供全局获取,保证单例唯一性

二级缓存(半成品缓存)

earlySingletonObjects

已实例化、未完成初始化的早期Bean(原始对象/代理对象)

缓存提前暴露的早期引用,避免重复创建早期Bean,解决普通循环依赖

三级缓存(工厂缓存)

singletonFactories

Bean工厂对象(ObjectFactory),延迟生成早期Bean/代理对象

解决AOP代理场景下的循环依赖,保证代理对象唯一性,避免原始对象与代理对象不一致

三、循环依赖解决全流程(以A依赖B、B依赖A为例)

以下是Spring创建单例Bean A、B并解决循环依赖的完整步骤,贴合源码执行逻辑,清晰体现三级缓存的流转过程:

步骤1:容器启动,开始创建Bean A

调用getSingleton(A)尝试从一级缓存获取A,此时A未创建,进入doCreateBean方法。先通过构造器实例化A(仅分配内存,属性未赋值、初始化未执行),得到A的原始半成品对象

步骤2:提前暴露A的工厂对象到三级缓存

实例化完成后,Spring会将A的ObjectFactory工厂存入三级缓存singletonFactories,标记A已进入实例化阶段,允许其他Bean提前获取A的引用。此时A仅完成实例化,未进行属性填充和初始化。

步骤3:填充A的属性,触发Bean B的创建

执行属性填充逻辑,发现A依赖B,转而调用getSingleton(B)创建B。重复步骤1-2:实例化B,将B的工厂对象存入三级缓存,随后填充B的属性,发现B依赖A。

步骤4:B从三级缓存获取A的早期引用

B填充属性需要A,调用getSingleton(A):先查一级缓存无A,再查二级缓存无A,最后从三级缓存获取A的工厂对象,调用getObject()生成A的早期引用(普通场景为原始对象,AOP场景为代理对象)。

生成后,将A的早期引用移入二级缓存earlySingletonObjects,并删除三级缓存中A的工厂对象,避免重复生成。B成功获取A的早期引用,完成自身属性填充和初始化,最终存入一级缓存。

核心答疑:A、B互相依赖,为何B不进二级缓存?看似双向依赖,但依赖获取的时机完全不同,这是判断是否进二级缓存的唯一标准:✅ B获取A的时机:A仅完成实例化、还在属性填充阶段,属于未初始化的半成品,没有成品A可用,必须提取A的早期引用,因此A需要存入二级缓存过渡;❌ A获取B的时机:A回头找B时,B已经完成属性填充+初始化全流程,是直接存入一级缓存的成品Bean,A无需调用B的半成品引用,自然不需要把B放入二级缓存。简言之:二级缓存只救“自身没完工,就被别人强行索要引用”的Bean,B是顺利完工的成品,无需二级缓存兜底。

步骤5:A完成属性填充和初始化

B创建完成后,A顺利获取B的成品对象,完成自身属性填充、初始化方法执行等后续逻辑。随后将A从二级缓存移入一级缓存,删除三级缓存中残留数据,A最终成为成品Bean。

步骤6:闭环完成

此时A和B均存入一级缓存,相互持有对方的合法引用,循环依赖闭环成功打破,容器正常启动。

二级缓存准入铁律:谁先被“中途截胡”,谁才进二级缓存。循环依赖中,只有先创建、先被对方提前依赖的半成品Bean(如本例A)需要二级缓存;后创建、顺利完工的Bean(如本例B)直接晋级一级缓存,全程不接触二级。

四、关键设计:为什么必须用三级缓存?

很多开发者疑惑二级缓存能否解决循环依赖,答案是:普通无AOP的循环依赖用二级缓存即可,但AOP代理场景必须用三级缓存

  • 若只用二级缓存:AOP代理会在Bean初始化后生成,循环依赖时B获取的是A的原始对象,而容器最终存放的是A的代理对象,导致对象不一致,破坏单例原则
  • 三级缓存的ObjectFactory实现延迟代理:只有发生循环依赖时,才通过工厂生成代理对象并放入二级缓存;无循环依赖时,代理对象在初始化后生成,保证逻辑一致性

五、Spring无法解决的循环依赖场景

以下场景即便开启缓存机制,Spring仍无法处理,会直接抛出BeanCurrentlyInCreationException异常:

  • 构造器注入的循环依赖:实例化阶段就需要依赖对象,无法提前暴露引用,彻底无法解决
  • 原型(prototype)作用域循环依赖:原型Bean无缓存,每次获取都会创建新实例,无法形成缓存闭环
  • @DependsOn强制循环依赖:注解强制指定Bean创建顺序,形成死锁,容器直接校验失败
  • 多线程并发创建单例Bean:并发场景下缓存读写冲突,可能导致早期引用异常

六、总结

Spring解决循环依赖的核心是三级缓存+提前暴露早期引用,仅适配单例Bean的setter/属性注入场景,通过分层缓存实现Bean生命周期的解耦,既保证了单例唯一性,又兼容AOP代理逻辑。日常开发中,应尽量避免构造器循环依赖、原型循环依赖等违规场景,减少容器启动风险。

ps:如果这篇帖子对于还在找工作和找实习的你有所帮助,可以关注我,给本贴点赞、评论、收藏并订阅专栏;同时不要吝啬您的花花

Spring 文章被收录于专栏

本专栏聚焦Spring全生态体系,从IoC/AOP核心原理入手,覆盖Spring Boot自动配置、事务管理、Web开发等实战内容。拆解循环依赖、动态代理等高频面试难点,助力开发者从入门到精通,打通单体到微服务的技术链路,解决企业级开发痛点,提升架构设计与问题排查能力,成为Java后端进阶的必备技术专栏。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
正在热议
更多
# 春招至今,你的战绩如何? #
9578次浏览 87人参与
# 你的实习产出是真实的还是包装的? #
1710次浏览 40人参与
# MiniMax求职进展汇总 #
23835次浏览 308人参与
# 军工所铁饭碗 vs 互联网高薪资,你会选谁 #
7453次浏览 43人参与
# 简历第一个项目做什么 #
31555次浏览 330人参与
# 重来一次,我还会选择这个专业吗 #
433352次浏览 3926人参与
# 米连集团26产品管培生项目 #
5735次浏览 214人参与
# 当下环境,你会继续卷互联网,还是看其他行业机会 #
186969次浏览 1122人参与
# 牛客AI文生图 #
21408次浏览 238人参与
# 不考虑薪资和职业,你最想做什么工作呢? #
152290次浏览 887人参与
# 研究所笔面经互助 #
118872次浏览 577人参与
# 简历中的项目经历要怎么写? #
310060次浏览 4193人参与
# AI时代,哪些岗位最容易被淘汰 #
63407次浏览 803人参与
# 面试紧张时你会有什么表现? #
30488次浏览 188人参与
# 你今年的平均薪资是多少? #
213009次浏览 1039人参与
# 你怎么看待AI面试 #
179861次浏览 1234人参与
# 高学历就一定能找到好工作吗? #
64313次浏览 620人参与
# 你最满意的offer薪资是哪家公司? #
76436次浏览 374人参与
# 我的求职精神状态 #
447984次浏览 3128人参与
# 正在春招的你,也参与了去年秋招吗? #
363243次浏览 2637人参与
# 腾讯音乐求职进展汇总 #
160584次浏览 1111人参与
# 校招笔试 #
470425次浏览 2963人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务