Spring bean 循环依赖

·  阅读 1626

Java 中的循环依赖

什么是循环依赖

例如,有下面两个类

class A {
    private B b;
    public void setB(B b) {this.b = b;}
}


class B {
    private A a;
    public void setA(A a) {this.a = a;}
}
复制代码

A 对象需要 B 对象,B 对象中需要 A 对象,相互需要,所以被称为循环依赖

怎么创建循环依赖的对象

要创建上面这种循环依赖的对象,可以这样做

public static void main(String[] args) {
    A a = new A();
    B b = new B();

    // 依赖注入
    a.setB(b);
    b.setA(a);
}
复制代码

核心思想就是,先实例化,后设置依赖

image.png

如上图,是一次运行的结果,为了更好的方便大家理解这种循环依赖对象的创建过程,我画了一张图

image.png

  • 执行完第一句代码后:堆中有了 A 类的实例 A@491,其中属性 b 为 null
  • 执行完第二句代码后:堆中有了 B 类的实例 B@492,其中属性 a 为 null
  • 执行完第三句代码后:堆中 A@491 的属性 b 指向 B@492
  • 执行完第四句代码后:堆中 B@492 的属性 a 指向 A@491

代码中的 A 中有 B,B 中有 A ,体现在堆上,其实 A@491 中有一个指向 B@492 的引用,B@492 中也有一个指向 A@491 的引用

这样我们就创建出了循环依赖的对象

什么样的循环依赖无法解决

如果某种循环依赖的对象无法先实例化,后设置依赖,那么这种循环依赖是无解的!

例如,A、B 通过构造方法设置依赖

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

class B {
    private A a;
    public B(A a) {
        this.a = a;
    }
}
复制代码

这种循环依赖,在实例化时,就需要依赖对象,而依赖对象在实例化时又需要自己,因此这种循环依赖是无解的!

也就是说并不是所有的循环依赖都可以解决!

Spring 循环依赖

Spring 解决循环依赖的思路跟上面的一样,先实例化,后设置依赖。同样,对于通过构造方法造成的循环依赖无法解决!

Spring 能解决哪种场景下的循环依赖

条件一:依赖注入方式!

Spring 中,依赖注入有三种方式

关于 Spring 依赖注入的详情,参考 Spring 依赖注入

  1. 基于构造方法的依赖注入
  2. 基于 setter() 方法的依赖注入
  3. 基于 成员变量 的依赖注入

第一种基于构造方法的依赖注入,上面也提到了,如果产生了循环依赖问题,是无法解决的(真的吗?参考后记),其余两种如果产生了循环依赖问题,都可以通过先实例化,后设置依赖的方式解决

条件二:BeanScope

关于 BeanScope 的详情,参考 Spring Bean Scope

Spring 中, BeanScope 会直接影响 Bean 的创建行为,而创建行为则决定了是否能解决循环依赖问题,在Bean 的5种 Scope 中,只有 singleton Scope 的创建行为,在发生循环依赖问题时,Spring 框架可以自动帮你解决,前提是满足条件一

总结

发生循环依赖时,Spring 框架可以自动帮我们解决,但是有两个前提条件

  1. Bean 基于成员变量或 setter() 方法的方式注入依赖
  2. BeanScope 必须为 singleton

Spring singleton Bean 的生命周期

既然只有 singleton 作用域下,创建 Bean 时,才能解决 bean 的循环依赖,那我们就必须了解一下,创建 bean 的过程,这一块内容比较多,我单独写了一篇文章,参考 Spring singleton Bean 的生命周期

Spring bean 循环依赖案例分析

我们 A、B 两个类,他们相互依赖

@Service
class B {
    private A a;

    @Autowired
    public void setA(A a) {this.a = a;}
}

@Service
class A {
    private B b;

    @Resource
    public void setB(B b) {this.b = b;}
}
复制代码

要解决循环依赖必须使用基于成员变量或 setter() 方法的方式注入依赖,Spring 不推荐使用基于成员变量的方式注入依赖,所以我使用的是 setter() 方法的方式,注意,这里特意用了 @Autowired@Resource 两种注解,两者都可以进行自动装配,效果一样

现在我们结合 Spring singleton Bean 的生命周期 中的流程,来梳理 Spring 是如何解决这种循环依赖

spring bean 生命周期.jpg

