Spring循环依赖

Spring循环依赖

1. 什么是循环依赖

两个或多个对象之间存在相互依赖的情况,形成了一个闭环的依赖关系。
具体来说,在循环依赖中,A依赖于B,B又依赖于C,而C又回到依赖于A。这样的依赖链条形成了一个循环,导致在创建或使用这些对象时出现问题。

示例代码如下:

@Component
public class A {
    // A中注入了B
 @Autowired
 private B b;
}

@Component
public class B {
    // B中注入C
 @Autowired
 private C C;
}

@Component
public class C {
    // C中注入A
 @Autowired
 private A a;
}

2. 循环依赖能引发什么问题

循环依赖可能引发以下问题:

  1. 初始化顺序不确定:循环依赖导致无法确定哪个对象应该先被创建和初始化,从而造成初始化顺序的混乱。这可能导致错误的结果或意外的行为。

  2. 死锁和无限递归:当循环依赖中的对象在创建过程中相互等待对方完成创建时,可能会导致死锁或无限递归的情况发生。这将使程序无法继续执行,最终导致系统崩溃或进入无限循环。

  3. 难以理解和维护:循环依赖增加了代码的复杂性和耦合度,使得代码难以理解和维护。在一个循环依赖链中,修改一个类可能会影响到其他所有相关的类,增加了代码的脆弱性。

  4. 可测试性下降:循环依赖使得各个模块或类之间的责任不清晰,难以进行单独的单元测试或模块拆解。这给软件的测试和调试带来困难,降低了可测试性和可维护性。

为避免循环依赖带来的问题,应尽量避免在设计和实现中出现循环依赖关系。重新考虑系统的架构,采用合适的设计模式、解耦方式和依赖注入机制,可以帮助解决或避免循环依赖带来的问题。

3. Spring是怎么处理循环依赖的

我们先创建如下示例:

public class A {
    private B b;

    public B getB() {
        return b;
    }

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

    public A() {
        System.out.println("---A created success");
    }

}

public class B {
    private A a;

    public A getA() {
        return a;
    }

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


    public B() {
        System.out.println("---B created success");
    }
}

public class ClientCode {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        A a = context.getBean("a",A.class);
        B b = context.getBean("b",B.class);
    }
}

创建applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--
        1.spring容器默认的单例模式可以解决循环引用,单例默认支持
        2.spring容器原型依赖模式scope="prototype"多例模式下不能解决循环引用
    -->

    <!--depends-on 的意思就是当前这个bean如果要完成,先看depends-on指定的bean是否已经完成了初始化-->
    <!--scope="prototype"代表每次都要新建一次对象-->


    <bean id="a" class="com.xmc.circular.A" scope="prototype">
        <property name="b" ref="b"/>
    </bean>

    <bean id="b" class="com.xmc.circular.B" scope="prototype">
        <property name="a" ref="a"/>
    </bean>

</beans>

执行过程中报错如下:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:274)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
	at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
	... 17 more

据此我们得到以下结论:

spring容器中原型(Prototype)的场景是不支持循环依赖的,抛出BeanCurrentlyInCreationException异常

其实,Spring容器中单例(singleton)的场景是支持循环依赖的,Spring使用了三级缓存提前曝光(early exposure)的机制来处理循环依赖问题

  • 第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
  • 第二级缓存: earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
  • 第三级缓存: Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂

DefaultSingletonBeanRegistry

所谓的三级缓存其实就是spring容器内部用来解决循环依赖问题的三个map

Spring实例Bean的本质

  • 实例化
    • 堆内存中申请一块内存空间
    • 租赁好房子,自己的家具东西还没有搬家进去
  • 初始化属性填充
    • 完成属性的各种赋值
    • 装修、家电家具进场

3大Map和四大方法,总体相关对象

1.getSingleton:从容器里面获得单例的bean    
2.doCreateBean: 没有就创建bean  
3.populateBean: 创建完了以后,要填充属性    
4.addSingleton: 填充完了以后,再添加到容器进行使用  
  • 第一层singletonObjects存放的是已经初始化好了的Bean,
  • 第二层earlySingletonObjects存放的是实例化了,但是未初始化的Bean,
  • 第三层singletonFactories存放的是FactoryBean。假如A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean

A/B两对象在三级缓存中的迁移说明

  • 1 A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B
  • 2 B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
  • 3 B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态) 然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。

4. Spring怎样注入才能不产生循环依赖问题

我们这里直接上结论

依赖情况注入方式能否解决循环依赖
AB相互依赖(循环依赖)均采用属性注入不能
AB相互依赖(循环依赖)均采用set方法注入
AB相互依赖(循环依赖)均采用构造器注入不能
AB相互依赖(循环依赖)A中注入B采用属性注入,B中注入A采用set方法注入
AB相互依赖(循环依赖)A中注入B采用set方法注入,B中注入A采用属性注入

Spring版本

spring-context:5.3.23

属性注入方式验证示例如下:

@Component
public class ComponentA {

    @Autowired
    private ComponentB componentB;
}

@Component
public class ComponentB {

    @Autowired
    private ComponentA componentA;
}

