Spring循环依赖与代理增强的冲突解析
作者:da吃一鲸8862026.11.07 02:31浏览量:0简介:本文从一个由@Async引发的启动异常入手,深入拆解Spring解决循环依赖的核心机制、三级缓存设计,以及当循环依赖遇到AOP代理时为何会失效。读者将理解Bean提前暴露的本质、代理对象替换的时机,并掌握避免此类问题的实用方案。
一、从一个启动报错说起
在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 aspart of a circular reference, but has eventually been wrapped. This meansthat said other beans do not use the final version of the bean.
如果将 @Async 注解移除,启动恢复正常。这说明问题并不只在于循环依赖本身,而在于循环依赖与代理增强的叠加效应。要理解这一现象,需要先回到Spring处理循环依赖的基本原理。
二、Spring解决循环依赖的核心机制
Spring容器在创建单例Bean时,大致经历三个关键阶段:实例化、属性填充、初始化。实例化只是创建对象外壳,属性填充负责注入依赖,初始化则完成各种生命周期回调与代理增强。
循环依赖之所以能被解决,是因为Spring允许在Bean尚未完成全部创建流程时,将其“半成品”引用提前暴露给其他Bean使用。这一能力依赖三级缓存结构:
| 缓存级别 | 缓存名称 | 存放内容 |
|---|---|---|
| 一级缓存 | singletonObjects | 完全创建完成的Bean实例 |
| 二级缓存 | earlySingletonObjects | 提前暴露的原始实例或代理实例 |
| 三级缓存 | singletonFactories | 对象工厂,用于延迟生成提前暴露对象 |
处理流程的核心逻辑如下:
- 创建 TestService1 时,先实例化得到原始对象 A,随后将 A 对应的 ObjectFactory 放入三级缓存。
- 填充 TestService1 的属性时,发现需要 TestService2,触发 TestService2 的创建。
- 创建 TestService2 时,同样先实例化得到原始对象 B,然后填充属性时发现需要 TestService1。
- 容器从三级缓存中取出 TestService1 的 ObjectFactory,调用其 getObject 方法获取可提前暴露的对象。
- 如果 TestService1 不需要AOP代理,此时直接返回原始对象 A,并将其放入二级缓存。
- TestService2 完成属性注入后继续初始化,最终成为完整Bean,进入一级缓存。
- 回到 TestService1 的创建流程,将 TestService2 注入,完成后续初始化,进入一级缓存。
这样一来,TestService1 持有的是完整的 TestService2,而 TestService2 持有的可能是一个提前暴露的 TestService1 引用。如果 TestService1 最终不需要被代理,那么提前暴露的引用与最终实例是同一个对象,一切正常。
三、AOP代理如何打破这一平衡
当 TestService1 的 test1 方法被 @Async 修饰时,Spring会为该Bean创建代理对象。代理对象通常通过后置处理器在初始化阶段生成。这意味着时序发生了变化:
- TestService1 实例化后,三级缓存中存放的是针对原始对象的 ObjectFactory。
- 当 TestService2 需要注入 TestService1 时,容器调用 ObjectFactory 的 getObject 方法。此时 TestService1 尚未执行初始化,后置处理器尚未生成代理,因此返回的是原始对象。
- 容器将原始对象注入到 TestService2 中,并将该原始对象放入二级缓存。
- TestService2 完成创建。
- 回到 TestService1 的初始化阶段,@Async 对应的后置处理器开始工作,创建了一个全新的代理对象。
- TestService1 最终进入一级缓存的是代理对象,而 TestService2 持有的却是原始对象。
关键矛盾点:TestService2 中注入的 TestService1 与容器最终暴露的 TestService1 不是同一个对象。Spring在启动检查时发现这一不一致,立即抛出 BeanCurrentlyInCreationException,防止应用在运行期出现更隐蔽的行为差异。
四、为何普通循环依赖不受影响
当没有 @Async 或其他代理增强时,TestService1 从三级缓存中提前暴露的是原始对象,最终放入一级缓存的也是同一个原始对象。引用完全一致,不存在替换问题,因此循环依赖可以被正常解决。
这解释了为什么有时两个Service互相注入可以正常工作,但在其中一个Service上添加了事务、异步、缓存等注解后却突然报错。事务注解 @Transactional 与异步注解 @Async 一样,通常会触发AOP代理。
值得注意的是,并非所有代理都会导致问题。如果代理对象的生成时机发生在提前暴露阶段(即三级缓存的 ObjectFactory 中已经具备创建代理的能力),那么二级缓存中存入的就是代理对象,后续一致性可以保证。Spring在高版本中针对部分场景做了优化,但 @Async 的特殊性在于其代理创建逻辑与常规事务代理存在差异,仍然可能触发原始对象提前暴露的问题。
五、常见解决方案与工程实践
针对循环依赖与代理增强的冲突问题,有以下几种经过验证的解决思路。
方案一:打破循环依赖
最根本的解决方式是重新审视类之间的关系,消除不必要的循环依赖。常见手法包括:
- 将其中一个类依赖的公共逻辑抽取到第三个类中,两个类都依赖新类。
- 使用事件机制解耦,一个服务发布事件,另一个服务监听事件。
- 通过方法参数传递依赖,而非在字段层面互相注入。
以事件机制为例:
@Servicepublic class TestService1 {private final ApplicationEventPublisher publisher;public TestService1(ApplicationEventPublisher publisher) {this.publisher = publisher;}public void test1() {publisher.publishEvent(new SomeEvent(this));}}@Servicepublic class TestService2 {@EventListenerpublic void onSomeEvent(SomeEvent event) {// 处理事件}}
这种方式彻底消除了两个Bean之间的直接依赖关系,从根源上避免了循环引用问题。
方案二:延迟注入
在其中一个注入点上使用 @Lazy 注解,通知Spring在首次实际使用时才创建代理引用。这样在Bean创建过程中,容器注入的是一个惰性代理对象,不会立即触发目标Bean的真实创建,从而打破创建链上的循环等待。
@Servicepublic class TestService1 {@Autowired@Lazyprivate TestService2 testService2;@Asyncpublic void test1() {}}
这种方式改动最小,适合循环依赖关系本身比较合理、只是创建时序存在冲突的场景。
方案三:手动指定依赖加载时机
通过 Setter 注入代替构造器或字段注入,并不能从根本上解决代理对象不一致的问题。更可靠的做法是在配置类中使用 ObjectProvider 或 Provider 延迟获取Bean:
@Servicepublic class TestService1 {private final ObjectProvider<TestService2> provider;public TestService1(ObjectProvider<TestService2> provider) {this.provider = provider;}@Asyncpublic void test1() {TestService2 service2 = provider.getIfAvailable();}}
方案四:调整代理创建策略
如果必须保留 @Async,可以尝试将 @Async 方法移动到独立的第三类中,使需要被代理的Bean不再与其他Bean形成循环依赖。例如:
@Servicepublic class TestService1 {@Autowiredprivate TestService2 testService2;@Autowiredprivate AsyncExecutor asyncExecutor;public void test1() {asyncExecutor.doAsync();}}@Servicepublic class AsyncExecutor {@Asyncpublic void doAsync() {}}
六、源码层面的关键节点解析
为了更清晰地理解问题,可以对照Spring源码中的几个关键位置。
在 AbstractAutowireCapableBeanFactory 的 doCreateBean 方法中,实例化完成后会调用 addSingletonFactory,将当前Bean的 ObjectFactory 放入三级缓存。该工厂的 getObject 方法内部会调用 getEarlyBeanReference:
protected 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;}
这里的后置处理器可以对提前暴露的Bean进行加工。但 @Async 的代理生成逻辑并不在 getEarlyBeanReference 阶段完成,而是等到初始化阶段的 applyBeanPostProcessorsAfterInitialization 中才执行。这导致提前暴露时拿到的是原始对象,而最终Bean是代理对象。
随后在 doCreateBean 末尾有一段校验逻辑:
if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {// 检查依赖该Bean的其他Bean是否持有最终版本}}}
当容器发现存在依赖Bean且当前Bean已经发生了包装替换时,就会抛出文章开头看到的异常。
七、总结与建议
Spring的循环依赖解决方案并非万能,它建立在“提前暴露的引用与最终Bean引用一致”这一前提之上。当AOP代理、特殊生命周期回调或其他后置处理改变了最终对象时,提前暴露机制就可能失效。
以下几点建议可供工程实践参考:
- 优先从设计层面消除循环依赖。循环依赖往往是模块边界划分不清的信号。
- 关注Bean的增强需求。使用 @Async、@Transactional 等注解时,对其可能引发的代理行为保持敏感。
- 利用 @Lazy 或 Provider 作为轻量级缓解手段,但要理解其背后的延迟语义,避免将问题掩盖到运行期才暴露。
- 注意Spring版本差异。不同版本对提前暴露与代理生成的处理策略可能不同,升级版本后应重新验证相关行为。
理解容器内部的时序与缓存机制,不仅能帮助排障,也能在设计阶段做出更稳健的决策。

登录后可评论,请前往 登录 或 注册