简介:本文从一次真实的循环依赖异常出发,系统剖析Spring解决循环依赖的底层机制,重点解释为何普通循环依赖可以正常注入,而叠加@Async等代理增强后却抛出BeanCurrentlyInCreationException。读者将掌握三级缓存的工作原理、提前暴露原始对象与最终代理对象之间的矛盾,以及AOP场景下循环依赖失效的根本原因与规避策略。
在一次常规的代码审查中,团队遇到了一种颇为典型却又容易被忽视的场景:两个服务类互相注入对方实例,形成闭环依赖。简化后的代码如下:
@Servicepublic class OrderService {@Autowiredprivate PayService payService;@Asyncpublic void submitOrder() {// 异步处理下单逻辑}}@Servicepublic class PayService {@Autowiredprivate OrderService orderService;public void pay() {// 支付逻辑}}
启动时抛出异常:
BeanCurrentlyInCreationException: Error creating bean with name 'orderService':Bean with name 'orderService' has been injected into other beans [payService]in its raw version as part of a circular reference, but has eventually beenwrapped. This means that said other beans do not use the final version of the bean.
将 @Async 注解去掉后,同样的循环依赖结构却能正常启动。这一差异引出了本文要讨论的核心问题:Spring 号称能够解决循环依赖,为什么加了代理增强之后就不行了?
循环依赖指的是一个或多个 Bean 之间形成直接或间接的引用闭环。例如 A 依赖 B,B 又依赖 A;或者 A→B→C→A 这样的长链路闭环。从依赖注入的时机来看,可以分为三类:
本文讨论的 @Autowired 字段注入属于第二类,也是 Spring 能够处理的那一类。
Spring 解决单例循环依赖的关键,是 IoC 容器中的三级缓存结构:
| 缓存层级 | 名称 | 存储内容 |
|---|---|---|
| 一级 | singletonObjects | 完全初始化好的成品 Bean |
| 二级 | earlySingletonObjects | 提前暴露的半成品 Bean(尚未填充属性、未执行初始化方法) |
| 三级 | singletonFactories | 生成早期 Bean 引用的对象工厂(ObjectFactory) |
创建 Bean 时的核心流程如下:
instantiate):通过反射创建对象实例,此时所有字段都是默认值。populateBean):注入依赖的 Bean。initializeBean):执行 BeanPostProcessor、@PostConstruct、InitializingBean 等方法,并在此阶段生成 AOP 代理。当 A 创建到第 2 步发现需要 B,而 B 又需要 A 时,Spring 会:
getObject() 获取 A 的早期引用,放入二级缓存,完成 B 的创建;这一机制的关键前提是:B 注入的“A”和最终放入一级缓存的“A”可以是同一个对象,也可以不是同一个对象——只要 B 不关心里面持有的是不是增强后的代理即可。
@Async 的底层实现依赖 Spring 的 AOP 代理机制。当 Bean 被 @Async 标记的方法所在类需要被代理时,Spring 会在 初始化后的 postProcessAfterInitialization 阶段 为其生成代理对象。
结合循环依赖场景,问题就显现了:
AsyncAnnotationBeanPostProcessor 发现 A 需要异步增强,于是生成代理 A,并将其放入一级缓存。Spring 在 DefaultSingletonBeanRegistry 与 AbstractAutowireCapableBeanFactory 的协作中,会对这种“早期引用被注入,但最终被包装成另一个对象”的情况进行检测,一旦发现不一致就抛出 BeanCurrentlyInCreationException,避免应用中出现“部分依赖拿到代理、部分依赖拿到原始对象”的隐性 bug。
如果 A 在放入三级缓存之前就完成了代理包装,B 拿到的就是代理对象,最终一致。但 AOP 通常发生在初始化之后,除非用 @Lazy 或自定义 SmartInstantiationAwareBeanPostProcessor 提前决定代理。这也是 Spring 提供 getEarlyBeanReference 这一扩展点的意义所在:
public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor {default Object getEarlyBeanReference(Object bean, String beanName) {return bean;}}
AbstractAutoProxyCreator 实现了这个方法,理论上可以在此返回代理对象。但注意:这种做法要求 AOP 在早期就具备足够信息(目标类、切点等都已确定),对于某些需要完整初始化后才能判定的增强(比如依赖方法返回值的场景)并不适用。
更稳妥的思路不是让 Spring 在代理和循环依赖之间做取舍,而是从设计上消除闭环:
@Lazy 延迟注入:在注入点加上 @Lazy,Spring 会注入一个代理占位符,直到第一次真正调用时才触发目标 Bean 的创建,从而避免创建期的闭环。ApplicationContext 或 ObjectProvider 延迟获取:避免在字段上直接声明对方类型,改为运行时按需获取。@Async、@Transactional、@Cacheable 等代理增强注解:它们都会改变 Bean 的最终形态,与循环依赖叠加时最容易触发异常。@Lazy 是缓解手段而非根治方案:它把依赖解析推迟到运行时,问题可能被隐藏得更深,最好配合日志和测试明确其行为。BeanCurrentlyInCreationException 的语义:它并非简单的“循环依赖未解决”,而是在提示“早期引用与最终 Bean 不一致”,定位时要优先检查 AOP 相关注解。Spring 解决循环依赖的核心是“提前暴露半成品引用”,但这套机制成立的前提是——被提前暴露的对象与最终放入容器的对象在被依赖方看来必须一致。@Async 等注解会在初始化后生成代理,使得这一前提被打破,Spring 出于安全考虑主动抛出异常。
因此,真正专业的做法不是试图绕开异常,而是把循环依赖本身视为设计信号:能用构造器注入暴露问题就用构造器注入;必须保留循环时,用 @Lazy 或抽取协调者打破闭环;涉及 AOP 增强时,更要从创建时序角度审视依赖关系。理解三级缓存的边界,才能在实际工程中既享受框架带来的便利,又不被它的“智能兜底”所误导。