spring如何解决循环依赖(3种常用方式详解)

spring如何解决循环依赖(3种常用方式详解)-mikechen

什么是循环依赖

循环依赖是指在软件系统中的两个或多个模块、类、或对象之间,形成了相互依赖关系的情况,而这种依赖关系形成一个循环闭环。

如下图所示:

spring如何解决循环依赖(3种常用方式详解)-mikechen

A依赖于B、B依赖于A,就是多个bean之间相互依赖,形成了一个闭环,这就是循环依赖。

 

循环依赖的危害

循环依赖在软件开发中可能会引发多种危害,包括但不限于以下几个方面:

  1. 难以维护和理解:循环依赖增加了代码的复杂性,开发人员需要跟踪多个类之间的相互引用关系,使得代码难以理解和维护。
  2. 编译问题:循环依赖可能导致编译器无法正确解析模块或类之间的依赖关系,从而引发编译错误。
  3. 运行时问题:在程序运行时,循环依赖可能导致死锁,或无限递归调用,从而导致程序崩溃或性能问题。
  4. 性能问题:循环依赖可能导致不必要的性能开销,因为系统可能会频繁地执行不必要的递归调用或循环操作。
  5. 不良设计:循环依赖通常是设计不佳的标志,它可能表明系统的模块划分和架构需要重新审视。好的软件设计应该尽量避免或减少循环依赖。

 

Spring如何解决循环依赖

Spring框架解决循环依赖的方式主要依赖于三种机制:构造器注入、单例模式、和提前曝露。

1.构造器注入

Spring容器可以通过使用构造器注入来解决循环依赖问题,构造器注入是一种依赖注入方式,允许Bean通过构造函数接受依赖,从而在Bean的创建阶段解决循环依赖。

假设有两个类,ClassAClassB,它们彼此相互依赖:

public class ClassA {
    private ClassB classB;

    public ClassA(ClassB classB) {
        this.classB = classB;
    }
    // Other methods
}

public class ClassB {
    private ClassA classA;

    public ClassB(ClassA classA) {
        this.classA = classA;
    }
    // Other methods
}

在配置文件中,Spring容器可以如下实例化这两个类:

<bean id="classA" class="com.example.ClassA">
    <constructor-arg ref="classB" />
</bean>

<bean id="classB" class="com.example.ClassB">
    <constructor-arg ref="classA" />
</bean>

通过构造器注入,Spring容器能够正确解决循环依赖问题,确保ClassAClassB都正确地初始化。

构造器注入原理

  • 当Spring容器实例化Bean时,它会首先检查Bean定义中的构造函数参数,以确定Bean的依赖关系。
  • 如果Bean A 依赖于 Bean B,而 Bean B 同时依赖于 Bean A,Spring容器会选择其中一个Bean(通常是A或B)来优先创建,并传递依赖关系。
  • 当被选择的Bean(比如A)在构造函数中接受依赖(Bean B)作为参数时,Spring容器会将Bean B 的一个代理(Proxy)传递给Bean A 的构造函数。

随后,Bean B 的创建会继续,但这次它不再依赖于Bean A,而是依赖于Bean A 的代理。

Bean A 的代理最终会被初始化,同时解决了Bean B 对 Bean A 的循环依赖。

 

2.单例模式

Spring容器的单例模式是另一种用于解决循环依赖的方法。

在单例模式下,Spring容器默认创建Bean的单个实例,并在容器的生命周期内共享这个实例。

Spring容器默认将Bean配置为单例(Singleton),这意味着容器只会创建一个Bean实例,并在应用程序的整个生命周期中重复使用该实例。

当两个Bean相互依赖并配置为单例时,Spring容器会先创建其中一个Bean的实例,然后将其引用注入到另一个Bean。

假设有两个类,ClassAClassB,它们彼此相互依赖:

public class ClassA {
    private ClassB classB;

    public void setClassB(ClassB classB) {
        this.classB = classB;
    }
    // Other methods
}

public class ClassB {
    private ClassA classA;

    public void setClassA(ClassA classA) {
        this.classA = classA;
    }
    // Other methods
}

在配置文件中,Spring容器可以如下实例化这两个类:

<bean id="classA" class="com.example.ClassA">
    <property name="classB" ref="classB" />
</bean>

<bean id="classB" class="com.example.ClassB">
    <property name="classA" ref="classA" />
</bean>

通过单例模式,Spring容器能够在创建ClassA时将ClassB的引用注入,同时创建ClassB时将ClassA的引用注入,这使得两个Bean之间的循环依赖得以解决。

 

3.提前曝露

Spring容器在处理循环依赖时,可以提前曝露尚未初始化的Bean引用。这使得Bean可以引用其他Bean的代理而不必等到它们完全初始化。

Spring使用CGLIB或JDK动态代理来创建代理Bean,这些代理Bean会在实际Bean初始化之前被注入。

mikechen

mikechen睿哥,10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复架构即可获取《阿里架构师进阶专题全部合集》,后台回复面试即可获取《史上最全阿里Java面试题总结

评论交流
    说说你的看法