购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 Spring循环依赖案例分析

在介绍三种不同的依赖注入类型时,我们引出了使用Spring IoC容器时的一个常见问题,即循环依赖。同时也明确了在单例作用域下,Setter方法注入能够解决循环依赖问题,而构造器注入则不能。你可能好奇在这背后Spring具体的实现过程,作为一个专题,本节也会对这个话题做深入分析。

对于单例作用域来说,Spring容器在整个生命周期内,有且只有一个Bean对象,所以很容易想到这个对象应该存在于缓存中。Spring为了解决单例Bean的循环依赖问题,使用了三级缓存。这是Spring在设计和实现上的一大特色,也是开发人员在面试过程中经常遇到的话题。

2.3.1 三级缓存结构

所谓的三级缓存,在Spring中表现为三个Map对象,如代码清单2-17所示。这三个Map对象定义在DefaultSingletonBeanRegistry类中,该类是DefaultListableBeanFactory的父类。

代码清单2-17 DefaultSingletonBeanRegistry中的三级缓存Map定义代码

/** 单例对象的缓存: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** 单例对象工厂的缓存: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** 提前暴露的单例对象的缓存: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

请注意,这里的singletonObjects变量就是第一级缓存,用来持有完整的Bean实例。而earlySingletonObjects中存放的是那些提前暴露的对象,也就是已经创建但还没有完成属性注入的对象,属于第二级缓存。最后的singletonFactories存放用来创建earlySingleton-Objects的工厂对象,属于第三级缓存。

那么,三级缓存是如何发挥作用的呢?让我们来分析获取Bean的代码流程,如代码清单2-18所示。

代码清单2-18 获取Bean的getSingleton()方法代码

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    //首先从一级缓存singletonObjects中获取
    Object singletonObject = this.singletonObjects.get(beanName);

    //如果获取不到,就从二级缓存earlySingletonObjects中获取
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);

            //如果还是获取不到,就从三级缓存singletonFactory中获取
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();

                    //一旦获取成功,就把对象从第三级缓存移动到第二级缓存中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

看了这段代码,我们不难理解对三级缓存的依次访问过程,但可能还是不理解Spring为什么要这样设计。事实上,解决循环依赖的关键还是要围绕Bean的生命周期。在2.2.2节中介绍Bean的实例化时,我们知道它包含三个核心步骤,而在第一步和第二步之间,存在一个addSingletonFactory()方法,如代码清单2-19所示。

代码清单2-19 Bean实例化过程中的addSingletonFactory()方法代码

//1. 初始化Bean,通过构造函数创建Bean
instanceWrapper = createBeanInstance(beanName, mbd, args);

//针对循环依赖问题暴露单例工厂类
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

//2. 初始化Bean实例,完成Bean实例的完整创建
populateBean(beanName, mbd, instanceWrapper);

Spring解决循环依赖的诀窍就在于singletonFactories这个第三级缓存,上述addSingleton-Factory()方法用于初始化这个第三级缓存中的数据,如代码清单2-20所示。

代码清单2-20 addSingletonFactory()方法代码

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            //添加Bean到第三级缓存中
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

请注意,这段代码的执行时机是在已经通过构造函数创建Bean,但还没有完成对Bean中完整属性的注入的时候。换句话说,Bean已经可以被暴露出来进行识别了,但还不能正常使用。接下来我们就来分析一下为什么通过这种机制就能解决循环依赖问题。

2.3.2 循环依赖解决方案

让我们回顾2.1.3节中基于Setter方法注入的循环依赖场景,如代码清单2-21所示。

代码清单2-21 基于Setter方法注入的循环依赖代码

public class ClassA {
    private ClassB classB;

    @Autowired
    public void setClassB(ClassB classB) {
        this.classB = classB;
    }
}

public class ClassB {
    private ClassA classA;

    @Autowired
    public void setClassA(ClassA classA) {
        this.classA = classA;
    }
}

现在假设我们先初始化ClassA。ClassA首先通过createBeanInstance()方法创建了实例,并且将这个实例提前暴露到第三级缓存singletonFactories中。然后,ClassA尝试通过populateBean()方法注入属性,发现自己依赖ClassB这个属性,就会尝试去获取ClassB的实例。

显然,这时候ClassB还没有被创建,所以要走创建流程。ClassB在初始化第一步的时候发现自己依赖了ClassA,就会尝试从第一级缓存singletonObjects去获取ClassA的实例。因为ClassA这时候还没有被创建完毕,所以它在第一级缓存和第二级缓存中都不存在。当尝试访问第三级缓存时,因为ClassA已经提前暴露了,所以ClassB能够通过singletonFactories拿到ClassA对象并顺利完成所有初始化流程。

ClassB对象创建完成之后会被放到第一级缓存中,这时候ClassA就能从第一级缓存中获取ClassB的实例,进而完成ClassA的所有初始化流程。这样ClassA和ClassB都能够成功完成创建过程,整个流程如图2-3所示。

049-1

图2-3 基于Setter方法注入的循环依赖解决流程

讲到这里,相信你也理解了为什么构造器注入无法解决循环依赖问题。这是因为构造器注入过程是发生在Bean初始化的第一个步骤createBeanInstance()中,而这个步骤还没有调用addSingletonFactory()方法完成第三级缓存的构建,自然也就无法从该缓存中获取目标对象。

2.3.3 消除循环依赖案例分析

在本节中,我们将基于日常开发需求,通过一个具体的案例来介绍组件之间循环依赖的产生过程以及解决方案。

这个案例描述了医疗健康类系统中的一个常见场景,每个用户都有一份健康档案,存储着代表用户当前健康状况的健康等级以及一系列的健康任务。用户每天可以通过完成医生所指定的任务来获取一定的健康积分,而这个积分的计算过程取决于该用户当前的健康等级。也就是说,不同的健康等级下完成同一个任务所能获取的积分也是不一样的。反过来,等级的计算也取决于该用户当前需要完成的任务数量,任务越多说明用户越不健康,其健康等级也就越低。健康档案和健康任务之间的关联关系如图2-4所示。

049-2

图2-4 健康档案和健康任务之间的关联关系

针对这个场景,我们可以抽象出两个类,一个是代表健康档案的HealthRecord类,一个是代表健康任务的HealthTask类。我们先来看HealthRecord类,这个类包含一个HealthTask列表以及添加HealthTask的方法,同样也包含一个获取健康等级的方法,这个方法根据任务数量来判断健康等级,如代码清单2-22所示。

代码清单2-22 HealthRecord类实现代码

public class HealthRecord {
    private List<HealthTask> tasks = new ArrayList<HealthTask>();

    public Integer getHealthLevel() {
        //根据健康任务数量来判断健康等级
        //任务越多说明越不健康,健康等级就越低
        if(tasks.size() > 5) {
            return 1;
        }
        if(tasks.size() < 2) {
            return 3;
        }
        return 2;
    }

    public void addTask(String taskName, Integer initialHealthPoint) {
        HealthTask task = new HealthTask(this, taskName, initialHealthPoint);
        tasks.add(task);
    }

    public List<HealthTask> getTasks() {
        return tasks;
    }
}

对应的HealthTask中显然应该包含对HealthRecord的引用,同时也实现了一个方法来计算该任务所能获取的积分,这时候就需要使用到HealthRecord中的等级信息,如代码清单2-23所示。

代码清单2-23 HealthTask类实现代码

public class HealthTask {
    private HealthRecord record;
    private String taskName;
    private Integer initialHealthPoint;

    public HealthTask(HealthRecord record, String taskName, Integer initialHealthPoint) {
        this.record = record;
        this.taskName = taskName;
        this.initialHealthPoint = initialHealthPoint;
    }

    public Integer calculateHealthPointForTask() {
        //计算该任务所能获取的积分需要健康等级信息
        //健康等级越低积分越高,以鼓励用户多做任务
        Integer healthPointFromHealthLevel = 12 / record.getHealthLevel();
        //最终积分为初始积分加上与健康等级相关的积分
        return initialHealthPoint + healthPointFromHealthLevel;
    }

    public String getTaskName() {
        return taskName;
    }

    public int getInitialHealthPoint() {
        return initialHealthPoint;
    }
}

从代码中,我们不难看出HealthRecord和HealthTask之间存在明显的相互依赖关系。

那么,如何消除循环依赖?软件行业有一句很经典的话,即当我们碰到问题无从下手时,不妨考虑一下是否可以通过“加一层”的方法进行解决。消除循环依赖的基本思路也是这样,就是通过在两个相互循环依赖的组件之间添加中间层,变循环依赖为间接依赖。有三种方法可以做到这一点,分别是提取中介者、转移业务逻辑和引入回调。

1. 提取中介者

我们先来看第一种方法:提取中介者。提取中介者的核心思想是把两个相互依赖的组件中的交互部分抽象出来形成一个新的组件,而新组件同时包含着对原有两个组件的引用,这样就把循环依赖关系剥离出来并提取到一个专门的中介者组件中,如图2-5所示。

051-1

图2-5 提取中介者之后的类图

这个中介者组件的实现也非常简单,通过提供一个计算积分的方法来对循环依赖进行了剥离,该方法同时依赖于HealthRecord和HealthTask对象,并实现了原有HealthTask中根据HealthRecord的等级信息进行积分计算的业务逻辑。中介者HealthPointMediator类的实现代码如代码清单2-24所示。

代码清单2-24 HealthPointMediator类实现代码

public class HealthPointMediator {
    private HealthRecord record;

    public HealthPointMediator(HealthRecord record) {
        this.record = record;
    }

    public Integer calculateHealthPointForTask(HealthTask task) {
        Integer healthLevel = record.getHealthLevel();
        Integer initialHealthPoint = task.getInitialHealthPoint();
        Integer healthPoint = 12 / healthLevel + initialHealthPoint;
        return healthPoint;
    }
}
2. 转移业务逻辑

我们继续介绍第二种消除循环依赖的方法,这就是转移业务逻辑。这种方法的实现思路在于提取一个专门的业务组件来完成对等级的计算过程。这样,HealthTask原本对HealthRecord的依赖就转移到了对这个业务组件的依赖,而这个业务组件本身不需要依赖任何对象,如图2-6所示。

052-1

图2-6 转移业务逻辑之后的类图

图2-6中的专门负责处理业务逻辑的HealthLevelHandler类的实现代码也很简单,如代码清单2-25所示。

代码清单2-25 HealthLevelHandler类实现代码

public class HealthLevelHandler {
    private Integer taskCount;

    public HealthLevelHandler(Integer taskCount) {
        this.taskCount = taskCount;
    }

    public Integer getHealthLevel() {
        if(taskCount > 5) {
            return 1;
        }
        if(taskCount < 2) {
            return 3;
        }
        return 2;
    }
}
3. 引入回调

介绍完了提取中介者和转移业务逻辑这两种方法之后,我们来看最后一种消除循环依赖的方法,这种方法会采用回调接口。所谓回调,本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于计算健康等级的业务接口,然后让HealthRecord去实现这个接口。我们同样将这个接口命名为HealthLevelHandler,其中包含一个计算健康等级的方法定义。这样,HealthTask在计算积分时只需要依赖这个业务接口,而不需要关心这个接口的具体实现类,如图2-7所示。

053-1

图2-7 引入回调之后的类图

由于篇幅有限,完整的消除循环依赖案例代码可以在以下GitHub地址下载:https://github.com/tianminzheng/acyclic-relationships-demo。 9dPAZDuMQsZWfYxxHYfeZ4bnU2BasE0KVKbVCwMBtosxTrNUkabqL11pD/eO/ktN

点击中间区域
呼出菜单
上一章
目录
下一章
×