浅谈循环依赖

循环依赖或多或少地都会充斥在我们开发的进程中,或许我们借助了框架的力量化解掉其中可能发生的问题,但到底是如何解决的可能大家会觉得太复杂而避而不谈。实际上循环依赖处理起来是非常简单的,下面笔者将会把这些都摆上台面逐帧进行分析。

概述

在软件工程中,循环依赖指模块间的属性存在相互依赖的关系。如果存在循环依赖可能会导致以下的不良结果:

  • 导致相互依赖的模块紧耦合,从而降低各自模块的复用性。
  • 导致模块间的多米诺效应,一个很小的改动引发其他模块甚至全局的不可用,例如导致无限循环或其他不可预期的异常发生。
  • 导致内存泄露,阻止垃圾收集器回收无用对象(使用引用计数法的垃圾收集器)。

因此,我们应该遵循循环依赖原则(ADP),即在包或者组件的依赖图谱中不应该存在循环。

循环依赖是一种反模式(anti-pattern)。

如何避免

传统模式下,如果我们通过内建的方式创建依赖属性,则很容易导致循环依赖的发生,从而在创建对象时发生死循环。例如,

java 复制代码
public class Component1 { 
    private Component2 component2;

    public Component1() {
        this.component2 = new Component2();
    }
}

public class Component2 {
    private Component1 component1;

    public Component2() {
        this.component1 = new Component1();
    }
}

public class Application {
    public static void main(String[] args) {
        // 无限循环导致栈溢出
        Component1 component1 = new Component1();
        // 无限循环导致栈溢出
        Component2 component2 = new Component2();
    }
}

上述代码中,对ComponentOne或者ComponentSecond的创建都会导致无限循环的发生。

一般,我们可以通过依赖反转(DIP)和依赖注入(DI)很大程度地避免循环依赖地发生。例如,

java 复制代码
public interface IComponent1 {
}

public interface IComponent2 {
}

public class Component1 implements IComponent1 { 
    private IComponent2 component2;

    public Component1(IComponent2 component2) {
        this.component2 = component2;
    }
}

public class Component2 implements IComponent2 {
    private IComponent1 component1;

    public Component2(IComponent1 component1) {
        this.component1 = component1;
    }
}

public class Application {
    public static void main(String[] args) {
        // compiler error,无法通过构造方法创建出Component1
        IComponent1 component1 = new Component1(...);
        // compiler error,无法通过构造方法创建出Component2
        IComponent2 component2 = new Component2(...);
    }
}

这样,我们在构造对象时就能及时发现循环依赖的存在,而不是运行时错误。

如何解决

对于已存在循环依赖的情况,我们应该如何正确地构建对象呢?如果要在程序中正确的构建具有循环依赖的对象,则需要提前将尚未初始化属性的对象提前暴露出去。例如,

java 复制代码
public class Component1 { 
    private Component2 component2;

    public Component1(){
    }

    public void setComponent2(Component2 component2) {
        this.component2 = component2;
    }
}

public class Component2 {
    private Component1 component1;

    public Component2(){
    }

    public void setComponent1(Component1 component1) {
        this.component1 = component1;
    }
}

public class Application {
    public static void main(String[] args) {
        // 提前将尚未初始化属性的Component1对象提前暴露出去
        Component1 component1 = new Component1();
        // 提前将尚未初始化属性的Component2对象提前暴露出去
        Component2 component2 = new Component2();
        // 初始化属性
        component1.setComponent2(component2);
        component2.setComponent1(component1);
    }
}

然而,对于体量较大的项目使用这种方式效率却是有点低,所以一些应用层框架(例如Spring)常会将控制反转(IoC)结合依赖注入(DI)进行使用。在控制反转(IoC)的作用下,框架首先会构造一个类似于控制器的对象,并将控制权转交给控制器。然后,控制器会先实例化应用对象中的依赖,并通过依赖注入(DI)传递到应用对象中。

以Spring为例,我们先来看看它是如何使用控制反转(IoC)和依赖注入(DI)。

通过将Spring的@Autowired注解声明于构造方法上,我们即可轻松实现对其的依赖注入(DI),例如:

java 复制代码
@Component
public class Component1 { 

    private Component2 component2;

    @Autowired
    public Component1(Component2 component2){
        this.component2 = component2;
    }
}

@Component
public class Component2 {
    
    private Component1 component1;

    @Autowired
    public Component2(Component1 component1) {
        this.component1 = component1;
    }
}

显然地,通过构造方法注入是可以产生出循环依赖的。而如果想用正确的姿势使用含循环依赖的类,可通过setter的方式进行注入,例如:

java 复制代码
@Component
public class Component1 { 
 
    private Component2 component2;

    @Autowired
    public void setComponent2(Component2 component2){
        this.component2 = component2;
    }
}

@Component
public class Component2 {
    
    private Component1 component1;

    @Autowired
    public void setComponent1(Component1 component1){
        this.component1 = component1;
    }
}

或者,使用更加简洁的注入方式,即直接在属性字段上声明@Autowired,例如:

java 复制代码
@Component
public class Component1 { 
    @Autowired
    private Component2 component2;
}

@Component
public class Component2 {
    @Autowired
    private Component1 component1;
}

这样,我们就能顺利的实例化具有循环依赖的类了。

当然,循环依赖被定性为一种反模式,不推荐使用。在发生循环依赖时,我们应该更多地思考我们的模块划分是否合理,如何去避免它的发生,而不是如何去解决它。

抛开循环依赖的争议,我们看一下在Spring中是如何解决循环依赖的(仅考虑单例模式下的bean)。

