【Spring】使用@Async注解后导致的循环依赖问题

前言:最近遇到一个问题,使用@Async注解将方法设置为异步的时候,出现了循环依赖(circular reference)问题。

1.问题复现

java 复制代码
@Service
public class A implements AInterface {
    @Autowired
    private BInterface b;
    @Async
    @Override
    public void funA() {
    }
}

@Service
public class B implements BInterface {
    @Autowired
    private AInterface a;
    @Override
    public void funB() {
        a.funA();
    }
}

根本原理是只要能被切面AsyncAnnotationAdvisor切入(即只需要类/方法有标注@Async注解即可)的Bean最终都会生成一个代理对象返回,最终加入到Spring容器内,即Spring容器中的对象为代理对象。

具体报错点在org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory的doCreateBean方法中

  • exposedObject代表提前暴露的实例
  • bean代表整个bean加载完成的实例(加入到Spring容器内的实例)
  • 即提前暴露的bean不等于初始化完成的bean,那么就会报出循环依赖的错误。

逐步解析:

  • context.getBean(A)开始创建A,A实例化完成后给A的依赖属性b开始赋值
  • context.getBean(B)开始创建B,B实例化完成后给B的依赖属性a开始赋值
  • 此时因为A支持循环依赖,所以会执行A的getEarlyBeanReference方法,B得到A的早期引用。而执行getEarlyBeanReference()的时候因为@Async根本还没执行,所以最终返回的仍旧是A原始对象的地址
  • 然后B完成了初始化,完成属性的赋值,此时属性field持有的是Bean A原始类型的引用
  • 回到A,A完成了A的属性的赋值(此时已持有B的实例的引用),继续执行初始化方法initializeBean(...),在此处会解析@Aysnc注解,从而生成一个代理对象A,所以最终exposedObject是一个代理对象(而非原始对象)最终加入到容器里
  • 但是B引用的属性A是个原始对象,而此处返回的实例A是个代理对象,也就是说B引用的并非是最终对象(不是最终放进容器里的对象)
  • 执行自检程序:由于allowRawInjectionDespiteWrapping默认值是false,表示不允许上面不一致的情况发生,最终就报错了

2.解决方案

2.1 @Lazy注解

java 复制代码
@Service
public class B implements BInterface {
    @Lazy
    @Autowired
    private AInterface a;

    @Override
    public void funB() {
        a.funA();
    }
}

注意:因为a最终会是@Async的代理对象,所以在@Autowired它的地方加@Lazy,若不存在循环依赖而是直接引用a,是不用加@Lazy的,因为是B希望依赖进来的是最终的代理对象进来,所以B加上即可,A上并不需要加。


2.2 不让@Async的Bean参与循环依赖

这种方法无疑是最优的方案。但它却是现实情况中最为难达到的方案。因为在实际业务开发中像循环依赖、类内方法调用等情况并不能避免,除非重新设计、按规范改变代码结构。


3. 其他

@Async 注解的异步方法在同一类内部调用时不会生效,这是因为 @Async 是基于 Spring AOP 代理机制的,在同一类内部调用是直接在同一个 Bean 内部完成的,没有通过代理,只有通过代理对象进行的调用才能触发异步处理。

至于@Async没生效这种问题为何总是不被注意,因为它和事务不生效不一样,@Async若没生效99%情况下都不会影响到业务的正常进行,因为它不会影响数据正确性,只会影响到性能(异步变同步)。


也许你可能会问,Spring不是已经解决了循环依赖的问题了吗?为什么不能解决使用@Async 注解导致的循环依赖,原因有以下几点:

  • @Async 注解的功能是通过 Spring AOP 代理来实现的。当 Spring 容器启动时,@Async 注解会触发 Spring AOP 创建一个代理对象,代理对象会将方法调用异步转发到线程池执行。如果存在循环依赖的情况(例如,Bean A 依赖于 Bean B,Bean B 又依赖于 Bean A),Spring 会通过三级缓存来进行处理,但是异步调用和代理对象的行为与普通的 Bean 实例化过程不同,它们之间存在一定的隔离性。
  • @Async 的代理对象是在容器初始化过程中创建的,它是通过代理机制生成的,并不是直接由 @Async 注解产生的。这就意味着,@Async 代理是与常规的 Bean 实例化和注入过程有差异的。因此,虽然 Spring 可以通过三级缓存解决常规的循环依赖问题,但是对于 @Async 的代理对象,Spring 无法直接通过三级缓存来解决循环依赖的问题。
  • @Async 的方法调用是异步执行的,它本身并不等同于常规的同步方法调用,因此,循环依赖问题仍然会影响到代理对象的初始化和依赖注入。

Spring如何解决循环依赖可以看我的这一篇博客:Spring三级缓存以及如何解决循环依赖

相关推荐
●VON几秒前
go语言的成神之路-标准库篇-os标准库
linux·运维·服务器·开发语言·后端·学习·golang
一只拉古1 分钟前
后端编程大师之路:在 .NET 应用中使用 ElasticSearch 和 Kibana 进行日志管理
后端·elasticsearch·架构
小屁孩大帅-杨一凡9 分钟前
python实现文件夹打包成jar
java·开发语言·python·pycharm·jar
RealmElysia29 分钟前
谷粒商城基础篇完结
java·微服务
lxl_h41 分钟前
IDEA 打包普通JAVA项目为jar包
java·intellij-idea·jar
好奇的菜鸟42 分钟前
解决 IntelliJ IDEA 启动错误:插件冲突处理
java·ide·intellij-idea
what_201843 分钟前
idea添加作者注释和方法注释、属性注释
java·ide·intellij-idea
m0_7482540944 分钟前
2024.1.4版本的IntelliJ IDEA创建Spring Boot项目的详细步骤
java·spring boot·intellij-idea
m0_748239471 小时前
Spring IDEA 2024 安装Lombok插件
java·spring·intellij-idea
dr李四维1 小时前
PO、VO、DAO、BO、DTO、POJO 你能分清吗?
java·po·dao·dto·vo·pojo·bo