循环依赖是指两个或多个组件之间相互依赖,形成一个闭环,从而导致这些组件无法正确地被初始化或加载。这种情况可能会在软件开发中引起问题,因为循环依赖会导致初始化顺序混乱,组件之间的关系变得复杂,甚至可能引发死锁或其他不稳定行为。
在编程中,循环依赖通常出现在模块、类、或者组件之间的相互引用上,导致编译、加载或初始化过程中的问题。在依赖注入(DI)框架中,循环依赖可能会导致对象的创建和初始化失败。
@Service
public class TestService1 {
@Autowired
private TestService2 testService2;
public void test1() {
}
}
@Service
public class TestService2 {
@Autowired
private TestService1 testService1;
public void test2() {
}
}
A 依赖于 B,而 B 也依赖于 A,形成了循环依赖。如果没有适当的处理,初始化这两个类的实例可能会导致问题。这段代码之所以能正常运行,是因为Spring内部机制,解决了循环依赖的问题。
Spring内部有三级缓存:
- singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
- earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
- singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。
Spring解决循环依赖步骤:
-
Bean 的注册:首先,Spring会解析并注册所有的Bean定义,但不会立即创建Bean的实例。
-
提前创建 Bean 实例:对于每个要创建的单例(
@Service
默认是单例作用域),Spring会提前创建一个半初始化的实例,并将其放入singletonFactories
缓存中。这是为了解决循环依赖问题。 -
开始创建 TestService1:当开始创建
TestService1
时,发现它依赖于TestService2
。因为TestService2
的半初始化实例已经在singletonFactories
缓存中,所以会使用该实例创建TestService1
,并在singletonFactories
缓存中移除TestService2
的工厂。 -
开始创建 TestService2:当创建
TestService2
时,发现它依赖于TestService1
,但由于TestService1
的半初始化实例已经存在,所以会使用该实例创建TestService2
,并在singletonFactories
缓存中移除TestService1
的工厂。 -
完全初始化:一旦创建过程完成,Spring会执行
TestService1
和TestService2
的初始化操作,包括注入和其他初始化方法。这将使它们变成完全初始化的Bean
图解:
循环依赖可能出现的场景
-
模块之间的相互引用:在模块化的软件设计中,不同模块之间可能会相互引用,特别是当模块之间存在交叉的功能需求或依赖时。如果模块之间的依赖关系没有正确管理,就可能产生循环依赖。
-
类的相互引用:在面向对象编程中,不同类之间可能会有相互引用,尤其是在它们之间存在双向的依赖关系时。如果类的构造函数或方法参数中出现了相互引用,就可能导致循环依赖问题。
-
依赖注入框架配置不当:依赖注入框架(如Spring)用于管理组件之间的依赖关系。如果配置文件中出现了循环依赖,框架可能无法正确初始化对象,导致异常。
-
事件和消息驱动的系统:在事件和消息驱动的系统中,不同组件可能通过事件或消息进行通信。如果事件的发起者和接收者之间出现相互依赖,就可能导致循环依赖。
-
单例模式的使用:当使用单例模式时,如果多个单例对象之间相互依赖,可能会形成循环依赖。单例对象在整个应用程序中只有一个实例,因此其依赖关系需要特别注意。
-
构建和初始化顺序问题:在某些情况下,对象的构建和初始化顺序可能导致循环依赖。如果某个对象在构建阶段需要引用另一个对象,而后者又需要在构建阶段引用前者,就可能形成循环依赖。
如何解决循环依赖
-
重构设计:重新审视组件之间的关系,尝试将循环依赖问题转化为单向的依赖关系。这可能需要重新划分模块或类的职责,以减少相互依赖。
-
引入接口或抽象类:通过引入接口或抽象类,可以将具体的依赖关系替换为更高层次的抽象依赖,从而解耦循环依赖。
-
延迟初始化:将对象的初始化推迟到实际使用它们的时候,以避免在初始化阶段出现循环依赖。这可以通过懒加载等方式来实现。
-
使用中介者模式:引入中介者或事件系统,将类之间的通信转移到中介者中,从而降低直接的循环依赖。这可以将相互依赖的关系集中在一个地方进行处理。
-
使用依赖注入容器:依赖注入框架(如Spring)可以处理循环依赖问题。这些框架使用一些特殊的策略,以确保对象的正确初始化顺序,从而解决循环依赖。
-
通过Setter注入或后处理器解决:在某些情况下,通过使用Setter方法注入依赖或使用依赖后处理器可以解决循环依赖问题。这样,对象的构建和初始化可以分为多个步骤。
-
更改对象创建时机:有时,将对象的创建从构造函数移至其他方法中,可以避免在构造函数阶段引发循环依赖。
-
设计模式:一些设计模式,如工厂方法模式、抽象工厂模式,可以用来分离对象的构建和初始化,从而避免循环依赖。