【Spring】Spring的循环依赖以及解决方案

1. 什么是Spring循环依赖?

​ Spring循环依赖指的是两个或多个Bean之间相互依赖,形成一个环状依赖的情况。通俗的说,就是A依赖B,B依赖C,C依赖A,这样就形成了一个循环依赖的环。

​ Spring循环依赖通常会导致Bean无法正确地被实例化,从而导致应用程序无法正常启动或者出现异常。因此,Spring循环依赖是一种需要尽量避免的情况。

2. 常见形成原因

1. 构造函数循环依赖

​ 在使用构造函数注入Bean时,如果两个Bean之间相互依赖,就可能会形成构造函数循环依赖,例如:

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

上述代码,A、B的构造函数分别需要创建对方,A依赖B,B依赖A,它们之间形成了一个循环依赖。
当Spring容器启动时,它会尝试先实例化A,但是在实例化A的时候需要先实例化B,而实例化B的时候需要先实例化A,这样就形成了一个循环依赖的死循环,从而导致应用程序无法正常启动。

2. 属性循环依赖

​ 在使用属性注入Bean时,如果两个Bean之间相互依赖,就可能会形成属性循环依赖。例如:

@Component
public class A {   
    @Autowired    
    private B b;
}
=============
@Component
public class B {    
    @Autowired    
    private A a;
}

类似的,同样Spring在实例化A时会注入B,而注入B时又需要注入A,形成循环依赖

3. Spring如何解决循环依赖

要了解如何解决Spring的循环依赖,就必须要从Spring对Bean的创建过程入手,从原理上去理解,而不是单纯记忆

3.1 Spring 是如何创建Bean的?

image-20231127202430747

​ (1) 首先Spring容器启动之后,会根据使用不同类型的Application Context,通过不同的方式去加载Bean配置,如xml方式、注解方式,将这些Bean配置加载到容器中,作为Bean定义包装成BeanDefinition对象保存起来,为下一步创建Bean做准备。

​ (2) 根据加载的Bean定义信息,通过反射来创建Bean实例,如果是普通Bean,则直接创建Bean,如果是FactoryBean,说明真正要创建的对象为getObject()的返回值,调用getObject()将返回值作为Bean。

​ (3) 目前已经完成了Bean实例的创建,还需要对依赖的属性进行装配,例如,平时开发中Controller中,往往需要将Service Bean注入进来,循环依赖也是在这一步解决的,后面会详细说明,如果通过@Autowired注解注入的成员变量,则会通过AutowiredAnnotationBeanPostProcessor后置处理器进行注入,如果xml自动注入,则会根据按名字自动装配和按类型自动装配分别进行处理。

​ (4) 自动装配完成后,将完整的Bean对象保存到Spring 缓存中,接下来进入Bean的初始化流程,执行Bean的后置处理器的前置处理方法,如果Bean本身实现了InitializingBean接口,就去执行对应的afterPropertiesSet()方法,最后再执行Bean的后置处理器的后置处理方法。

3.2 Spring三级缓冲机制

1. Spring可以解决的依赖循环

回到上述的依赖循环案例,A、B相互依赖时,产生的循环依赖现象还需进一步细分:

image-20231127205639553

注意:只有单例的 Bean 存在循环依赖的情况,Spring才可以解决,原型(Prototype)情况下,Spring 会直接抛出异常。

​ Spring 不支持基于构造器注入的循环依赖。 但是假如 AB 循环依赖,如果一个是构造器注入,一个是 setter 注入呢?

看看几种情形:

循环依赖的几种情形
第四种可以而第五种不可以的原因是 Spring 在创建 Bean 时默认会根据自然排序进行创建,所以 A 会先于 B 进行创建。

简单总结,当循环依赖的实例都采用 setter 方法注入的时候,Spring 可以支持,都采用构造器注入的时候,不支持,构造器注入和 setter 注入同时存在的时候,看天。

2. 三层缓存机制

bean的创建流程:

image-20231127210241590

​ 依赖注入就发生在第二步,属性赋值,结合这个过程,Spring 通过三级缓存解决了循环依赖:

  1. 一级缓存 : Map<String,Object> singletonObjects,单例池,用于保存实例化、属性赋值(注入)、初始化完成的 bean 实例
  2. 二级缓存 : Map<String,Object> earlySingletonObjects,早期曝光对象,用于保存实例化完成的 bean 实例
  3. 三级缓存 : Map<String,ObjectFactory<?>> singletonFactories,早期曝光对象工厂,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。

三级缓存

我们来看一下三级缓存解决循环依赖的过程:

当 A、B 两个类发生循环依赖时:
循环依赖

A 实例的初始化过程:

  1. 创建 A 实例,实例化的时候把 A 对象⼯⼚放⼊三级缓存,表示 A 开始实例化了,虽然我这个对象还不完整,但是先曝光出来让大家知道

11

  1. A 注⼊属性时,发现依赖 B,此时 B 还没有被创建出来,所以去实例化 B
  2. 同样,B 注⼊属性时发现依赖 A,它就会从缓存里找 A 对象。依次从⼀级到三级缓存查询 A,从三级缓存通过对象⼯⼚拿到 A,发现 A 虽然不太完善,但是存在,把 A 放⼊⼆级缓存,同时删除三级缓存中的 A,此时,B 已经实例化并且初始化完成,把 B 放入⼀级缓存。

22

  1. 接着 A 继续属性赋值,顺利从⼀级缓存拿到实例化且初始化完成的 B 对象,A 对象创建也完成,删除⼆级缓存中的 A,同时把 A 放⼊⼀级缓存
  2. 最后,⼀级缓存中保存着实例化、初始化都完成的 A、B 对象

55

所以,我们就知道为什么 Spring 能解决 setter 注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。

所以,我们就知道为什么 Spring 能解决 setter 注入的循环依赖了,因为实例化和属性赋值是分开的,所以里面有操作的空间。如果都是构造器注入的化,那么都得在实例化这一步完成注入,所以自然是无法支持了。

  • 49
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值