Skip to content

循环依赖与@EnableAsync在特定场景下存在问题

在Java项目中,允许循环依赖开启@EnableAsync 本身并不存在直接冲突,但在特定场景下可能会因Spring的依赖注入机制和异步代理逻辑的交互而产生问题。下面从原理、冲突场景、解决方案三个维度深入分析:

一、核心概念与正常工作机制

在分析冲突前,先明确两者的核心机制:

1. 循环依赖(Circular Dependency)

指两个或多个Bean相互依赖(如A依赖B,B依赖A)。Spring默认支持单例Bean的循环依赖,通过三级缓存机制解决:

  • 一级缓存(singletonObjects):存储完全初始化的Bean;
  • 二级缓存(earlySingletonObjects):存储提前暴露的未完全初始化的Bean;
  • 三级缓存(singletonFactories):存储Bean的工厂方法,用于提前暴露代理对象。

当允许循环依赖时,Spring会在Bean未完全初始化时提前暴露其实例(或代理对象),供依赖方使用。

2. @EnableAsync注解

用于开启Spring的异步方法支持,其核心原理是:

  • 通过@Async标记的方法会被动态代理(默认使用JdkDynamicAopProxyCglibAopProxy);
  • 代理对象会将方法调用提交到线程池执行,实现异步效果;
  • 代理对象的创建时机通常是在Bean初始化完成后(postProcessAfterInitialization阶段)。

二、潜在冲突场景与原理分析

两者的冲突并非必然,而是发生在循环依赖的Bean中包含@Async方法的场景下,核心原因是代理对象创建时机与循环依赖的提前暴露机制不兼容

冲突场景示例

java
// 循环依赖的两个Bean,均包含@Async方法
@Service
public class AService {
    @Autowired
    private BService bService;

    @Async
    public void asyncMethodA() { ... }
}

@Service
public class BService {
    @Autowired
    private AService aService;

    @Async
    public void asyncMethodB() { ... }
}

此时启动项目可能出现两种异常:

  1. BeanCurrentlyInCreationException:提示Bean正在创建中,无法获取;
  2. 异步方法不生效:调用的是原始对象而非代理对象,导致同步执行。

冲突原理深度剖析

  1. 正常循环依赖流程(无@Async

    • AService初始化时依赖BService,Spring会先将未完全初始化的AService通过三级缓存暴露;
    • 初始化BService时,从三级缓存获取AService的早期引用,完成BService初始化;
    • BService初始化后,AService获取其引用,完成自身初始化,最终存入一级缓存。
  2. 引入@Async后的问题

    • @Async需要为Bean创建代理对象(代理对象才具备异步执行能力);
    • 代理对象默认在Bean初始化完成后创建(postProcessAfterInitialization);
    • 但循环依赖中,Spring会在Bean初始化过程中提前暴露原始对象(非代理对象);
    • 依赖方(如BService)注入的是AService的原始对象,而非代理对象,导致:
      • 若原始对象后续被代理,依赖方持有的引用与最终的代理对象不一致,可能引发异常;
      • 调用asyncMethodA()时使用的是原始对象,异步逻辑不生效。
  3. 根本矛盾

    • 循环依赖需要提前暴露Bean的引用(原始对象);
    • @Async需要用代理对象替换原始对象;
    • 两者的时机不匹配,导致依赖注入的对象与最终的代理对象不一致。

三、解决方案

针对上述冲突,可通过调整代理创建时机、修改依赖注入方式等手段解决,具体方案如下:

方案1:强制提前创建代理对象(推荐)

通过配置proxyTargetClass = trueexposeProxy = true,让Spring在循环依赖暴露阶段就创建代理对象,确保依赖方注入的是代理对象。

配置方式:
java
@Configuration
@EnableAsync(proxyTargetClass = true) // 强制使用CGLIB代理(类代理)
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.initialize();
        return executor;
    }

    // 开启代理暴露,允许通过AopContext获取当前代理对象
    @Bean
    public static AspectJAutoProxyRegistrar exposeProxy() {
        // 设置exposeProxy=true,强制提前暴露代理对象
        System.setProperty("spring.aop.exposeProxy", "true");
        return new AspectJAutoProxyRegistrar();
    }
}
原理:
  • proxyTargetClass = true:使用CGLIB代理(类继承方式),而非JDK动态代理(接口实现),CGLIB代理的创建时机更早,可在循环依赖暴露阶段生成;
  • spring.aop.exposeProxy = true:强制Spring在三级缓存中暴露代理对象,而非原始对象,确保依赖方注入的是代理对象。