首先,我们可以看到用于实例化bean的doGetBean方法,在doGetBean方法中Spring会完成bean的创建、属性填充和初始化,具体代码如下所示:

java 复制代码
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws ... {

    String beanName = ...;

    Object beanInstance;

    // Eagerly check singleton cache for manually registered singletons.
    Object sharedInstance = getSingleton(beanName, true);
    if (sharedInstance != null && args == null) {
        // ...
        beanInstance = getObjectForBeanInstance(sharedInstance, ...);
    }

    else {

        // ...
        
        RootBeanDefinition mbd = ...; 

        // ...

        // Create bean instance.
        sharedInstance = getSingleton(beanName, () -> {
            // 假设没有异常
            return createBean(beanName, mbd, args);
        });
        beanInstance = getObjectForBeanInstance(sharedInstance, ...);
        
    }

    return adaptBeanInstance(name, beanInstance, requiredType);
}

在上述代码中,我们可以看到在真正执行bean的创建前会先通过getSingleton(String, boolean)方法在缓存中查找出对应的bean实例,在查找失败的情况下再使用getSingleton(String, ObjectFactory)方法创建对应的bean实例。为了便于后续的分析,下面我们先来看看getSingleton(String, boolean)方法的执行逻辑(为了便于理解,笔者对getSingleton(String, boolean)方法进行了大量的精简)。

java 复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
        singletonObject = this.earlySingletonObjects.get(beanName);
    }
    return singletonObject;
}

具体执行流程如下所示:

  1. 在singletonObjects缓存中查询是否存在创建完成的bean实例,若存在立即返回,否则执行第2步。
  2. 在earlySingletonObjects缓存中查询是否存在早期创建的bean实例,若存在立即返回,否则返回null。

显然地,在首次通过doGetBean方法实例化bean是不会在getSingleton(String, boolean)方法中查询到缓存实例的,那么我们就进入到getSingleton(String, ObjectFactory)方法进行bean实例的创建了(为了便于理解,笔者对getSingleton(String, ObjectFactory)方法进行了大量的精简)。

java 复制代码
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {
        singletonObject = singletonFactory.getObject();
        addSingleton(beanName, singletonObject);
    }
    return singletonObject;
}

protected void addSingleton(String beanName, Object singletonObject) {
    this.singletonObjects.put(beanName, singletonObject);
    this.earlySingletonObjects.remove(beanName);
}

具体执行流程如下所示:

  1. 在singletonObjects缓存中查询是否存在创建完成的bean实例,若存在立即返回,否则执行第2步。
  2. 执行ObjectFactory#getObject创建bean实例,即执行createBean方法创建bean实例。
  3. 将创建完成的bean实例加入到singletonObjects缓存中,同时将它从earlySingletonObjects缓存中移除。

同样地,在首次创建时singletonObjects缓存必然不会存在对应的缓存实例,那么我们再进入到createBean方法中(为了便于理解,笔者对createBean方法和doCreateBean方法进行了大量的精简)。

java 复制代码
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
        throws BeanCreationException {

    RootBeanDefinition mbdToUse = mbd;

    //...

    Object beanInstance = doCreateBean(beanName, mbdToUse, args);
    return beanInstance;
}

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {

    // 1. 执行`raw bean`实例的创建
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // ...       

    // 2. 将`raw bean`实例提前添加到earlySingletonObjects缓存中
    addEarlySingletonObjects(beanName, bean);

    Object exposedObject = bean;
    // 3. 执行`raw bean`实例的属性填充
    populateBean(beanName, mbd, instanceWrapper);
    // 4. 执行`raw bean`实例的初始化
    exposedObject = initializeBean(beanName, exposedObject, mbd);
    
    // ...

    return exposedObject;
}

具体执行流程如下所示:

  1. 执行raw bean实例的创建
  2. 将raw bean实例添加到earlySingletonObjects缓存中
  3. 执行raw bean实例的属性填充
  4. 执行raw bean实例的初始化

需要注意,在第2步中"将raw bean实例添加到earlySingletonObjects缓存中"是笔者在某种场景下简化原有逻辑转化而来的,实际上Spring在此处会先构建出创建earlySingletonObject实例的工厂实例并将其添加到工厂实例的缓存中,然后在循环依赖发生时再次执行getSingleton(String, boolean)方法触发earlySingletonObject实例的创建并将其添加到earlySingletonObjects缓存中。这可能与当前笔者给出的代码和执行流程有所出入,但在大部分场景下它们最终所生成的bean实例是相同的。

在上述流程中我们可以看到Spring会在第2步中将早期创建的bean提前添加到earlySingletonObjects缓存中,那么在执行到第3步属性填充时如果发生了循环依赖再次通过doGetBean方法请求当前bean时会在getSingleton(String, boolean)方法的earlySingletonObjects缓存中获取到这个bean实例,这样就终止了循环依赖过程中无限创建bean实例的循环。

至此,我们看到了完整的Spring循环依赖解决方案。不难发现,这本质上就是我们上文解决循环依赖所提出的方案,即提前将尚未初始化属性的对象提前暴露出去。

关于Spring循环依赖的问题,如果读者想更进一步地了解整个处理流程可以阅读笔者之前的一篇博客《用6w字的篇幅向你全方位解析Spring IoC》。

总结

总的来说,无论形态如何发生变化,本质上最终对循环依赖的解决方案都是相同的,即提前将尚未初始化属性的对象提前暴露出去。

参考

相关推荐
金銀銅鐵20 分钟前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
福建佰胜张工1 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop1 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x1 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事2 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客2 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1883 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn3 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su3 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123455 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法