// 报错信息如下:
The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  componentA (field private com.xmc.hello.circular.ComponentB com.xmc.hello.circular.ComponentA.componentB)
↑     ↓
|  componentB (field private com.xmc.hello.circular.ComponentA com.xmc.hello.circular.ComponentB.componentA)
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

属性注入方式验证示例如下:

@Component
public class ComponentA {

    private ComponentB componentB;

    public void setComponentB(ComponentB componentB) {
        this.componentB = componentB;
    }
}

@Component
public class ComponentB {

    private ComponentA componentA;

    public void setComponentA(ComponentA componentA) {
        this.componentA = componentA;
    }
}

// 注入成功

构造器注入方式验证示例如下:

@Component
public class ComponentA {

    private ComponentB componentB;

    public ComponentA(ComponentB componentB) {
        this.componentB = componentB;
    }
}

@Component
public class ComponentB {

    private ComponentA componentA;

    public ComponentB(ComponentA componentA) {
        this.componentA = componentA;
    }
}

// 报错如下:
The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  componentA defined in file [E:\ideaworkspaces\myself-ideaworkspaces\spring-boot-test\spring-boot-hello-world\target\classes\com\xmc\hello\circular\ComponentA.class]
↑     ↓
|  componentB defined in file [E:\ideaworkspaces\myself-ideaworkspaces\spring-boot-test\spring-boot-hello-world\target\classes\com\xmc\hello\circular\ComponentB.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

A中注入B采用属性注入,B中注入A采用set方法注

@Component
public class ComponentA {

    @Autowired
    private ComponentB componentB;

}

@Component
public class ComponentB {

    private ComponentA componentA;

    public void setComponentA(ComponentA componentA) {
        this.componentA = componentA;
    }
}

// 注入成功
// A中注入B采用set方法注入,B中注入A采用属性注入 和这个示例类似

接下来我们看看官网关于循环依赖是怎么解释的:

那么有什么方式可以解决循环依赖了

  • set方法注入
  • 使用@Lazy注解
  • 可以采用cn.hutool.extra.spring#SpringUtil.getBean()方法
  • 当然业务重新设计,解除循环依赖更好

5. 总结

Spring是如何解决的循环依赖?

  • Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化
  • 每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个
  • 当我们创建 beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完 beanB填充属性时又发现它依赖了beanA又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA,所以不需要继续创建,用它注入beanB,完成beanB的创建
  • 既然 beanB创建好了,所以beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成

Spring解决循环依赖依靠的是Bean的中间态这个概念,而这个中间态指的是已经实例化但还没初始化的状态,也就是
半成品。 实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。

Spring为了解决单例的循环依赖问题,使用了三级缓存其中

  • 一级缓存为单例池(singletonObjects)
  • 二级缓存为提前曝光对象(earlySingletonObjects)
  • 三级缓存为提前曝光对象工厂(singletonFactories)。

假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程。

Spring解决循环依赖的整个流程图

Debug的步骤---->Spring解决循环依赖过程

  • 1 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
  • 2 在getSingleton()方法中,从一级缓存中查找,没有,返回null
  • 3 doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
  • 4 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法
  • 5 进入AbstractAutowireCapableBeanFactory#doCreateBean,先反射调用构造器创建出beanA的实例,然后判断。是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中〈即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中
  • 6 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
  • 7 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
  • 8 此时beanB依赖于beanA,调用getsingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
  • 9 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
  • 10 随后beanA继续他的属性填充工作,此时也获取到了beanBbeanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中

6. 参考技术

尚硅谷Java大厂面试题第3季

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring循环依赖指的是在Spring中,多个Bean之间存在相互依赖的情况。具体来说,当一个Bean A依赖于另一个Bean B,同时Bean B也依赖于Bean A时,就形成了循环依赖。这种情况下,Spring需要解决Bean的创建和依赖注入的顺序问题。 在Spring中,循环依赖问题是由于Bean的生命周期所引起的。Spring的Bean生命周期包括了Bean的实例化、属性注入、初始化以及销毁等过程。当出现循环依赖时,Spring通过使用“提前暴露”的方式来解决这个问题。 具体来说,当Spring创建Bean A时,发现它依赖于Bean B,于是创建一个A的半成品对象,并将其暂时放入一个缓存中。然后,Spring继续创建Bean B,并将其注入到A的属性中。接着,Spring继续完成B的创建,并将其放入缓存中。最后,Spring将A的半成品对象交给B进行依赖注入,完成A的创建,并将其从缓存中移除。 需要注意的是,Spring循环依赖有一定的限制条件。例如,如果Bean A和Bean B都是单例模式,那么它们之间的循环依赖是无法解决的。因为单例模式下,Bean的创建和依赖注入是同时进行的,无法通过缓存来解决循环依赖。在这种情况下,程序员需要手动调整Bean的依赖关系或使用其他解决方案来避免循环依赖问题。 综上所述,Spring循环依赖是指在Spring中多个Bean之间存在相互依赖的情况。Spring通过使用缓存和提前暴露的方式来解决循环依赖问题,但在某些情况下有一定的限制条件需要注意。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值