logo

Spring循环依赖与代理增强的冲突解析

作者:da吃一鲸8862026.11.07 02:31浏览量:0

简介:本文从一个由@Async引发的启动异常入手,深入拆解Spring解决循环依赖的核心机制、三级缓存设计,以及当循环依赖遇到AOP代理时为何会失效。读者将理解Bean提前暴露的本质、代理对象替换的时机,并掌握避免此类问题的实用方案。

一、从一个启动报错说起

在Spring应用开发中,循环依赖并不罕见。假设我们有两个服务类互相注入:

  1. @Service
  2. public class TestService1 {
  3. @Autowired
  4. private TestService2 testService2;
  5. @Async
  6. public void test1() {
  7. }
  8. }
  9. @Service
  10. public class TestService2 {
  11. @Autowired
  12. private TestService1 testService1;
  13. public void test2() {
  14. }
  15. }

TestService1 注入了 TestService2,TestService2 又注入了 TestService1,二者形成了典型的循环依赖。同时,TestService1 的 test1 方法上标注了 @Async,意味着该类最终需要被包装为代理对象。

启动应用后,控制台抛出如下异常:

  1. org.springframework.beans.factory.BeanCurrentlyInCreationException:
  2. Error creating bean with name 'testService1': Bean with name 'testService1'
  3. has been injected into other beans [testService2] in its raw version as
  4. part of a circular reference, but has eventually been wrapped. This means
  5. that said other beans do not use the final version of the bean.

如果将 @Async 注解移除,启动恢复正常。这说明问题并不只在于循环依赖本身,而在于循环依赖与代理增强的叠加效应。要理解这一现象,需要先回到Spring处理循环依赖的基本原理。

二、Spring解决循环依赖的核心机制

Spring容器在创建单例Bean时,大致经历三个关键阶段:实例化、属性填充、初始化。实例化只是创建对象外壳,属性填充负责注入依赖,初始化则完成各种生命周期回调与代理增强。

循环依赖之所以能被解决,是因为Spring允许在Bean尚未完成全部创建流程时,将其“半成品”引用提前暴露给其他Bean使用。这一能力依赖三级缓存结构:

缓存级别 缓存名称 存放内容
一级缓存 singletonObjects 完全创建完成的Bean实例
二级缓存 earlySingletonObjects 提前暴露的原始实例或代理实例
三级缓存 singletonFactories 对象工厂,用于延迟生成提前暴露对象

处理流程的核心逻辑如下:

  1. 创建 TestService1 时,先实例化得到原始对象 A,随后将 A 对应的 ObjectFactory 放入三级缓存。
  2. 填充 TestService1 的属性时,发现需要 TestService2,触发 TestService2 的创建。
  3. 创建 TestService2 时,同样先实例化得到原始对象 B,然后填充属性时发现需要 TestService1。
  4. 容器从三级缓存中取出 TestService1 的 ObjectFactory,调用其 getObject 方法获取可提前暴露的对象。
  5. 如果 TestService1 不需要AOP代理,此时直接返回原始对象 A,并将其放入二级缓存。
  6. TestService2 完成属性注入后继续初始化,最终成为完整Bean,进入一级缓存。
  7. 回到 TestService1 的创建流程,将 TestService2 注入,完成后续初始化,进入一级缓存。

这样一来,TestService1 持有的是完整的 TestService2,而 TestService2 持有的可能是一个提前暴露的 TestService1 引用。如果 TestService1 最终不需要被代理,那么提前暴露的引用与最终实例是同一个对象,一切正常。

三、AOP代理如何打破这一平衡

当 TestService1 的 test1 方法被 @Async 修饰时,Spring会为该Bean创建代理对象。代理对象通常通过后置处理器在初始化阶段生成。这意味着时序发生了变化:

  1. TestService1 实例化后,三级缓存中存放的是针对原始对象的 ObjectFactory。
  2. 当 TestService2 需要注入 TestService1 时,容器调用 ObjectFactory 的 getObject 方法。此时 TestService1 尚未执行初始化,后置处理器尚未生成代理,因此返回的是原始对象。
  3. 容器将原始对象注入到 TestService2 中,并将该原始对象放入二级缓存。
  4. TestService2 完成创建。
  5. 回到 TestService1 的初始化阶段,@Async 对应的后置处理器开始工作,创建了一个全新的代理对象。
  6. TestService1 最终进入一级缓存的是代理对象,而 TestService2 持有的却是原始对象。

关键矛盾点:TestService2 中注入的 TestService1 与容器最终暴露的 TestService1 不是同一个对象。Spring在启动检查时发现这一不一致,立即抛出 BeanCurrentlyInCreationException,防止应用在运行期出现更隐蔽的行为差异。

四、为何普通循环依赖不受影响

当没有 @Async 或其他代理增强时,TestService1 从三级缓存中提前暴露的是原始对象,最终放入一级缓存的也是同一个原始对象。引用完全一致,不存在替换问题,因此循环依赖可以被正常解决。

这解释了为什么有时两个Service互相注入可以正常工作,但在其中一个Service上添加了事务、异步、缓存等注解后却突然报错。事务注解 @Transactional 与异步注解 @Async 一样,通常会触发AOP代理。

值得注意的是,并非所有代理都会导致问题。如果代理对象的生成时机发生在提前暴露阶段(即三级缓存的 ObjectFactory 中已经具备创建代理的能力),那么二级缓存中存入的就是代理对象,后续一致性可以保证。Spring在高版本中针对部分场景做了优化,但 @Async 的特殊性在于其代理创建逻辑与常规事务代理存在差异,仍然可能触发原始对象提前暴露的问题。