方案2:避免字段注入,使用构造器注入+@Lazy

通过@Lazy注解延迟依赖注入的初始化,避免循环依赖在初始化阶段直接引用未完成的Bean。

修改代码:
java
@Service
public class AService {
    private final BService bService;

    // 构造器注入+@Lazy,延迟BService的初始化
    @Autowired
    public AService(@Lazy BService bService) {
        this.bService = bService;
    }

    @Async
    public void asyncMethodA() { ... }
}

@Service
public class BService {
    private final AService aService;

    @Autowired
    public BService(@Lazy AService aService) {
        this.aService = aService;
    }

    @Async
    public void asyncMethodB() { ... }
}

原理:

  • @Lazy会为依赖对象创建一个代理(延迟初始化代理),在首次调用时才真正初始化目标Bean;
  • 构造器注入时,@Lazy避免了循环依赖双方在初始化阶段的直接引用,从而绕过提前暴露原始对象的问题;
  • 由于代理对象在调用时才初始化,此时@Async的代理已创建,确保异步方法生效。

方案3:重构代码,消除循环依赖(最佳实践)

循环依赖本质上是代码设计问题,消除循环依赖可从根本上避免冲突:

  • 将A和B共同依赖的逻辑抽取为第三方Bean(如CService),让A和B都依赖C,而非相互依赖;
  • 示例:
    java
    // 抽取共同逻辑到CService
    @Service
    public class CService {
        // 原A和B共同依赖的逻辑
    }
    
    @Service
    public class AService {
        @Autowired
        private CService cService; // 依赖C,而非B
    
        @Async
        public void asyncMethodA() { ... }
    }
    
    @Service
    public class BService {
        @Autowired
        private CService cService; // 依赖C,而非A
    
        @Async
        public void asyncMethodB() { ... }
    }

方案4:使用ObjectProvider延迟获取依赖

通过Spring的ObjectProvider接口延迟获取依赖对象,避免在初始化阶段直接注入。

修改代码:
java
@Service
public class AService {
    @Autowired
    private ObjectProvider<BService> bServiceProvider; // 延迟获取BService

    public void doSomething() {
        BService bService = bServiceProvider.getIfAvailable(); // 首次调用时才获取
        bService.asyncMethodB();
    }

    @Async
    public void asyncMethodA() { ... }
}
原理:
  • ObjectProvider会在调用getIfAvailable()时才真正获取BService实例,此时BService已完成初始化和代理创建;
  • 避免了初始化阶段的循环依赖引用,确保注入的是完整的代理对象。

四、总结

允许循环依赖与@EnableAsync的冲突本质是代理对象创建时机与循环依赖提前暴露机制的不匹配,导致依赖注入的对象不是最终的异步代理对象。解决方案优先级如下:

  1. 重构代码消除循环依赖:从设计层面解决,避免后续隐患;
  2. 强制提前创建代理对象:通过proxyTargetClass = trueexposeProxy = true确保依赖注入代理对象;
  3. 构造器注入+@Lazy:适合无法重构代码的场景,通过延迟初始化绕过冲突;
  4. ObjectProvider延迟获取:灵活度高,适合局部依赖场景。

实际开发中,推荐优先采用“消除循环依赖”的方案,这是最彻底且符合代码设计原则的做法。

Released under the MIT License.