Spring框架的核心考点之一——循环依赖,很多开发者平时用着没问题,一到面试就答不清楚。本文用AI助手搜集了最新的技术资料,系统梳理Spring解决循环依赖的底层原理、三级缓存机制及面试常考要点。
一、痛点切入:为什么需要关注循环依赖

开发中最常见的循环依赖场景如下:
@Servicepublic class OrderService { @Autowired private UserService userService; // OrderService依赖UserService } @Service public class UserService { @Autowired private OrderService orderService; // UserService依赖OrderService,形成闭环 }
在Spring Boot 2.6+版本中,默认已禁止循环依赖,启动时会直接抛出异常-27。即便在允许循环依赖的低版本中,若不做特殊处理,也会抛出BeanCurrentlyInCreationException。
旧有代码的典型问题是:Bean之间的依赖关系混乱,修改一处往往会牵连多处,耦合度越来越高,最终难以维护。Spring的三级缓存机制正是为了解决这一问题而设计的-。
二、核心概念讲解:什么是循环依赖
循环依赖(Circular Dependency),指两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系-1。典型场景即Bean A依赖Bean B,同时Bean B又依赖Bean A。
生活化类比:A说“我要干活,但我得先拿到B的工具”,B说“我也要干活,但我得先拿到A的工具”。A等B,B等A,谁也动不了,这就卡死了-12。
在Spring中,循环依赖分为三种类型-11:
构造器注入循环依赖:创建对象时即需依赖,无法解决,会报异常
多例模式setter注入循环依赖:每次getBean都会创建新实例,无穷无尽,最终导致OOM
单例模式setter/字段注入循环依赖:唯一能被Spring解决的场景
三、关联概念讲解:Spring的三级缓存
Spring解决循环依赖的核心是三级缓存机制,定义在DefaultSingletonBeanRegistry类中-1:
| 缓存级别 | 名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的Bean(成品) | 供业务直接使用 |
| 二级缓存 | earlySingletonObjects | 提前暴露的半成品Bean(已实例化,未填充属性) | 存储已确定的早期引用 |
| 三级缓存 | singletonFactories | ObjectFactory对象工厂 | 按需生成代理对象 |
三级缓存不存对象,而是存一个lambda或匿名内部类形式的ObjectFactory-2,仅在真正需要暴露早期引用时,才动态决定是否生成代理。
获取Bean的缓存读取顺序:一级缓存 → 二级缓存 → 三级缓存-11。
四、概念关系与区别总结
三级缓存的核心协作逻辑:一级存成品,二级存已确定的早期引用,三级存“将来可能生成代理”的工厂-2。
一句话概括:Spring通过提前暴露正在创建中的Bean引用,让依赖方先拿到一个“半成品”,等双方都完成后,再自动完成最终的依赖注入。
五、代码示例演示
以A依赖B、B依赖A为例,Spring的处理流程如下-49:
步骤1:创建A → 将A的ObjectFactory放入三级缓存singletonFactories
步骤2:填充A的依赖B → 触发B的生命周期
步骤3:创建B → 将B的ObjectFactory放入三级缓存
步骤4:填充B的依赖A → 从三级缓存获取A的工厂,生成早期引用,放入二级缓存earlySingletonObjects,并清除三级缓存中A的条目
步骤5:B初始化完成 → 移入一级缓存singletonObjects
步骤6:A继续填充 → 从一级缓存获取B,完成初始化
执行流程可简化为:getBean(A) → 实例化A → 放入三级缓存 → 填充A属性(发现依赖B)→ getBean(B) → 实例化B → 放入三级缓存 → 填充B属性(发现依赖A)→ 从三级缓存获取A工厂 → 生成代理对象 → 放入二级缓存 → B初始化完成 → 放入一级缓存 → A继续填充 → A初始化完成 → 放入一级缓存
六、底层原理/技术支撑
三级缓存机制底层依赖的核心技术包括:
反射机制:Spring通过反射实例化Bean对象,即便在未完成依赖注入时也能创建对象实例
代理模式:三级缓存中的
ObjectFactory采用了工厂模式设计,将“是否代理”的决定延迟到第一次被引用时-2singletonsCurrentlyInCreation集合:记录当前正在创建的Bean名称,用于判断是否发生循环依赖-1
为什么需要三级,两级不行吗? 因为Spring要兼顾两个需求:保证循环依赖能解,同时保证AOP代理正确生效。如果只有两级缓存,必须在创建Bean实例后立刻决定是否代理,但此时尚未走到初始化步骤,无法判断是否需要增强(如@PostConstruct方法里才加标记)-2。
七、高频面试题与参考答案
Q1:Spring是如何解决循环依赖的?
核心答案是三级缓存机制。Spring在创建Bean时,会提前将实例化后的对象(尚未完成依赖注入)以ObjectFactory的形式放入三级缓存,当检测到循环依赖时,其他Bean可以从三级缓存获取这个早期引用来完成自身创建,从而打破依赖闭环-。
Q2:为什么要用三级缓存,两级不行吗?
两级缓存无法同时满足循环依赖解决和AOP代理正确性。若只使用两级缓存,就必须在实例化后立即决定是否生成代理,但此时尚未执行初始化步骤(如@PostConstruct),无法判断是否需要AOP增强。三级缓存将“要不要代理”的判断延迟到第一次被其他Bean引用时,实现了按需代理-2。
Q3:构造器注入的循环依赖为什么解决不了?
因为构造器注入要求在实例化阶段就完成所有依赖注入,此时Bean尚未放入三级缓存,Spring无法提前暴露未完成的对象引用,导致循环依赖检测时无法提供早期引用,最终抛出BeanCurrentlyInCreationException-。
Q4:如果A依赖B、B依赖A,具体的创建流程是怎样的?
getBean(A) → 实例化A → 放入三级缓存 → 填充A属性(发现依赖B)→ getBean(B) → 实例化B → 放入三级缓存 → 填充B属性(发现依赖A)→ 从三级缓存获取A工厂 → 生成早期引用 → 放入二级缓存 → B初始化完成 → 放入一级缓存 → A继续填充 → A初始化完成 → 放入一级缓存-6。
Q5:Spring Boot 2.6+版本中如何处理循环依赖?
Spring Boot 2.6+版本默认禁止循环依赖,启动时会抛出异常。如需解决,可在配置文件中设置spring.main.allow-circular-references=true启用支持,但更推荐使用@Lazy延迟加载或重构代码设计从根本上解决问题-27。
八、结尾总结
本文核心知识点回顾:
循环依赖:两个或多个Bean互相引用形成闭环
三级缓存:
singletonObjects(一级)→earlySingletonObjects(二级)→singletonFactories(三级)核心原理:提前暴露半成品Bean引用,按需生成代理
唯一可解决场景:单例模式 + setter/字段注入
无法解决场景:构造器注入、多例模式注入
易错点提醒:
三级缓存不是为了解决所有循环依赖,而是精准支持单例 + 构造器之外注入的场景-2
@Lazy注解是解决构造器循环依赖最优雅的方式,通过注入代理对象打破循环-27
本文由AI助手辅助资料完成,数据均基于公开技术文章,力求准确。下一篇将深入讲解Spring AOP的代理机制与三级缓存的配合细节,敬请期待。