假设 spring 是先创建 A, singleton 作用域下,创建 Bean 时,会调用 doGetBean() ,先从缓存中获取,获取不到在创建

  1. 调用doGetBean(nameA,...) 方法,获取/创建 beanA
  2. 先调用 getSingleton(nameA) 从三级缓存中查找,如果找到直接返回,此时三级缓存还是空的,因此找不到
  3. 没找到就调用 getSingleton(nameA, ()->{createBean(nameA,...)}) 去创建
  4. getSingleton() 方法内部会调用 createBean(nameA,...) 创建 beanA
  5. createBean(nameA,...) 方法内部会首先调用 createBeanInstance(nameA,...)来实例化 beanA,此时虽然有 beanA 对象了,但是 beanA 内部的属性 b 还是 null ,需要等待后期填充
  6. 调用 addSingletonFactory(nameA, () -> getEarlyBeanReference(nameA, mbd, beanA)) 方法将能够获取到 beanA 或 beanA的代理对象的匿名函数(bean 工厂)放入第三级缓存中
  7. 调用populateBean(nameA, mbd, beanA)方法填充 beanA 的属性a,在该方法里面最终会调用 doGetBean(nameB,...) 方法,获取/创建 beanB
    1. 调用doGetBean(nameB,...) 方法,获取/创建 beanB
    2. 先调用 getSingleton(nameB) 从三级缓存中查找,如果找到直接返回,此时只有第三级缓存中有一个 beanA 的工厂方法,因此找不到
    3. 没找到就调用 getSingleton(nameB, ()->{createBean(nameB,...)}) 去创建
    4. getSingleton() 方法内部会调用 createBean(nameB,...) 创建 beanB
    5. createBean(nameB,...) 方法内部会首先调用 createBeanInstance(nameB,...)来实例化 beanB,此时虽然有 beanB 对象了,但是 beanB 内部的属性 a 还是 null ,需要等待后期填充
    6. 调用 addSingletonFactory(nameB, () -> getEarlyBeanReference(nameB, mbd, beanB)) 方法将能够获取到 beanB 或 beanB的代理对象的匿名函数(bean 工厂)放入第三级缓存中
    7. 调用populateBean(nameB, mbd, beanB)方法填充 beanB 的属性a,在该方法里面最终会调用 doGetBean(nameB,...) 方法,获取 beanA
      1. 调用doGetBean(nameA,...) 方法,获取/创建 beanA
      2. 先调用 getSingleton(nameA) 从三级缓存中查找,此时第三级缓存中有 beanA 的工厂,因此会将 beanA 的工厂从第三级缓存拉出来,调用获取方法,获取 beanA的"提前引用",这个例子中由于没有对 A 使用 AOP 因此,这里拿到的就是原始的 beanA,否则,拿到的将是 beanA 的代理
      3. 将第三级缓存中 beanA 的工厂删掉,然后将 beanA的"提前引用"放入二级缓存中
      4. 返回 beanA的"提前引用"
    8. 此时 beanB 的属性已经填充好了,属性a是 beanA的"提前引用"。然后调用 initializeBean(nameB, beanB, mbd) 进行初始化
    9. 将创建完成的 beanB 放入到第一级缓存中,然后删除掉第二、三级缓存中 beanB 的内容
    10. 返回beanB
  8. 此时 beanA 的属性已经填充好了,属性b是第一级缓存中的 beanB。然后调用 initializeBean(nameA, beanA, mbd) 进行初始化
  9. 将创建完成的 beanA 放入到第一级缓存中,然后删除掉第二、三级缓存中 beanA 的内容
  10. 返回beanA

以上是 A 、B 没有被 AOP 代理的情况,如果被代理了,其实也差不多,唯一的区别在于从第三级缓存中拿到 bean 工厂后调用获取方法拿到的不是原始 bean 对象,而是 bean 的代理对象,其他地方都一样

后记

上面提到,基于构造方法的依赖注入方式,如果发生了循环依赖,无法解决,例如

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

class B {
    private A a;
    public B(A a) {
        this.a = a;
    }
}
复制代码

A 在实例化阶段,就需要依赖对象B,而依赖对象B在实例化阶段又要依赖A,因此我刚开始认为这种循环依赖是无法解决的

但评论区中一位读者提醒说,在 spring 中 基于构造方法的依赖注入方式,如果发生了循环依赖,可以通过加 @Lazy 注解解决

经过实践发现确实可行,那 spring 是如何解决的呢?

答案是动态代理,实例化 A 的时候,传入的不是 B,而是 B 的代理,它继承了 B,例如

class A {
    private B b;
    @Lazy
    public A(B b) {
        this.b = b;
    }
}

class B {
    private A a;
    public B(A a) {
        this.a = a;
    }
}
复制代码

创建完成之后,对应的关系如下

image-20210903114149150.png

B 的 CGLib代理对象继承了 B,可以替代 B ,行使 B 的所有能力,他们在内存中的结构如下

image.png

以上例子使用的是 singleton 作用域

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改