五、常见解决方案与工程实践

针对循环依赖与代理增强的冲突问题,有以下几种经过验证的解决思路。

方案一:打破循环依赖

最根本的解决方式是重新审视类之间的关系,消除不必要的循环依赖。常见手法包括:

  • 将其中一个类依赖的公共逻辑抽取到第三个类中,两个类都依赖新类。
  • 使用事件机制解耦,一个服务发布事件,另一个服务监听事件。
  • 通过方法参数传递依赖,而非在字段层面互相注入。

以事件机制为例:

  1. @Service
  2. public class TestService1 {
  3. private final ApplicationEventPublisher publisher;
  4. public TestService1(ApplicationEventPublisher publisher) {
  5. this.publisher = publisher;
  6. }
  7. public void test1() {
  8. publisher.publishEvent(new SomeEvent(this));
  9. }
  10. }
  11. @Service
  12. public class TestService2 {
  13. @EventListener
  14. public void onSomeEvent(SomeEvent event) {
  15. // 处理事件
  16. }
  17. }

这种方式彻底消除了两个Bean之间的直接依赖关系,从根源上避免了循环引用问题。

方案二:延迟注入

在其中一个注入点上使用 @Lazy 注解,通知Spring在首次实际使用时才创建代理引用。这样在Bean创建过程中,容器注入的是一个惰性代理对象,不会立即触发目标Bean的真实创建,从而打破创建链上的循环等待。

  1. @Service
  2. public class TestService1 {
  3. @Autowired
  4. @Lazy
  5. private TestService2 testService2;
  6. @Async
  7. public void test1() {
  8. }
  9. }

这种方式改动最小,适合循环依赖关系本身比较合理、只是创建时序存在冲突的场景。

方案三:手动指定依赖加载时机

通过 Setter 注入代替构造器或字段注入,并不能从根本上解决代理对象不一致的问题。更可靠的做法是在配置类中使用 ObjectProvider 或 Provider 延迟获取Bean:

  1. @Service
  2. public class TestService1 {
  3. private final ObjectProvider<TestService2> provider;
  4. public TestService1(ObjectProvider<TestService2> provider) {
  5. this.provider = provider;
  6. }
  7. @Async
  8. public void test1() {
  9. TestService2 service2 = provider.getIfAvailable();
  10. }
  11. }

方案四:调整代理创建策略

如果必须保留 @Async,可以尝试将 @Async 方法移动到独立的第三类中,使需要被代理的Bean不再与其他Bean形成循环依赖。例如:

  1. @Service
  2. public class TestService1 {
  3. @Autowired
  4. private TestService2 testService2;
  5. @Autowired
  6. private AsyncExecutor asyncExecutor;
  7. public void test1() {
  8. asyncExecutor.doAsync();
  9. }
  10. }
  11. @Service
  12. public class AsyncExecutor {
  13. @Async
  14. public void doAsync() {
  15. }
  16. }

六、源码层面的关键节点解析

为了更清晰地理解问题,可以对照Spring源码中的几个关键位置。

在 AbstractAutowireCapableBeanFactory 的 doCreateBean 方法中,实例化完成后会调用 addSingletonFactory,将当前Bean的 ObjectFactory 放入三级缓存。该工厂的 getObject 方法内部会调用 getEarlyBeanReference:

  1. protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
  2. Object exposedObject = bean;
  3. if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
  4. for (SmartInstantiationAwareBeanPostProcessor bp :
  5. getBeanPostProcessorCache().smartInstantiationAware) {
  6. exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
  7. }
  8. }
  9. return exposedObject;
  10. }

这里的后置处理器可以对提前暴露的Bean进行加工。但 @Async 的代理生成逻辑并不在 getEarlyBeanReference 阶段完成,而是等到初始化阶段的 applyBeanPostProcessorsAfterInitialization 中才执行。这导致提前暴露时拿到的是原始对象,而最终Bean是代理对象。

随后在 doCreateBean 末尾有一段校验逻辑:

  1. if (earlySingletonExposure) {
  2. Object earlySingletonReference = getSingleton(beanName, false);
  3. if (earlySingletonReference != null) {
  4. if (exposedObject == bean) {
  5. exposedObject = earlySingletonReference;
  6. } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
  7. // 检查依赖该Bean的其他Bean是否持有最终版本
  8. }
  9. }
  10. }

当容器发现存在依赖Bean且当前Bean已经发生了包装替换时,就会抛出文章开头看到的异常。

七、总结与建议

Spring的循环依赖解决方案并非万能,它建立在“提前暴露的引用与最终Bean引用一致”这一前提之上。当AOP代理、特殊生命周期回调或其他后置处理改变了最终对象时,提前暴露机制就可能失效。

以下几点建议可供工程实践参考:

  1. 优先从设计层面消除循环依赖。循环依赖往往是模块边界划分不清的信号。
  2. 关注Bean的增强需求。使用 @Async、@Transactional 等注解时,对其可能引发的代理行为保持敏感。
  3. 利用 @Lazy 或 Provider 作为轻量级缓解手段,但要理解其背后的延迟语义,避免将问题掩盖到运行期才暴露。
  4. 注意Spring版本差异。不同版本对提前暴露与代理生成的处理策略可能不同,升级版本后应重新验证相关行为。

理解容器内部的时序与缓存机制,不仅能帮助排障,也能在设计阶段做出更稳健的决策。

发表评论

活动