写点什么

Spring 循环依赖案例分析:三级缓存结构 + 循环依赖解决方案

  • 2023-06-16
    湖南
  • 本文字数:4825 字

    阅读完需:约 16 分钟

在介绍三种不同的依赖注入类型时,我们引出了使用 Spring IoC 容器时的一个常见问题,即循环依赖。

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


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

三级缓存结构

所谓的三级缓存,在 Spring 中表现为三个 Map 对象。这三个 Map 对象定义在 DefaultSingletonBeanRegistry 类中,该类是 DefaultListableBeanFactory 的父类。


如下代码所示:

/** 单例对象的缓存: 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 的代码流程,如下所示:

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 的生命周期。


在介绍 Bean 的实例化时,我们知道它包含三个核心步骤,而在第一步和第二步之间,存在一个 addSingletonFactory()方法,如下所示:

//1. 初始化Bean,通过构造函数创建BeaninstanceWrapper = createBeanInstance(beanName, mbd, args);//针对循环依赖问题暴露单例工厂类addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName,mbd, bean));//2. 初始化Bean实例,完成Bean实例的完整创建populateBean(beanName, mbd, instanceWrapper);
复制代码

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

protected void addSingletonFactory(String beanName, ObjectFactory<?>singletonFactory) {	Assert.notNull(singletonFactory, "Singleton factory must not benull");	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.1.3 节中基于 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 都能够成功完成创建过程,整个流程如下图所示:

讲到这里,相信你也理解了为什么构造器注入无法解决循环依赖问题。


这是因为构造器注入过程是发生在 Bean 初始化的第一个步骤 createBeanInstance()中,而这个步骤还没有调用 addSingletonFactory()方法完成第三级缓存的构建,自然也就无法从该缓存中获取目标对象。

消除循环依赖案例分析

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


这个案例描述了医疗健康类系统中的一个常见场景,每个用户都有一份健康档案,存储着代表用户当前健康状况的健康等级以及一系列的健康任务。用户每天可以通过完成医生所指定的任务来获取一定的健康积分,而这个积分的计算过程取决于该用户当前的健康等级。


也就是说,不同的健康等级下完成同一个任务所能获取的积分也是不一样的。反过来,等级的计算也取决于该用户当前需要完成的任务数量,任务越多说明用户越不健康,其健康等级也就越低。健康档案和健康任务之间的关联关系如下图所示:

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

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 中的等级信息,如下所示:

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. 提取中介者

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

这个中介者组件的实现也非常简单,通过提供一个计算积分的方法来对循环依赖进行了剥离,该方法同时依赖于 HealthRecord 和 HealthTask 对象,并实现了原有 HealthTask 中根据 HealthRecord 的等级信息进行积分计算的业务逻辑。中介者 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 的依赖就转移到了对这个业务组件的依赖,而这个业务组件本身不需要依赖任何对象,如图所示:

图中的专门负责处理业务逻辑的 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 在计算积分时只需要依赖这个业务接口,而不需要关心这个接口的具体实现类,如图所示:


用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
Spring循环依赖案例分析:三级缓存结构+循环依赖解决方案_互联网架构师小马_InfoQ写作社区