← 每日面试题 学习看板

什么是Spring的循环依赖问题?

面试题/00java/04 框架/01Spring/01Spring核心机制/什么是Spring的循环依赖问题?.md

面试记录 2026-09-28

题目:什么是Spring的循环依赖问题?

难度:difficulty/low

标签:spring/core, general/knowledge, difficulty/low

评分:⭐⭐⭐⭐☆(4/5)

我的回答

Spring 的循环依赖指的是,由于 Spring 的 Bean 对象是由 Spring 自身管理的,这时候你要使用一个对象的时候,注入、管理、创建都是由 Spring 操作的。如果你创使用的 A 对象调用 B 对象,生成的时候会调用 B 对象,但是 B 对象生成的时候又调用 A 对象,这时候就产生了循环依赖。

嗯,对应的处理方式主要看你是怎么调用的。Spring 默认的处理的方式,循环依赖的方式是通过三级缓存去对单例对象进行循环依赖的处理。如果你是用的其他模式的对象创建模式,它是无法处理循环依赖的。另外,循环依赖的处理在 Spring 中默认只能处理 set 注入的那种模式的循环依赖。如果你是构造器模式的注入,这时候它是无法处理对应的循环依赖的。

而实质上,我们在开发过程中就不应该出现循环依赖的东西,它属于一种你设计过程中出现了问题,应该将循环依赖的部分摘出去,放到独立的位置单独引用。

点评

优点:

不足:

补充:

完整讲解

第一部分:知识重点剖析

核心考点定位:考察对 Spring IoC 容器 Bean 创建流程的深度理解,特别是循环依赖场景下的三级缓存机制。

知识点拆解:

  1. 循环依赖的定义与判定:两个或多个 Bean 在创建过程中直接或间接地互相引用,导致 Spring 无法在任何一方完全初始化前完成注入。常见形式:A→B→A、A→B→C→A、构造器循环依赖。
  2. 三级缓存结构:
  1. 解决流程(以 A↔B、@Autowired setter、需要 AOP 为例):
  1. 为什么需要第三级(ObjectFactory):核心是 AOP 场景——若只有两级,Spring 不知道 Bean 是否需要被代理;L3 用工厂延迟决定"暴露代理还是原始对象",避免提前创建代理对象
  2. Spring 的限制:
  1. 最佳实践:

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. 为什么三级缓存必须存在第三级?如果只用两级会发生什么?
  1. 构造器循环依赖 Spring 一定解不了吗?有没有绕过方式?
  1. 在 Spring Boot 3 中,遇到循环依赖应该先改什么?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring 的循环依赖指的是两个或多个 Bean 在创建过程中互相需要对方——A 要注入 B,B 又要注入 A,Spring 在创建 A 时调用 B 的构造函数去创建 B,B 又反向需要 A,此时 A 还没初始化完成,就会卡死或者抛出异常。

Spring 官方是默认支持处理循环依赖的,核心方案是三级缓存:一级 singletonObjects 存放完全初始化好的成品 Bean,二级 earlySingletonObjects 存放已实例化但还没走完初始化的半成品,三级 singletonFactories 存放 ObjectFactory,用来按需生成半成品——这里的关键是 AOP 场景。当 A 实例化后,Spring 先把 A 的工厂放到三级缓存,然后开始填充属性时触发 B 的创建;B 在填充属性需要 A 时,从三级拿到 A 的 ObjectFactory,判断是否需要 AOP 代理,如果需要的话就提前生成一个代理对象,放到二级;B 拿到这个代理完成初始化后,回到 A 的填充流程,A 也是从二级拿到自己的代理对象继续走初始化流程,最后 A、B 都会进入一级缓存。

为什么必须三级而不是两级? 因为 AOP 代理的生成是延迟决策的——如果只用两级,Spring 就无法区分"要不要提前暴露代理",会导致 B 持有 A 的原始对象而 A 持有 B 的代理对象,两边引用不一致。

Spring 的限制:只支持 singleton 作用域、只支持 setter 或 field 注入,构造器注入的循环依赖 Spring 会直接抛出 BeanCurrentlyInCreationException,因为它在构造器还没执行完时就无法拿到对象引用去暴露。另外 Spring Boot 2.6 之后默认 allow-circular-references=false,遇到循环依赖会启动直接失败,这是官方在推工程实践的收紧。

实际开发中我的做法是尽量不出现循环依赖——循环依赖本身就说明两个 Bean 的职责边界没切好,我一般通过拆 Bean、抽第三方 Bean 承接公共逻辑、或者用事件解耦(ApplicationEvent)来打破循环。实在绕不过去的话用 @Lazy 让注入端先拿代理对象,本质是把真正的构造调用延后到使用阶段。