Spring是如何解决循环依赖问题的

循环依赖是使用Spring IoC容器时的一个常见问题。在单例作用域下,Spring所提供的Setter方法注入能够解决循环依赖问题,而构造器注入则不能。理解循环依赖基本概念以及Spring解决循环依赖的底层实现原理对于我们更好的使用Spring框架有重要意义。

Spring框架所提供的了核心功能之一就是依赖注入(Dependency Injection,DI)。依赖注入可以说是开发人员使用Spring框架的基本手段,我们通过依赖注入获取所需的各种Bean。但在使用不同类型的依赖注入类型时,开发人员经常会碰到循环依赖问题,今天的内容就将围绕这一主题进行展开。为了深入分析循环依赖问题的解决方案,让我们先从Spring依赖注入的类型和循环依赖的基本概念开始讲起。

Spring依赖注入和循环依赖

我们知道,Spring为开发人员提供了三种不同的依赖注入类型,分别是字段注入、构造器注入和Setter方法注入。

其中,字段注入是三种注入方式中最常用、也是最容易使用的一种。但是,它也是三种注入方式中最应该避免使用的。字段注入的一大问题就是可能导致潜在的循环依赖。所谓循环依赖,就是两个类之间互相进行注入,例如如下所示的这段示例代码。

public class ClassA {

@Autowired

private ClassB classB;

}

public class ClassB {

@Autowired

private ClassA classA;

}

显然,这里的ClassA和ClassB发生了循环依赖。上述代码在Spring中是合法的,容器启动时并不会报任何错误,而只有在使用到具体某个ClassA或ClassB时才会报错。

事实上,Spring官方也不推荐开发人员使用字段注入这种注入模式,而是推荐了构造器注入。基于构造器注入,前面介绍的ClassA和ClassB之间的循环依赖关系是这样的。

public class ClassA {

private ClassB classB;

@Autowired

public ClassA(ClassB classB) {

this.classB = classB;

}

}

public class ClassB {

@Autowired

public ClassB(ClassA classA) {

this.classA = classA;

}

}

那么在Spring项目启动的时候,就会抛出一个循环依赖异常,从而提醒你避免循环依赖。

另一方面,Setter方法可以很好解决应用程序中的循环依赖问题,如下所示的代码是可以正确执行的。

public class ClassA {

private ClassB classB;

@Autowired

public void setClassB(ClassB classB) {

this.classB = classB;

}

}

public class ClassB {

@Autowired

public void setClassA(ClassA classA) {

this.classA = classA;

}

}

请注意,上述代码能够正确执行的前提是ClassA和ClassB的作用域都是"Singleton",即单例。所谓的单例,指的也就是说不管对Bean的引用有多少个,容器只会创建一个实例。

讲到这里,你可以回好奇,Setter方法注入是如何解决循环依赖问题的呢?这就需要剖析Spring中对于单例Bean的存储和获取方式,让我们一起来看一下。

Spring循环依赖解决方案

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

三级缓存结构

所谓的三级缓存,在Spring中表现为三个Map对象,定义在DefaultSingletonBeanRegistry类中,该类是DefaultListableBeanFactory的父类,如下所示。代码4。

/** 单例对象的缓存: 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存放用来创建earlySingletonObjects的工厂对象,属于第三级缓存。

那么三级缓存是如何发挥作用的呢?让我们来分析获取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;

}

上述代码中,我们首先从一级缓存singletonObjects中获取目前对象,如果获取不到,则从二级缓存earlySingletonObjects中获取;如果还是获取不到,就从三级缓存singletonFactory中获取。而一旦获取成功,就会对象从第三级缓存移动到第二级缓存中,从而为下一次获取对象做准备。

看了这段代码,我们了解了三级缓存的依次访问过程,但可能还是不理解Spring为什么要这样设计。事实上,解决循环依赖的关键点还是在要围绕Bean的生命周期。在前面介绍到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这个第三级缓存,上述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已经可以被暴露出来进行识别了,但还不能正常使用。接下来我们就来分析一下为什么通过这种机制就能解决循环依赖问题。

循环依赖解决方案

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

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

ClassB对象创建完成之后会放到第一级缓存中,这时候ClassA就能从第一级缓存中获取ClassB的实例,进而完成ClassA的所有初始化流程。

这样ClassA和ClassB都能够成功完成创建过程,整个流程如下所示,我们用红色部分表示ClassA的创建过程,用黄色部分表示ClassB的创建过程。

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

总结

今天的内容系统分析了Spring为开发人员提供的循环依赖解决方案。虽然基于Spring框架实现Bean的依赖注入比较简单,但也存在一些最佳实践。尤其是在使用Spring的过程中经常碰到的循环依赖问题,需要开发人员对该框架的底层原理有一定的了解。我们基于Spring提供的三层缓存机制对这一主题进行了源码级的深入分析。

相关推荐
神明不懂浪漫4 小时前
【第五章】Java中的继承与多态
java·开发语言
AI多Agent协作实战派6 小时前
AI多Agent协作系统实战(十七):凌晨4点,我的AI系统在“假装工作“——3个bug同时爆炸的5小时
java·前端·bug
gaolei_eit7 小时前
Java+Ai+vue
java·spring·maven
qq_2518364577 小时前
基于java Web 动漫视频网站毕业论文
java·开发语言·前端
LayZhangStrive7 小时前
JUC相关的函数、注解、变量杂记
java·面试·多线程·juc
未秃头的程序猿7 小时前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程
曹牧7 小时前
Eclipse 批量文本替换
java·ide·eclipse
程序员清风7 小时前
OpenAI官方发布最新提示词技巧!
java·后端·面试
一只枫林7 小时前
MySQL内、外连接知识点汇总
java·前端·数据库
梅头脑7 小时前
写了3年CRUD,volatile和synchronized的区别还是答不清——直到我画出了这张三性两锁边界图
java