什么是循环依赖
循环依赖是指在软件系统中的两个或多个模块、类、或对象之间,形成了相互依赖关系的情况,而这种依赖关系形成一个循环闭环。
如下图所示:
A依赖于B、B依赖于A,就是多个bean之间相互依赖,形成了一个闭环,这就是循环依赖。
循环依赖的危害
循环依赖在软件开发中可能会引发多种危害,包括但不限于以下几个方面:
- 难以维护和理解:循环依赖增加了代码的复杂性,开发人员需要跟踪多个类之间的相互引用关系,使得代码难以理解和维护。
- 编译问题:循环依赖可能导致编译器无法正确解析模块或类之间的依赖关系,从而引发编译错误。
- 运行时问题:在程序运行时,循环依赖可能导致死锁,或无限递归调用,从而导致程序崩溃或性能问题。
- 性能问题:循环依赖可能导致不必要的性能开销,因为系统可能会频繁地执行不必要的递归调用或循环操作。
- 不良设计:循环依赖通常是设计不佳的标志,它可能表明系统的模块划分和架构需要重新审视。好的软件设计应该尽量避免或减少循环依赖。
Spring如何解决循环依赖
Spring框架解决循环依赖的方式主要依赖于三种机制:构造器注入、单例模式、和提前曝露。
1.构造器注入
Spring容器可以通过使用构造器注入来解决循环依赖问题,构造器注入是一种依赖注入方式,允许Bean通过构造函数接受依赖,从而在Bean的创建阶段解决循环依赖。
假设有两个类,ClassA
和 ClassB
,它们彼此相互依赖:
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容器能够正确解决循环依赖问题,确保ClassA
和ClassB
都正确地初始化。
构造器注入原理:
- 当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。
假设有两个类,ClassA
和 ClassB
,它们彼此相互依赖:
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面试题总结》