@Async注解导致偶发循环依赖问题产生原因

问题

碰到本期启动没问题,但是发布到测环境时偶尔发生循环依赖导致报错的问题,有时重新发布又变正常,十分奇怪,那么原因是什么,需要认真研究下。
问题描述
现在有三个bean A,B,C
A和B之间互相相互依赖
依赖关系如下图
image.png
箭头方向表示被依赖。

@Async注解

首先要知道的一点是,spring虽然默认解决了属性注入的循环依赖,但是对于一些代理类是无法解决的。例如这个@Async注解经常导致循环依赖的问题。使用了这个注解,会导致bean被代理。具体报错点在
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean的
image.png
exposedObject代表提前暴露的实例
bean代表整个bean加载完成对的实例
即提前暴露的bean不等于初始化完成的bean,那么就会报出循环依赖的错误。如果想要了解更多关于循环依赖,请阅读https://blog.csdn.net/qq_37436172/article/details/127028061,或详细了解循环依赖的原理。

问题复现

上面我们知道了使用@Async会导致循环依赖的问题,现在有几种加载顺序
1、C->A,A->B,B->A
首先加载C,C根据依赖加载A,A加载B,B又依赖A,那么又会反过来加载A。我们知道这种情况下,spring循环依赖会自动解决这种循环依赖,因此不会报错。
2、A->B ,B->A,C->A
跟1是相同的情况,不会导致循环依赖
3、B->A,A->B,C->A
先初始化B,提前暴露B的实例,继续初始化A,A依赖到B提前暴露的实例,返回,B继续完成对自己的初始化,会被加上一层代理,A依赖的B未被代理和真实被代理的B不是一个实例,显然spring会为此抛出错误,就是上面说的那个错误原因。
这三种情况,先加载B会导致循环依赖只有第三种情况。即先加载的是带@Async注解的那个bean才会报循环依赖的错误。

问题原因

首先明确,默认的bean加载顺序并不是固定的,本地运行没问题,不能代表万事大吉。这也是出现偶发循环依赖错误的问题所在。如果是第三种加载顺序就是会报出循环依赖的错误。
原因是@CompontScan扫描的包路径,扫描class顺序未必是一致的。例如我本地和服务器就是不一致的。

控制bean加载顺序

解决方法也很容易想到,那就是保证A先于B进行加载。
那么如何进行控制?
首先需要说明的是:在Bean上加@Order(xxx)是无法控制bean注入的顺序的!
原因:注解@Order或者接口Ordered的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不受@Order或Ordered接口的影响
控制bean的加载顺序的方法

  1. @DependsOn 注解
  2. BeanPostProcessor 扩展

1.@DependsOn

不推荐的原因:这种方法是通过bean的名字(字符串)来控制顺序的,如果改了bean的类名,很可能就会忘记来改所有用到它的注解,那就问题大了。
当一个bean需要在另一个bean实例化之后再实例化时,可使用这个注解。

@DependsOn("A")
@Component
public class B {
    
    
    private String name = "Im B";
    @Autowired
    A a;
 
    public B() {
    
    
        System.out.println(name);
    }
}
 
@Component
public class A {
    
    
    private String name = "Im A";
    @Autowired
    B b;
    public A() {
    
    
        System.out.println(name);
    }
}

2.自定义BeanDefinitionRegistryPostProcessor

通过实现BeanDefinitionRegistryPostProcessor接口,在postProcessBeanDefinitionRegistry方法中通过BeanDefinitionRegistry获取到所有bean的注册信息,将bean保存到LinkedHashMap中,并从BeanDefinitionRegistry中删除,然后将保存的bean定义排序后,重新再注册到BeanDefinitionRegistry中,即可实现bean加载顺序的控制
这种方法虽然自定义程序高,但是风险同样高,需要对spring的原理更加清晰。

最佳解决办法

最佳办法永远是,破除循环依赖。循环依赖已经给我们造成了如此难排查的问题。所以经验告诉我们,一定要慎用循环依赖的场景。最好使用构造注入

构造方法依赖(推荐)

会直接在启动时抛出循环依赖的错误,从而杜绝循环依赖的产生。

@Component
public class B {
    
    
    A a;
    public B(A a) {
    
    
        this.a=a;
    }
}
 
@Component
public class A {
    
    

        B b;
      public A(B b) {
    
    
        this.b=b;
    }
}

猜你喜欢

转载自blog.csdn.net/qq_37436172/article/details/131742331