面试必问:Spring 循环依赖的三种方式两万字详解
一、先搞清楚什么是循环依赖很多同学第一次听到「循环依赖」这个词脑子里第一反应是这不就是 A 依赖 B、B 又依赖 A 吗这个理解方向没错但如果只停留在这个层面面试是答不好的。我们先给一个严谨一点的定义。循环依赖Circular Dependency也叫循环引用指的是两个或两个以上的 Bean 在创建过程中互相依赖对方从而形成一个依赖环。最经典的场景就是 A Bean 里注入了 B BeanB Bean 里又注入了 A Bean。稍微复杂一点的场景是 A 依赖 B、B 依赖 C、C 又依赖 A本质上还是绕了一圈回到原点。在传统 Java 开发里如果你手动 new 对象循环依赖其实不那么难处理先 new A拿到的是个「半成品」再把半成品 A 传给 B最后把 B 传给 A把 A 修补完整。但在 Spring 容器里Bean 的创建、注入、代理、生命周期回调都由容器统一管理问题就复杂得多。尤其是 Bean 的创建并不是一行 new 就结束了它有一个完整而严密的流程。这里需要先记住一个非常关键的概念后面所有的源码分析都围绕它展开Spring 创建一个单例 Bean大致可以分为两个大阶段。实例化Instantiation通过构造器反射创建一个对象实例。这个时候对象已经 new 出来了但还没有进行依赖注入、初始化和代理通常被称为「早期引用」或「半成品对象」。初始化Initialization给实例填充属性、调用 Aware 接口回调、执行 BeanPostProcessor 的前置和后置处理、执行 init-method 等。我们常说的「依赖注入」「AOP 代理」大多发生在这个阶段。这个「实例化」和「初始化」的区分是 Spring 能解决部分循环依赖的根本原因。因为一个对象只要实例化完成内存里就已经有了它的引用哪怕它还没初始化完容器也可以先把这段「早期引用」交给别人使用等后续再慢慢把它填充完整。如果连实例化都做不到比如构造器里就需要对方作为参数那就真的没办法了。这一点我们在后面讲三种方式时会反复用到。先看一个最基础的循环依赖代码建立直观印象javaimport org.springframework.stereotype.Component; Component public class A { private B b; public A(B b) { this.b b; } } Component public class B { private A a; public B(A a) { this.a a; } }上面这段代码启动 Spring 容器时大概率会直接报BeanCurrentlyInCreationException因为 A 的构造器在等待 BB 的构造器又在等待 A谁都没法先完成实例化。这就是我们马上要讲的第一种方式构造器注入的循环依赖。接下来我们进入正题把循环依赖的三种方式一一拆解清楚。二、循环依赖的三种方式在 Spring 面试中「循环依赖有哪几种方式」这个问题通常不是让你列举数学上的组合而是考察你能否把循环依赖按「注入方式」或「Bean 作用域」分类并说清楚每一类 Spring 到底能不能解决、为什么。综合主流面试题和源码实际行为我们一般把循环依赖分成以下三种方式方式一构造器注入循环依赖Spring 无法解决直接抛异常。方式二Setter 注入或字段注入循环依赖在单例作用域下Spring 可以通过三级缓存解决。方式三Prototype 原型作用域循环依赖Spring 无法解决直接抛异常。下面我们逐个展开。为了让文章可落地每个方式都会给出可以复现的 Java 代码、运行现象和背后的原因。方式一构造器注入循环依赖构造器注入是很多团队推荐的注入方式因为它能让依赖不可变、更容易暴露依赖过多的问题。但它的代价就是一旦出现循环依赖Spring 完全无能为力。我们写两个通过构造器互相依赖的 Beanjavaimport org.springframework.stereotype.Component; Component public class A { private final B b; public A(B b) { this.b b; System.out.println(A 构造器执行注入了 B); } } Component public class B { private final A a; public B(A a) { this.a a; System.out.println(B 构造器执行注入了 A); } }启动 Spring 容器你会看到类似这样的异常栈。textorg.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name a: Requested bean is currently in creation: Is there an unresolvable circular reference or an asynchronous initialization cycle?异常信息的关键词是Requested bean is currently in creation。它表达的含义是容器正在创建 A还没创建完成创建过程中又回头来要 A于是陷入死循环。为什么构造器注入一定解决不了原因要从 Bean 的创建流程看。构造器注入发生在「实例化」阶段。要 new A必须先拿到 B要 new B又必须先拿到 A。可这个时候 A 和 B 都还没有 new 出来容器手里一个实例都没有自然没法把谁先交给谁。也就是说构造器循环依赖卡在了第一个阶段连「早期引用」都无法产生。既然没有早期引用后续的三级缓存机制也就派不上用场。面试时可以用一句话总结构造器循环依赖发生在实例化阶段此时对象尚未创建出来Spring 拿不到任何早期引用因此无法解决。方式二Setter 注入和字段注入循环依赖第二种方式是面试的重头戏也是 Spring 三级缓存存在的核心意义。这里说「Setter 注入和字段注入」是因为它们在循环依赖中的表现基本一致依赖注入都发生在实例化之后的「属性填充」阶段。先看 Setter 注入的例子javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; Component public class A { private B b; Autowired public void setB(B b) { this.b b; System.out.println(A 的 setter 注入了 B); } } Component public class B { private A a; Autowired public void setA(A a) { this.a a; System.out.println(B 的 setter 注入了 A); } }再看字段注入的例子这是日常开发中最常见的写法javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; Component public class A { Autowired private B b; } Component public class B { Autowired private A a; }这两段代码启动 Spring 容器都不会报循环依赖异常应用可以正常跑起来。原因是无论 setter 注入还是字段注入A 的构造器都不需要 B 作为参数因此 A 可以先被实例化出来。实例化完成后Spring 会把 A 的「早期引用」先记录下来再去填充 A 的属性。填充时发现需要 B容器就去创建 BB 同样先实例化再填充属性时发现需要 A此时容器把之前缓存的 A 的早期引用交给 B。B 填充完成、初始化完成后容器再把完整的 B 交给 A。整个链路就通了。这个过程背后的数据结构就是大名鼎鼎的三级缓存。很多面试官问「Spring 怎么解决循环依赖」其实主要问的就是方式二。三级缓存的细节我们在第三大节专门展开这里先记住结论单例 Bean 通过 Setter 或字段注入产生的循环依赖在绝大多数情况下是能被 Spring 解决的。不过面试中还有一个非常高频的追问既然 Setter 和字段注入能解决为什么不推荐字段注入虽然字段注入能解决循环依赖但它有很多工程上的缺点依赖隐藏在字段里导致类外部无法看清依赖、不方便用 final 保证不可变、强耦合 Spring 容器导致单元测试不方便、依赖过多时不容易暴露设计问题等。构造器注入虽然解决不了循环依赖但可以通过良好的分层设计来避免这种循环而不是靠字段注入掩盖问题。这个平衡点也要能讲清楚。方式三Prototype 原型作用域循环依赖第三种方式是原型作用域下的循环依赖。前面说的三级缓存只对单例 Bean 生效。对于prototype作用域的 BeanSpring 同样无法解决循环依赖。写两个原型 Bean 的例子javaimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; Component Scope(prototype) public class A { Autowired private B b; } Component Scope(prototype) public class B { Autowired private A a; }哪怕我们用的是字段注入启动容器或者从容器中获取这些原型 Bean 时仍然会抛出BeanCurrentlyInCreationException。原因也很好理解Spring 不会缓存 Prototype 作用域的 Bean。单例 Bean 可以复用同一个实例所以容器愿意花成本把它放进缓存方便打破循环而原型 Bean 每次获取都要创建一个全新的实例容器不可能、也不应该把每个原型实例都缓存起来。没有缓存就没有地方存放「早期引用」循环依赖自然无法打破。更深一层看Spring 在源码中会主动检查这一点。在AbstractBeanFactory#doGetBean里有一段逻辑如果发现当前 Bean 是 prototype 且正在创建中会直接抛出循环依赖异常。这个代码我们后面源码部分会看到。把这三种方式汇总成一张表面试前可以反复看循环依赖方式能否解决核心原因构造器注入循环依赖不能解决卡在实例化阶段拿不到早期引用Setter 或字段注入循环依赖单例可以解决实例化完成后可先暴露早期引用通过三级缓存打破循环Prototype 作用域循环依赖不能解决原型 Bean 不缓存没有早期引用可复用到这里三种方式已经讲清楚了。但如果你只停留在「知道结论」面试时很容易被追问到源码层面就露馅。下面我们把 Spring 解决循环依赖的底层机制也就是三级缓存从头到尾捋一遍。三、Spring 解决循环依赖的核心三级缓存机制三级缓存是 Spring 循环依赖问题中最核心的考点几乎可以这么说面试官问循环依赖有一半概率最后都会落到三级缓存上。很多同学能背出三个 Map 的名字但一问「为什么要三级、二级行不行、每级缓存到底存什么」就卡壳了。这一节我们尽量把它讲透。3.1 三个缓存分别是什么三级缓存在源码中定义于DefaultSingletonBeanRegistry类。这个类是BeanFactory体系里负责单例 Bean 注册和获取的基类。三个缓存的实际声明如下javapublic class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { /** 一级缓存存放已经完全初始化好的单例 Bean即最终可用的 Bean */ private final MapString, Object singletonObjects new ConcurrentHashMap(256); /** 二级缓存存放早期暴露的 Bean可能尚未完成初始化通常已经被提前引用 */ private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); /** 三级缓存存放 Bean 的 ObjectFactory也就是可以生产早期 Bean 引用的工厂 */ private final MapString, ObjectFactory? singletonFactories new HashMap(16); }为了方便记忆可以把它们简化成下表名称源码变量存放内容含义一级缓存singletonObjects完整可用的单例 Bean容器对外提供的最终对象二级缓存earlySingletonObjects早期 Bean 引用已经实例化但尚未完全初始化的对象三级缓存singletonFactoriesObjectFactory 工厂用来生成早期 Bean 引用的工厂必要时可以产生代理对象这里有一个最容易搞混的点三级缓存里存的是 ObjectFactory不是 Bean 本身。这个工厂负责在真正需要的时候把「早期 Bean 引用」生产出来。它存在的意义和 AOP 代理有非常紧密的关系我们在第七节会详细解释。另外还有一个重要概念early 早期引用。一个 Bean 刚被实例化出来、还没做属性填充和初始化时这个对象就叫做早期引用或者叫半成品 Bean、提前暴露的引用。三级缓存机制的本质就是在 Bean 还没完全变完整之前先把它能代表自己的那段引用保存起来等别人需要时先拿去用。3.2 一个完整流程A 依赖 BB 又依赖 A我们用最经典的 A 和 B 两个单例 Bean通过字段注入互相依赖逐步拆解容器内部发生了什么事。先给出整体流程图接下来用文字把每一步说清楚。第一步创建 A。容器调用getBean(a)先查一级缓存singletonObjects没有。于是开始创建 A调用 A 的无参构造器完成实例化。此时 A 还是半成品但引用已经真实存在于内存中。第二步提前暴露 A。实例化完成后进入populateBean填充属性之前Spring 判断当前 Bean 是单例、允许循环引用、且正在创建流程中于是调用addSingletonFactory把 A 的ObjectFactory放进三级缓存singletonFactories。这个工厂被调用时会返回 A 的早期引用如果 A 需要 AOP 代理工厂内部还会在这个时机提前生成代理对象。第三步填充 A 的属性。容器开始给 A 注入依赖发现A.b需要 B于是调用getBean(b)。第四步创建 B。同样先查各级缓存没有完整 B于是实例化 B。B 也走同样的流程实例化后被提前暴露到三级缓存singletonFactories中。第五步填充 B 的属性。容器给 B 注入依赖发现B.a需要 A。此时调用getSingleton(a)一级缓存里没有完整 A但是发现自己正在创建 A于是去三级缓存里找。找到了 A 的ObjectFactory调用getObject()拿到 A 的早期引用。第六步缓存升级。拿到 A 的早期引用后Spring 会把它放进二级缓存earlySingletonObjects同时从三级缓存singletonFactories中移除 A 对应的ObjectFactory。然后容器把这份 A 的早期引用注入到 B 的 a 属性中。到这里B 的属性填充工作完成了。第七步B 完成初始化并进入一级缓存。B 的属性填充完成后会继续执行 BeanPostProcessor 后置处理、可能的 AOP 代理、init-method 等初始化流程。当 B 完全初始化完成后Spring 把最终可用的 B 放入一级缓存singletonObjects并清理 B 在二级缓存和三级缓存中的早期信息。随后这个完整 B 会被返回给正在等待它的 A 属性填充过程。第八步A 拿到完整 B 后完成创建。回到 A 的 populateBean 流程A.b 被注入完整 B。A 继续完成属性填充、初始化和可能的 AOP 代理最后把完整 A 放进一级缓存singletonObjects。此时 A 和 B 都成为最终可用的单例 Bean容器启动完成。整个过程可以这样理解三级缓存负责暂存工厂二级缓存负责接收提前暴露的早期引用一级缓存负责保存最终成品。循环依赖中被提前注入的一方拿到的其实是对方的早期引用。因为 Java 对象是引用传递这份早期引用指向的对象最终会被填充完整引用地址不会改变所以后续调用仍然安全。3.3 getSingleton 是如何按顺序走缓存的很多面试官会进一步追问三个缓存到底是怎么被查询的核心逻辑在DefaultSingletonBeanRegistry#getSingleton中。简化后的源码如下javaprotected Object getSingleton(String beanName, boolean allowEarlyReference) { // 1. 先查一级缓存完整可用的单例 Bean Object singletonObject this.singletonObjects.get(beanName); // 2. 一级缓存没有但该 Bean 正在创建中考虑提前引用 if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { singletonObject this.earlySingletonObjects.get(beanName); // 3. 二级缓存也没有并且允许提前引用才查三级缓存 if (singletonObject null allowEarlyReference) { synchronized (this.singletonObjects) { singletonObject this.singletonObjects.get(beanName); if (singletonObject null) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null) { ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }这段代码揭示了三件事。查询顺序是一级、二级、三级。一级缓存中已经是完整 Bean直接返回查不到才判断是否正在创建并尝试获取早期引用。三级缓存不会反复被调用。一旦从三级缓存取出 ObjectFactory 并生成早期引用就会立即把结果放入二级缓存同时从三级缓存中移除。下次再来查询时直接从二级缓存拿到同一份早期引用保证引用一致性。加锁边界很关键。Spring 对整个查询过程加了同步锁避免并发下同一个 Bean 的早期引用被重复创建。理解了这段代码就理解了三级缓存的“流动方向”singletonFactories负责生产早期引用earlySingletonObjects负责承接早期引用singletonObjects负责保存最终成品。四、循环依赖关键源码解析第三大节把理论讲清楚了这一节我们从三个关键方法切入把源码中真正起作用的逻辑串起来提前暴露入口、暴露条件判断以及 Prototype 循环依赖的主动拦截。4.1 addSingletonFactory提前暴露的入口Spring 何时把一个 Bean 放进三级缓存答案在DefaultSingletonBeanRegistry#addSingletonFactory。它的简化源码如下javaprotected void addSingletonFactory(String beanName, ObjectFactory? singletonFactory) { Assert.notNull(singletonFactory, Singleton factory must not be null); synchronized (this.singletonObjects) { if (!this.singletonObjects.containsKey(beanName)) { this.singletonFactories.put(beanName, singletonFactory); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } } }注意两点放入的是 ObjectFactory而不是 Bean 本身。这说明 Spring 不急着生成早期对象而是把它包装成工厂等真正被循环依赖需要时再调用。只在一级缓存没有完整 Bean 时才暴露。如果 Bean 已经完整可用就无需再放工厂避免覆盖成品。4.2 doCreateBean何时才提前暴露addSingletonFactory是被doCreateBean调用的。但并不是每个 Bean 都会被提前暴露它有严格的条件判断javaboolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }这里出现了三个条件mbd.isSingleton()必须是单例 Bean。Prototype 作用域的 Bean 不会走提前暴露因为容器不缓存它。this.allowCircularReferences全局开关默认开启。Spring 抽象了循环引用开关方便在某些场景下关闭。isSingletonCurrentlyInCreation(beanName)该 Bean 必须已经开始创建但尚未完成。只有走在创建流程中的单例 Bean才可能有循环依赖需要打破。这三项全部成立Spring 才会把 Bean 的工厂放进三级缓存。这也回应了为什么构造器注入循环依赖无法借助三级缓存构造器阶段还没走到doCreateBean中提前暴露的时机也没有已经实例化出来的 Bean 对象工厂根本无从谈起。4.3 Prototype 循环依赖的主动拦截在第二节的「方式三」中我们说过Prototype 作用域循环依赖会抛异常。源码层面Spring 在AbstractBeanFactory#doGetBean中主动做了检查javaif (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); }简单说当一个 Prototype Bean 已经被创建、但还处于创建流程中再次请求同一个 Prototype Bean 时Spring 判定发生了循环依赖直接抛出BeanCurrentlyInCreationException。这和单例 Bean 的处理策略完全相反单例会尝试用缓存打破循环Prototype 则直接拒绝创建。五、为什么必须是三级缓存二级行不行这是循环依赖问答中最容易翻车的问题。很多同学会脱口而出“三级缓存是三个 Map一级放成品、二级放半成品、三级放工厂”但被追问“二级缓存已经放半成品了为什么还需要三级”时答不出来。先给结论如果完全没有 AOP 代理把 Bean 的早期引用直接放进二级缓存确实也能打破普通循环依赖但一旦涉及 AOP 代理只有二级缓存就会出问题三级缓存里的 ObjectFactory 正是为了解决“代理时机”和“代理一致性”问题。具体原因有三点。提前暴露的不一定是原始对象可能是代理对象。Spring 的 AOP 增强通常发生在初始化阶段的 BeanPostProcessor 后置处理中。如果 A 依赖 B、B 依赖 A并且 A 需要被 AOP 代理那么 B 在填充属性时拿到的 A必须是一个代理对象否则 B 里注入的 A 和外部最终获得的 A 不是同一个对象。ObjectFactory 提供了延迟决策能力。三级缓存放的是工厂而不是直接 new 出早期对象。只有真正发生循环依赖、确实有人需要提前引用时才会调用工厂触发getEarlyBeanReference逻辑。这样可以避免没有循环依赖时也提前生成本不必要创建的代理对象。保证引用一致性。工厂首次调用生成早期引用后会放入二级缓存并从三级缓存移除。后续无论谁再获取拿到的都是同一份早期引用。这个对象最终被填充完整后地址不变避免出现“两个 A”的错乱。如果只用二级缓存Spring 就必须在实例化后立刻决定是否生成代理。此时 Bean 的初始化流程还没走完Spring 无法确定该 Bean 最终是否需要代理、需要什么代理因此不得不“一刀切”处理既可能提前生成不必要的代理也可能导致代理对象不一致。所以三级缓存设计本质上是“把提前暴露的代价延迟到真正需要时再支付”。六、循环依赖的规避与实战建议虽然 Spring 能在单例 Setter 和字段注入场景下解决循环依赖但工程上仍然不建议放任循环依赖随意存在。循环依赖通常意味着对象职责划分不清、模块边界模糊后续重构和测试会越来越困难。下面给出几种常见的规避方案。重构设计打破双向依赖。把互相引用的部分抽到新的 Service、领域服务或事件机制中让依赖关系变成单向。例如订单服务依赖库存服务库存服务不再反向依赖订单服务而是通过回调、事件、数据库查询等方式解耦。使用 Lazy 延迟注入。在构造器注入场景下给依赖加上Lazy让容器先注入一个代理等真正使用时再获取目标 Bean从而绕过构造器阶段的死锁。注入 ApplicationContext 按需获取。通过ApplicationContext.getBean()延迟获取依赖。不过这个方法容易破坏依赖的显式性只适合极少数特殊场景不建议作为常规手段。避免 Prototype 作用域下互相依赖。原型 Bean 本身就不适合被缓存如果业务上必须原型且要互相引用通常说明设计出现了问题应优先重新设计。这里重点看一下Lazy在构造器注入循环依赖中的作用javaimport org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; Component public class A { private final B b; public A(Lazy B b) { this.b b; } } Component public class B { private final A a; public B(Lazy A a) { this.a a; } }加上Lazy后Spring 注入的不再是真实的 B而是一个延迟代理。只有当代码第一次调用 b 的方法时容器才会真正去获取 B 的实例。这样 A 的构造器就能先完成循环链被有效打断。需要提醒的是这种方式只是“绕过”循环并没有真正消除设计上的循环依赖所以仍然建议以重构为主要手段。七、三级缓存与 AOP 代理的关系第一节提到三级缓存和 AOP 代理关系非常紧密这一节把这条链讲完整。Spring 的 AOP 代理通常是在 Bean 初始化阶段的 BeanPostProcessor 后置处理中生成的。但在循环依赖发生时Bean 初始化还没完成就可能被其他 Bean 提前引用了。此时如果直接给一个未代理的原始对象循环依赖解开后外部容器持有的却是代理对象同一个 Bean 就会存在两个不同对象产生严重的一致性问题。解决这个问题的方法就是提前生成早期代理。前面提到的三级缓存工厂最终调用的是getEarlyBeanReference它的简化源码如下javaprotected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject bp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }这段代码的含义是遍历所有SmartInstantiationAwareBeanPostProcessor看它们是否需要把原始 Bean 替换成早期代理。Spring AOP 的AbstractAutoProxyCreator就是其中一个关键实现。如果 Bean 需要 AOP、并且尚未代理完成它会在这个时机提前创建代理对象如果不需要 AOP则原样返回 Bean。也就是说循环依赖 AOP 的完整链条是这样的Bean 实例化后放进三级缓存的 ObjectFactory。发生循环依赖时调用工厂的getObject()进入getEarlyBeanReference。如果 Bean 需要 AOP提前生成代理对象并作为早期引用返回。代理对象放入二级缓存其他 Bean 注入的就是这份代理对象。Bean 初始化完成时如果还没有代理再由后置处理器正常创建代理。正因如此三级缓存中的工厂不是可有可无的它是“在正确时机、以正确方式暴露早期引用”的核心设计。八、面试高频追问与回答要点把前面的内容浓缩成面试场景中可以直接使用的问答考前可以重点过一遍。问题一Spring 能解决哪些循环依赖回答要点能解决的是单例 Bean 通过 Setter 注入或字段注入形成的循环依赖构造器注入的循环依赖无法解决因为对象在实例化阶段就无法创建出来Prototype 作用域循环依赖也无法解决因为原型 Bean 不会被缓存。问题二Spring 靠什么机制解决循环依赖回答要点依靠三级缓存。一级缓存singletonObjects保存完整单例 Bean二级缓存earlySingletonObjects保存提前暴露的早期引用三级缓存singletonFactories保存能生产早期引用的ObjectFactory。核心思想是先实例化、提前暴露引用、再填充属性和初始化。问题三为什么需要三级缓存而不是两级回答要点主要为了处理 AOP 代理和延迟创建。三级缓存放ObjectFactory可以延迟到真正发生循环依赖时才决定是否生成早期代理避免没有循环依赖时提前创建不必要的代理对象同时保证循环依赖双方拿到的是同一个对象引用。问题四AOP 代理会对循环依赖产生什么影响回答要点循环依赖可能导致 Bean 在被初始化完成前就被引用。如果该 Bean 还需要 AOP 代理必须提前生成早期代理否则 B 注入的是原始 A外部拿到的却是代理 A对象不一致。Spring 通过getEarlyBeanReference在需要时提前创建代理对象来解决这个问题。问题五构造器注入为什么无法解决循环依赖回答要点构造器注入发生在实例化阶段。要创建 A 必须先得到 B要创建 B 又必须先得到 A两个对象谁都没有机会先被实例化出来因此连早期引用都无法产生。三级缓存建立在“对象已实例化”的基础之上所以构造器循环依赖无法被缓存机制挽救。问题六为什么仍不建议滥用字段注入回答要点字段注入虽然能借助三级缓存解决循环依赖但依赖被隐藏在字段中类外部难以看清依赖关系无法用 final 保证不可变强耦合 Spring 容器导致单元测试不方便依赖过多时也不容易暴露设计问题。面试时可以强调优先构造器注入真的出现循环依赖时优先重构设计其次再考虑Lazy等手段。九、总结循环依赖是 Spring 面试中绕不开的高频考点核心可以浓缩为以下几点定义与前提循环依赖是两个及以上 Bean 在创建过程中互相依赖。Spring 能解决部分循环依赖根本原因是把 Bean 创建拆成了实例化和初始化两个阶段实例化完成后就能提前暴露“早期引用”。三种方式构造器注入不能解决单例 Setter/字段注入可以通过三级缓存解决Prototype 作用域不能解决。三级缓存一级singletonObjects放成品二级earlySingletonObjects放早期引用三级singletonFactories放ObjectFactory。查询顺序是一级 → 二级 → 三级三级生成早期引用后会升级到二级避免重复创建。为什么必须三级为了处理 AOP 代理的时机和一致性。ObjectFactory延迟生产早期引用需要时才提前创建代理避免没有循环依赖时也提前生成代理。工程建议能重构就重构打破双向依赖构造器循环依赖可以用Lazy绕过但不要依赖字段注入来掩盖设计问题。