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后端进阶的必备技术专栏。

全部评论

相关推荐

头像
昨天 20:11
已编辑
百度_高级研发工程师
这篇继续盘点一下后端转 AI 方向面试时,最容易被面试官“扒皮”的几个工程落地场景。全是实打实的干货,希望能帮兄弟们避坑。一、 RAG(检索增强生成)的全链路拆解面试官极其看重你怎么把企业文档变成知识库的。这块如果你只是个纯调包侠(只会调 LangChain 的 API),被稍微一深挖绝对露馅。我跟他完整勾勒了整条数据流水线:从文档解析,到文本切片(Chunking)。这里有个加分项:一定要提**“按长度切分并保留 Overlap(重叠区)”**,这样能保证上下文语义不断裂。至于向量库选型,别干巴巴地只说一个。我给出的方案是:数据量极大、分布式要求高的场景直接上 Milvus;而轻量级、或者需要和传统关系型数据强绑定的场景,用 pgvector。顺带提一嘴查询时用的是“混合检索(Keyword + Vector)”,召回的精准度会靠谱很多。二、 大模型幻觉与 Prompt 约束兜底面试官必问:大模型胡说八道、乱承诺怎么办?对付这个,咱们后端有常规的三板斧:控参数: 调低模型生成时的 Temperature 参数,直接把发散性和创造性压下来。强指令: 在 Prompt 里加入极其严格的系统级指令兜底(比如:“如果你不知道,请直接回答不知道,严禁编造信息”)。引入 Few-Shot(少样本提示),给几个标准的问答 Case,把它的输出格式和边界死死限制住。三、 核心痛点:用 RocketMQ 做异步解耦与削峰AI 接口耗时极长,这是通病。面试时必须明确态度:大模型打分或推理,绝对不能同步阻塞主流程。当时的解法是:用户发消息后,聊天服务只管快速落库,然后立刻往 RocketMQ 里丢一条异步消息返回给前端。后端的打分微服务作为消费者,在后台慢慢跑,调完大模型再去更新数据库。进阶防坑: 面试官听到 MQ 肯定会追问重复消费。记得补一句:“我在消费端的 Java 代码里做了防重,基于业务主键(SessionID + MsgID)在 Redis 里做了 Key 校验,或者在 MySQL 用唯一索引兜底。坚决不能让大模型对同一条记录重复打分,浪费 Token 算力。”四、 全双工流式交互(WebSocket + SSE)解决 AI 响应慢导致用户吃灰的问题,还得靠前端流式输出。我重点聊了用 SSE(Server-Sent Events)和 WebSocket 技术,把大模型的响应“逐字”推给前端,而不是傻等到全部生成完再返回,这样能把首字延迟(TTFB)压到极致。进阶防坑: 聊流式交互必问断线处理。我们在网关层(如 Netty 构建的 WebSocket 集群)加了心跳保活机制(Ping-Pong)。一旦检测到死链接,立刻释放后端线程池资源;同时客户端配合自动重连和断点续传逻辑,保证流式数据不丢字。
AI求职实录
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
正在热议
更多
# 春招至今,你的战绩如何? #
10814次浏览 93人参与
# 你的实习产出是真实的还是包装的? #
1927次浏览 42人参与
# 米连集团26产品管培生项目 #
5991次浏览 216人参与
# 军工所铁饭碗 vs 互联网高薪资,你会选谁 #
7603次浏览 43人参与
# 简历第一个项目做什么 #
31715次浏览 338人参与
# 重来一次,我还会选择这个专业吗 #
433492次浏览 3926人参与
# 巨人网络春招 #
11353次浏览 223人参与
# 当下环境,你会继续卷互联网,还是看其他行业机会 #
187168次浏览 1122人参与
# 牛客AI文生图 #
21442次浏览 238人参与
# 不考虑薪资和职业,你最想做什么工作呢? #
152405次浏览 888人参与
# 研究所笔面经互助 #
118936次浏览 577人参与
# 简历中的项目经历要怎么写? #
310278次浏览 4216人参与
# AI时代,哪些岗位最容易被淘汰 #
63695次浏览 826人参与
# 面试紧张时你会有什么表现? #
30507次浏览 188人参与
# 你今年的平均薪资是多少? #
213102次浏览 1039人参与
# 你怎么看待AI面试 #
180077次浏览 1258人参与
# 高学历就一定能找到好工作吗? #
64329次浏览 620人参与
# 你最满意的offer薪资是哪家公司? #
76509次浏览 374人参与
# 我的求职精神状态 #
448101次浏览 3129人参与
# 正在春招的你,也参与了去年秋招吗? #
363434次浏览 2638人参与
# 腾讯音乐求职进展汇总 #
160658次浏览 1112人参与
# 校招笔试 #
471033次浏览 2964人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务