前言:最近遇到一个问题,使用@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三级缓存以及如何解决循环依赖