简介:本文从一个实际项目中的报错案例出发,深入剖析Spring容器解决循环依赖的核心机制——三级缓存与早期引用暴露。文章不仅解释了常规构造器与Setter注入下循环依赖被解决的原理,更重点分析了当循环依赖与@Async注解结合时为何会抛出BeanCurrentlyInCreationException异常。通过阅读本文,读者将透彻理解Spring Bean生命周期、代理创建时机以及循环依赖处理的边界条件,掌握此类问题的排查思路与解决方案。
在日常开发中,Spring框架凭借其强大的依赖注入能力,极大地简化了对象之间的装配工作。然而,当业务逻辑变得复杂时,我们偶尔会遇到一些看似矛盾的现象:Spring官方宣称其能够处理循环依赖问题,但某些特定场景下,启动容器却会抛出异常。
先看一段典型的业务代码:
@Servicepublic class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {// 异步业务逻辑}}@Servicepublic class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {// 同步业务逻辑}}
在这段代码中,TestService1注入了TestService2,而TestService2又反向注入了TestService1,二者构成了典型的循环依赖关系。唯一显得有些特殊的地方在于,TestService1的test1()方法上标注了@Async注解,表明该方法需要异步执行。
当我们尝试启动项目时,控制台却无情的抛出了如下异常:
org.springframework.beans.factory.BeanCurrentlyInCreationException:Error creating bean with name 'testService1':Bean with name 'testService1' has been injected into other beans [testService2]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.
这个异常信息告诉我们:testService1在创建过程中,其原始实例(raw version)被注入到了testService2中作为循环引用的一部分,但最终该Bean却被包装(wrapped)了。这意味着其他Bean没有使用到Bean的最终版本。
这个报错令人困惑——因为在常规理解中,Spring是可以解决循环依赖的。让我们做一个对照实验:将@Async注解从test1()方法上移除,保持循环依赖关系不变,再次启动项目,一切正常,容器能够顺利启动。
这究竟是为什么?同样是循环依赖,为什么添加了@Async注解就出现问题?要解答这个疑惑,我们必须从Spring解决循环依赖的底层机制说起。
在Spring中,解决循环依赖的能力与Bean的注入方式密切相关。常见的依赖注入方式分为构造器注入和Setter/字段注入两种。
如果我们使用的全部是构造器注入:
@Servicepublic class ServiceA {private final ServiceB serviceB;@Autowiredpublic ServiceA(ServiceB serviceB) {this.serviceB = serviceB;}}
在这种情况下,Spring无法解决循环依赖。原因在于,构造器注入要求在Bean实例化(即调用构造方法)的瞬间就获取到依赖的完整对象,而构建一个完整对象的前提是它的依赖也已经被完整构建。这就形成了一个无解的死锁。
而如果我们使用的是Setter注入或基于@Autowired字段注入(如开头案例所示),Spring则可以通过提前暴露早期引用的方式来打破僵局。
Spring容器内部维护了一个三层缓存结构,用来在Bean的生命周期不同阶段暂存对象。这三层缓存分别是:
ObjectFactory对象,其getObject()方法负责生成早期引用。整个流程的核心思路如下:
ObjectFactory,放入三级缓存中。这一步的目的是为了“暴露”该Bean的早期引用,以便其他依赖它的Bean能够在它彻底完成初始化之前就拿到它的引用。populateBean方法)。在此过程中,如果发现需要注入其他Bean,而该Bean又依赖当前Bean,那么对方就可以通过三级缓存中的ObjectFactory获取到当前Bean的早期引用。基于上述三级缓存机制,我们可以梳理一下常规Setter注入循环依赖的完整解决过程。假设存在ServiceA和ServiceB互相依赖:
ServiceA。ServiceA被实例化为原始对象a。a封装为ObjectFactory,放入三级缓存。ServiceA进行属性填充,发现需要注入ServiceB。ServiceB,发现其尚未创建,于是开始创建ServiceB。ServiceB被实例化为原始对象b,同样将其固化到三级缓存。ServiceB进行属性填充,发现需要注入ServiceA。ServiceA——此时在一级缓存中找不到,但可以从三级缓存中拿到ServiceA的ObjectFactory,调用其getObject()得到ServiceA的早期引用a。a注入到ServiceB中,并继续完成ServiceB的后续初始化流程,最后将成品ServiceB放入一级缓存。ServiceA的创建流程,将从一级缓存中取到的ServiceB注入其中。ServiceA的整个初始化,将成品ServiceA放入一级缓存。整个过程环环相扣,保证了容器能够顺利启动。
要理解@Async导致的异常,我们必须先认识到Spring AOP(面向切面编程)在容器创建过程中的作用。当某个类上或某个方法上标注了@Async注解时,Spring会认为这个Bean需要被增强,以便在调用该方法时能异步执行。这种增强是通过代理(Proxy)来实现的。
Spring在Bean的生命周期中存在一个关键阶段,即initializeBean方法中包含了applyBeanPostProcessorsAfterInitialization步骤。在这个步骤中,各个BeanPostProcessor会对初始化的Bean进行后置处理。AsyncAnnotationBeanPostProcessor会检查Bean是否需要应用异步代理,如果需要,它会返回一个代理对象来替代原始对象。
这里的关键点在于:代理对象的生成时机通常是在Bean完成属性填充之后、初始化完成之后的阶段,而不是在实例化完成之后就立刻生成。
现在,我们将三级缓存机制与@Async代理结合起来,重新审视开头那个失败案例。
TestService1开始创建。实例化得到原始对象rawA。rawA被包装为ObjectFactory放入三级缓存。注意:此时放入的ObjectFactory默认返回的就是原始对象rawA。TestService1进行属性填充,发现需要注入TestService2,于是开始创建TestService2。TestService2实例化得到rawB,同样将其工厂放入三级缓存。TestService2进行属性填充,发现需要注入TestService1。TestService2尝试获取TestService1。从三级缓存中取得ObjectFactory,调用其getObject()方法,得到的仍然是原始对象rawA,而非代理对象。rawA注入到TestService2中,继续完成TestService2的后续创建流程。TestService1的创建流程,完成其属性填充。到了initializeBean阶段,AsyncAnnotationBeanPostProcessor发现TestService1需要异步增强,于是生成了代理对象proxyA,并将该代理对象作为最终的Bean返回。proxyA放入一级缓存,但此时TestService2内部持有的仍然是原始对象rawA。此时,容器发现了一个严重的状态:TestService2中注入的TestService1并不是最终的代理版本proxyA。这意味着在后续运行时,testService2调用testService1的方法时,将无法触发异步逻辑,因为原始对象不具备异步能力。这种不一致会破坏整个容器的核心语义。
于是,Spring的安全机制被触发——它抛出BeanCurrentlyInCreationException异常,明确告知开发者:testService1已被作为原始版本注入到其他Bean中,最终却被包装了,其他Bean无法使用到Bean的最终版本。Spring宁可拒绝提供服务,也不允许产生这种状态不一致的隐患。
当我们将@Async注解去掉之后,TestService1在整个创建过程中不需要生成任何代理对象。它从三级缓存中暴露出来的ObjectFactory.getObject()返回的就是原始对象,并且这个原始对象就是最终要放入一级缓存的回结果。
因此,TestService2拿到的早期引用和最终的成品是同一个对象,状态完全一致,不存在代理替换的问题。三级缓存机制完美闭环,Spring当然可以安全地解决这个循环依赖。
但如果被循环依赖的Bean恰好涉及AOP(如@Async、@Transactional等需要在初始化后生成代理的切面),我们就必须考虑代理生成时机与循环依赖如何处理之间的冲突了。
一个值得思考的问题是:为什么Spring不直接在三级缓存的ObjectFactory中返回代理对象,从而避免后续的“包装不一致”问题?
答案是:代理的生成需要依赖于Bean的完整状态,而这个完整状态只有在Bean完成属性填充后才能获得。如果在实例化之后就立刻生成代理,此时Bean的成员变量都还没有注入,生成的代理也是无效的,因为它无法正确委托给一个半成品的目标对象。
另外,并非所有循环依赖中的Bean都需要代理。三级缓存的精妙之处在于延迟了“是否产生代理”的决策。Spring使用ObjectFactory而不是直接返回早期实例,就是为了留出余地:如果后续发现这个Bean需要增强,并且该增强必须在循环依赖处理之前完成,则可以通过自定义的SmartInstantiationAwareBeanPostProcessor来干预getEarlyBeanReference方法,提前生成代理。
但@Async的实现机制并未参与到早期引用的生成逻辑中,其代理生成被延后到了初始化阶段,因此就导致了上述的问题。
当一个循环依赖相关的报错信息包含以下关键词时:
BeanCurrentlyInCreationExceptionhas been injected into other beans in its raw versionbut has eventually been wrappeddoes not use the final version of the bean我们应当立即怀疑,循环依赖链中的某个Bean可能涉及代理增强(如@Async、@Transactional、自定义AOP拦截等)。
针对此类问题,业内通常有以下几种处理策略:
打破循环依赖:这是最根本的解决方案。重新梳理业务依赖关系,通过引入中间层(如一个独立的Service或事件发布机制)解除两个Bean之间的双向依赖。
使用@Lazy注解:在注入点添加@Lazy注解。例如在TestService1中注入TestService2时使用@Lazy,这会让Spring生成一个代理占位符,推迟对TestService2的实例化直至真正调用时,从而打破启动时的循环依赖链。
将@Async方法迁移到独立的Bean中:将被@Async修饰的方法抽取到一个专门的异步处理Service中,让循环依赖的两个Bean只依赖这个异步Service,而不是直接互相依赖。
调整代理创建时机:如果无法从架构上规避,可以考虑实现SmartInstantiationAwareBeanPostProcessor,重写getEarlyBeanReference方法,在早期引用阶段就返回代理对象。但这种方式复杂度高,且需要深入理解Spring内部机制,一般不建议在业务代码中贸然使用。
使用ObjectProvider或ApplicationContext进行延迟获取:不在字段注入时直接获取依赖对象,而是在实际需要时再从容器中按需获取,从而避免初始化阶段的强循环依赖。
Spring的循环依赖解决机制是三级缓存设计与Bean生命周期精细编排的典范。理解这一机制,不仅有助于解决启动过程中出现的疑难杂症,更能帮助我们深入把握Spring的设计哲学。
需要特别强调的是,Spring自始至终都没有承诺解决“所有”循环依赖问题。构造器循环依赖、涉及代理增强的字段循环依赖等场景,都是Spring故意暴露限制的地方。对于开发者而言,最重要的是要认识到:循环依赖本身就是一种设计上的坏味道,正确做法是从根源上减少和避免这类依赖关系,才能让系统架构更加清晰、可维护。
最后,当我们在开发过程中遇到框架行为与直觉不符的情况时,不妨深入源码层面,探究其运行机制背后的权衡取舍。这种探究不仅能解答眼前的问题,更能拓宽技术视野,帮助我们成为一名真正的资深开发者。