简介:Spring 通常能解决部分循环依赖,但当涉及异步代理、构造器注入或特定生命周期扩展时,缓存机制可能失效并抛出异常。本文从三级缓存的设计目标出发,结合 Bean 创建时序、代理生成时机和常见报错信息,分析循环依赖被“看似解决”与“实际失败”的边界条件,帮助读者建立更准确的判断框架。
在 Spring 应用中,循环依赖并不罕见。例如两个 Service 互相注入:
@Servicepublic class ServiceA {@Autowiredprivate ServiceB serviceB;public void doA() {}}@Servicepublic class ServiceB {@Autowiredprivate ServiceA serviceA;public void doB() {}}
上述代码通常可以正常启动。Spring 容器在创建 ServiceA 时,发现它依赖 ServiceB,于是转而创建 ServiceB;而 ServiceB 又依赖 ServiceA,此时 Spring 会将尚未完全初始化完成的 ServiceA 提前暴露出去,用于填充 ServiceB 的依赖,最终完成两个 Bean 的创建。
但如果给其中一个方法加上异步执行注解:
@Servicepublic class ServiceA {@Autowiredprivate ServiceB serviceB;@Asyncpublic void doA() {}}
再次启动应用,可能会看到类似如下错误:
BeanCurrentlyInCreationException: Error creating bean with name 'serviceA':Bean with name 'serviceA' has been injected into other beans [serviceB]in its raw version as part of a circular reference, but has eventually been wrapped.This means that said other beans do not use the final version of the bean.
同样的循环依赖结构,为什么加了一个注解就失败?这背后并不是 Spring 循环依赖支持时好时坏,而是代理对象生成时机发生了变化。
Spring 并非无条件支持所有循环依赖。它能够处理的主要场景是:
换句话说,Spring 解决的循环依赖本质上是一种“提前暴露半成品引用”的策略。这个半成品随后被替换为最终对象,依赖方能否拿到最终对象,取决于代理或增强处理发生在哪个阶段。
对于纯粹的属性注入,并且没有额外代理逻辑时,提前暴露的原始 Bean 引用与最终 Bean 引用通常是同一个对象,因此不会出现问题。
Spring 在 DefaultSingletonBeanRegistry 中维护了三个关键的 Map,通常称为三级缓存:
ObjectFactory,用于在需要时生成早期引用。核心流程可以概括为:
ObjectFactory 放入三级缓存;可以看出,三级缓存的主要作用是允许在 Bean 完全初始化前暴露一个可用的引用,同时保留后续生成代理或执行后置处理的可能。
以最初的 ServiceA 与 ServiceB 为例,创建过程大致如下:
ServiceA 实例;ServiceA 的 ObjectFactory 放入三级缓存;ServiceA 的属性,发现需要 ServiceB;ServiceB 实例;ServiceB 的 ObjectFactory 放入三级缓存;ServiceB 的属性,发现需要 ServiceA;ServiceA 的早期引用;ServiceB 完成初始化;ServiceA 获得 ServiceB 完成初始化。在这个流程中,ServiceA 始终是同一个 Java 对象。即使后续执行了一些后置处理器,但没有改变 Bean 的对象身份,因此 ServiceB 中注入的引用与最终暴露给外部的 ServiceA 是同一个实例。
当 ServiceA 的方法被标注为异步执行时,Spring 通常需要为该 Bean 生成一个代理对象。也就是说,容器中最终暴露的 ServiceA Bean 不是原始的 ServiceA 实例,而是经过包装的代理类。
此时创建流程变为:
ServiceA 原始实例;ServiceA 的 ObjectFactory 放入三级缓存;ServiceA 属性,触发创建 ServiceB;ServiceB 依赖 ServiceA,从三级缓存获取到原始 ServiceA 引用;ServiceB 完成初始化,它持有的是原始 ServiceA;ServiceA 属性填充完成;ServiceA 代理对象。此时容器中存在两个“版本”的 ServiceA:代理版本和原始版本。ServiceB 注入的是原始版本,而外部从容器中获取的是代理版本。Spring 检测到这种不一致后,选择抛出异常而不是静默允许。因为在异步、事务、缓存等场景中,如果依赖方拿到了未增强的原始对象,就可能导致功能失效。
报错中的关键句是:
has been injected into other beans in its raw version as part of a circular reference,but has eventually been wrapped.
它表达的核心含义是:
这其实是 Spring 的一种保护机制。它并不试图在运行时猜测这种不一致是否可接受,而是直接抛出异常,提示开发者调整结构。
除了代理问题,构造器注入也会导致循环依赖无法解决。例如:
@Servicepublic class ServiceC {private final ServiceD serviceD;public ServiceC(ServiceD serviceD) {this.serviceD = serviceD;}}@Servicepublic class ServiceD {private final ServiceC serviceC;public ServiceD(ServiceC serviceC) {this.serviceC = serviceC;}}
这种情况下,无论是否使用三级缓存,都无法提前拿到 Bean 引用。因为构造器是对象创建的第一道关口,必须完成依赖注入后才能进行实例化。Spring 无法在构造器调用前暴露引用,因此会直接抛出 BeanCurrentlyInCreationException。
这类循环依赖通常说明对象设计本身存在问题,应当优先考虑重新划分职责,而不是依赖框架的容错能力。
可以从以下几个维度判断:
如果前两项满足,且 Bean 不需要被包装,通常可以解决。如果涉及异步、事务、缓存、监听等需要生成代理的场景,就需要关注代理生成时机。一旦早期引用与最终 Bean 不是同一对象,就可能触发异常。
针对上述问题,常见处理方式包括:
其中,打破循环依赖是长期最稳定的方案。框架层面的缓存机制只能解决结构简单、生命周期明确的场景,不应作为设计的首要依赖。
Spring 的循环依赖支持是有边界的。三级缓存解决的是“实例创建与属性填充”之间的时间差,而无法消除对象身份变化所带来的不一致。当 Bean 需要被代理、包装或增强时,原始引用与最终引用可能不再是同一个对象,此时 Spring 选择抛出异常以避免静默错误。
理解这一机制后,再去看“为什么加一个异步注解就启动失败”这类问题,就会发现原因并不神秘:不是循环依赖从可解变为不可解,而是代理对象的生成使“提前暴露的引用”与“最终 Bean”发生了分离。开发者在遇到类似问题时,应当优先检查 Bean 的代理需求、注入方式和依赖方向,而不是简单归因于框架能力不足。