《Java 100 天进阶之路》第80篇:Spring循环依赖解决(2026版)
2026/9/6 8:58:01 网站建设 项目流程

第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),需要的时候随时打电话叫人。

解决过程

  1. A来了,先发一张名片放进三级缓存(名片盒)。
  2. A要认识B,但B还没来,于是去创建B。
  3. B来了,也发一张名片放进三级缓存。
  4. B要认识A,去三级缓存翻A的名片,打电话把A叫来(提前暴露)。
  5. B认识了A(半成品),B完成创建,变成正式朋友进入一级缓存。
  6. A继续完成创建,也进入一级缓存。

💡关键洞察提前暴露(Early Exposure)是核心——不等Bean完全初始化,先把“半成品”拿出来用。


三、三级缓存详解

3.1 三级缓存的定义

Spring在DefaultSingletonBeanRegistry类中定义了三个缓存:

缓存级别缓存名称存储内容状态
一级缓存singletonObjects完全初始化完成的成品Bean可用状态
二级缓存earlySingletonObjects提前暴露的半成品Bean(已实例化,未完成属性填充和初始化)半成品
三级缓存singletonFactoriesObjectFactory对象工厂——仅在调用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帮你兜底


十、练习题

  1. 简答题:Spring的三级缓存分别是什么?各自的作用是什么?

  2. 代码题:写一个构造器注入导致循环依赖的示例,然后用@Lazy解决它。

  3. 分析题:某项目启动时报BeanCurrentlyInCreationException,日志显示AServiceBService互相依赖。两个Bean都使用了构造器注入。请给出两种解决方案,并说明各自的优缺点。

📊 你的学习进度

  • 当前:第80篇 / 共108篇 ·进阶篇:Spring全家桶(第73~82篇)
  • ✅ 已完成:基础篇44篇 + 第45~80篇
  • 📖 正在学:第80篇
  • ⏳ 待学习:第81~108篇

👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇

👉 下一篇文章预告

🚀下一篇:《第81篇:Spring事件机制》

内容简介:Spring事件驱动模型——ApplicationEventApplicationListener@EventListener注解、异步事件、事务绑定事件。

👉Spring全家桶专题持续推进,拿下事件机制!

📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询