• 生活小妙招免费各类生活中的小问题知识以及音乐简谱等,是你了解世界未知知识的好地方。

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

十万个为什么 空空 2024-3-26 10:43:36 7次浏览

关于问题Spring 如何解决循环依赖的问题?一共有 4 位热心网友为你解答:

【1】、来自网友【苏三说技术】的最佳回答:

1.由同事抛的一个问题开始

最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到。平时自认为对 spring 循环依赖问题还是比较了解的,直到遇到这个和后面的几个问题后,重新刷新了我的认识。

我们先看看当时出问题的代码片段:

@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}}

@Servicepublicclass TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}

这两段代码中定义了两个 Service 类:TestService1 和 TestService2,在 TestService1 中注入了 TestService2 的实例,同时在 TestService2 中注入了 TestService1 的实例,这里构成了循环依赖。

只不过,这不是普通的循环依赖,因为 TestService1 的 test1 方法上加了一个@Async 注解。

大家猜猜程序启动后运行结果会怎样?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘testService1‘: Bean with name ‘testService1‘ has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching – consider using ‘getBeanNamesOfType‘ with the ‘allowEagerInit‘ flag turned off, for example.

报错了。。。原因是出现了循环依赖。

「不科学呀,spring 不是号称能解决循环依赖问题吗,怎么还会出现?」

如果把上面的代码稍微调整一下:

@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}}

把 TestService1 的 test1 方法上的@Async 注解去掉,TestService1 和 TestService2 都需要注入对方的实例,同样构成了循环依赖。

但是重新启动项目,发现它能够正常运行。这又是为什么?

带着这两个问题,让我们一起开始 spring 循环依赖的探秘之旅。

2.什么是循环依赖?

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

第一种情况:自己依赖自己的直接依赖

第二种情况:两个对象之间的直接依赖

第三种情况:多个对象之间的间接依赖

前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。

3.循环依赖的 N 种场景

spring 中出现循环依赖主要有以下场景:

单例的 setter 注入

这种注入方式应该是 spring 用的最多的,代码如下:

@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}}

@Servicepublicclass TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}

这是一个经典的循环依赖,但是它能正常运行,得益于 spring 的内部机制,让我们根本无法感知它有问题,因为 spring 默默帮我们解决了。

spring 内部有三级缓存:

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的 bean 实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的 bean 实例
  • singletonFactories 三级缓存,用于保存 bean 创建工厂,以便于后面扩展有机会创建代理对象。

下面用一张图告诉你,spring 是如何解决循环依赖的:

图 1

细心的朋友可能会发现在这种场景中第二级缓存作用不大。

那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;@Autowiredprivate TestService3 testService3;public void test1() {}}

@Servicepublicclass TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}

@Servicepublicclass TestService3 {@Autowiredprivate TestService1 testService1;public void test3() {}}

TestService1 依赖于 TestService2 和 TestService3,而 TestService2 依赖于 TestService1,同时 TestService3 也依赖于 TestService1。

按照上图的流程可以把 TestService1 注入到 TestService2,并且 TestService1 的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1 注入到 TestService3 的流程如图:

图 2

TestService1 注入到 TestService3 又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是 ObjectFactory 对象。说白了,两次从三级缓存中获取都是 ObjectFactory 对象,而通过它创建的实例对象每次可能都不一样的。

这样不是有问题?

为了解决这个问题,spring 引入的第二级缓存。上面图 1 其实 TestService1 对象的实例已经被添加到第二级缓存中了,而在 TestService1 注入到 TestService3 时,只用从第二级缓存中获取该对象即可。

图 3

还有个问题,第三级缓存中为什么要添加 ObjectFactory 对象,直接保存实例对象不行吗?

答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。

针对这种场景 spring 是怎么做的呢?

答案就在 AbstractAutowireCapableBeanFactory 类 doCreateBean 方法的这段代码中:

它定义了一个匿名内部类,通过 getEarlyBeanReference 方法获取代理对象,其实底层是通过 AbstractAutoProxyCreator 类的 getEarlyBeanReference 生成代理对象。

多例的 setter 注入

这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}}

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@Servicepublicclass TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}

很多人说这种情况 spring 容器启动会报错,其实是不对的,我非常负责任的告诉你程序能够正常启动。

为什么呢?

其实在 AbstractApplicationContext 类的 refresh 方法中告诉了我们答案,它会调用 finishBeanFactoryInitialization 方法,该方法的作用是为了 spring 容器启动的时候提前初始化一些 bean。该方法的内部又调用了 preInstantiateSingletons 方法

标红的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始 bean。

而多例即 SCOPE_PROTOTYPE 类型的类,非单例,不会被提前初始化 bean,所以程序能够正常启动。

如何让他提前初始化 bean 呢?

只需要再定义一个单例的类,在它里面注入 TestService1

@Servicepublicclass TestService3 {@Autowiredprivate TestService1 testService1;}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

构造器注入

这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,看看如下代码:

@Servicepublicclass TestService1 {public TestService1(TestService2 testService2) {}}

@Servicepublicclass TestService2 {public TestService2(TestService1 testService1) {}}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?

从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

单例的代理对象 setter 注入

这种注入方式其实也比较常用,比如平时使用:@Async 注解的场景,会通过 AOP 自动生成代理对象。

我那位同事的问题也是这种情况。

@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}}

@Servicepublicclass TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}

从前面得知程序启动会报错,出现了循环依赖:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘testService1‘: Bean with name ‘testService1‘ has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching – consider using ‘getBeanNamesOfType‘ with the ‘allowEagerInit‘ flag turned off, for example.

为什么会循环依赖呢?

答案就在下面这张图中:

说白了,bean 初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:

那位同事的问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异常。

如果这时候把 TestService1 改个名字,改成:TestService6,其他的都不变。

@Servicepublicclass TestService6 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}}

再重新启动一下程序,神奇般的好了。

what? 这又是为什么?

这就要从 spring 的 bean 加载顺序说起了,默认情况下,spring 是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以 TestService1 比 TestService2 先加载,而改了文件名称之后,TestService2 比 TestService6 先加载。

为什么 TestService2 比 TestService6 先加载就没问题呢?

答案在下面这张图中:

这种情况 testService6 中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

DependsOn 循环依赖

还有一种有些特殊的场景,比如我们需要在实例化 Bean A 之前,先实例化 Bean B,这个时候就可以使用@DependsOn 注解。

@DependsOn(value = “testService2”)@Servicepublicclass TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}}

@DependsOn(value = “testService1”)@Servicepublicclass TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}}

程序启动之后,执行结果:

Circular depends-on relationship between ‘testService2‘ and ‘testService1‘

这个例子中本来如果 TestService1 和 TestService2 都没有加@DependsOn 注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?

答案在 AbstractBeanFactory 类的 doGetBean 方法的这段代码中:

它会检查 dependsOn 的实例有没有循环依赖,如果有循环依赖则抛异常。

4.出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是 spring 默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:

生成代理对象产生的循环依赖

这类循环依赖问题解决方法很多,主要有:

  1. 使用@Lazy 注解,延迟加载
  2. 使用@DependsOn 注解,指定加载先后关系
  3. 修改文件名称,改变循环依赖类的加载顺序

使用@DependsOn 产生的循环依赖

这类循环依赖问题要找到@DependsOn 注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

多例循环依赖

这类循环依赖问题可以通过把 bean 改成单例的解决。

构造器循环依赖

这类循环依赖问题可以通过使用@Lazy 注解解决。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多 BAT 大厂的前辈交流和学习。

【2】、来自网友【算法金融】的最佳回答:

Spring 是通过先创建对象,将对象放在缓存中,再进行属性设置的。比如 A,B 互相依赖,先创建 A 对象,放在缓存中,设置属性时发现依赖 B,这时候初始化 B,设置 B 的属性,发现依赖 A,缓存中有 A 的引用,虽然还没有初始化完全。B 初始化完成后,A 就可以拿到 B 了。这仅仅是解决 set 依赖,如果是构造器依赖就解决不了了。

随便说一句,一般架构设计的时候都是上层调下层,同层和下层调上层都不应该出现,可以考虑下设计是不是有问题。

希望对你有帮助,如果有用记得点个赞哦,也可以关注一下我,会分享一些技术类文章。

【3】、来自网友【老熊程序员】的最佳回答:

(一)Spring IOC 容器—对象循环依赖

1. 什么是循环依赖? what?

(1)循环依赖–>循环引用。—>即 2 个或以上 bean 互相持有对方,最终形成闭环。

eg:A 依赖 B,B 依赖 C,C 又依赖 A。【注意:这里不是函数的循环调用【是个死循环,除非有终结条件】,是对象相互依赖关系】

2. Spring 中循环依赖的场景?where?

①:构造器的循环依赖。【这个 Spring 解决不了】

StudentA 有参构造是 StudentB。StudentB 的有参构造是 StudentC,StudentC 的有参构造是 StudentA ,这样就产生了一个循环依赖的情况,

我们都把这三个 Bean 交给 Spring 管理,并用有参构造实例化

public class StudentA {

private StudentB studentB ;

public void setStudentB(StudentB studentB) {

this.studentB = studentB;

}

public StudentA() {

}

public StudentA(StudentB studentB) {

this.studentB = studentB;

}

}

[java] view plain copy

public class StudentB {

private StudentC studentC ;

public void setStudentC(StudentC studentC) {

this.studentC = studentC;

}

public StudentB() {

}

public StudentB(StudentC studentC) {

this.studentC = studentC;

}

}

[java] view plain copy

public class StudentC {

private StudentA studentA ;

public void setStudentA(StudentA studentA) {

this.studentA = studentA;

}

public StudentC() {

}

public StudentC(StudentA studentA) {

this.studentA = studentA;

}

}

[html] view plain copy

<bean id=”a” class=”com.zfx.student.StudentA”>

<constructor-arg index=”0″ ref=”b”></constructor-arg>

</bean>

<bean id=”b” class=”com.zfx.student.StudentB”>

<constructor-arg index=”0″ ref=”c”></constructor-arg>

</bean>

<bean id=”c” class=”com.zfx.student.StudentC”>

<constructor-arg index=”0″ ref=”a”></constructor-arg>

</bean>

下面是测试类:

[java] view plain copy

public class Test {

public static void main(String[] args) {

ApplicationContext context = new ClassPathXmlApplicationContext(“com/zfx/student/applicationContext.xml”);

//System.out.println(context.getBean(“a”, StudentA.class));

}

}

执行结果报错信息为:

[java] view plain copy

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:

Error creating bean with name ‘a‘: Requested bean is currently in creation: Is there an unresolvable circular reference?

②【setter 循环依赖】field 属性的循环依赖【setter 方式 单例,默认方式–>通过递归方法找出当前 Bean 所依赖的 Bean,然后提前缓存【会放入 Cach 中】起来。通过提前暴露 –>暴露一个 exposedObject 用于返回提前暴露的 Bean。】

前两步骤得知:Spring 是先将 Bean 对象实例化【依赖无参构造函数】—>再设置对象属性的

这就不会报错了:

原因:Spring 先用构造器实例化 Bean 对象—–>将实例化结束的对象放到一个 Map 中,并且 Spring 提供获取这个未设置属性的实例化对象的引用方法。结合我们的实例来看,,当 Spring 实例化了 StudentA、StudentB、StudentC 后,紧接着会去设置对象的属性,此时 StudentA 依赖 StudentB,就会去 Map 中取出存在里面的单例 StudentB 对象,以此类推,不会出来循环的问题喽。

3. 如何检测是否有循环依赖?how to find?

可以 Bean 在创建的时候给其打个标记,如果递归调用回来发现正在创建中的话—>即可说明循环依赖。

4.怎么解决的? todo what?

Spring 的循环依赖的理论依据其实是基于 Java 的引用传递,当我们获取到对象的引用时,对象的 field 或 zh 属性是可以延后设置的(但是构造器必须是在获取引用之前)。

Spring 的单例对象的初始化主要分为三步:

①:createBeanInstance:实例化,其实也就是 调用对象的构造方法实例化对象

②:populateBean:填充属性,这一步主要是多 bean 的依赖属性进行填充

③:initializeBean:调用 spring xml 中的 init() 方法。

从上面讲述的单例 bean 初始化步骤我们可以知道,循环依赖主要发生在第一、第二步。也就是构造器循环依赖和 field 循环依赖。

那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在 Spring 容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在 Cache 中,Spring 为了解决单例的循环依赖问题,使用了三级缓存。

调整配置文件,将构造函数注入方式改为 属性注入方式 即可

3.源码怎么实现的? how?

(1)三级缓存源码主要 指:

/** Cache of singleton objects: bean name –> bean instance */

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name –> ObjectFactory */

private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name –> bean instance */

private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

这三级缓存分别指:

singletonFactories : 单例对象工厂的 cache

earlySingletonObjects :提前暴光的单例对象的 Cache 。【用于检测循环引用,与 singletonFactories 互斥】

singletonObjects:单例对象的 cache

我们在创建 bean 的时候,首先想到的是从 cache 中获取这个单例的 bean,这个缓存就是 singletonObjects。主要调用方法就就是:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {

Object singletonObject = this.singletonObjects.get(beanName);

if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {

synchronized (this.singletonObjects) {

singletonObject = this.earlySingletonObjects.get(beanName);

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 != NULL_OBJECT ? singletonObject : null);

}

上面的代码需要解释两个参数:

isSingletonCurrentlyInCreation()判断当前单例 bean 是否正在创建中,也就是没有初始化完成(比如 A 的构造器依赖了 B 对象所以得先去创建 B 对象, 或则在 A 的 populateBean 过程中依赖了 B 对象,得先去创建 B 对象,这时的 A 就是处于创建中的状态。)

allowEarlyReference 是否允许从 singletonFactories 中通过 getObject 拿到对象

分析 getSingleton()的整个过程,Spring 首先从一级缓存 singletonObjects 中获取。如果获取不到,并且对象正在创建中,就再从二级缓存 earlySingletonObjects 中获取。如果还是获取不到且允许 singletonFactories 通过 getObject()获取,就从三级缓存 singletonFactory.getObject()(三级缓存)获取,如果获取到了则:

this.earlySingletonObjects.put(beanName, singletonObject);

this.singletonFactories.remove(beanName);

从 singletonFactories 中移除,并放入 earlySingletonObjects 中。其实也就是从三级缓存移动到了二级缓存。

从上面三级缓存的分析,我们可以知道,Spring 解决循环依赖的诀窍就在于 singletonFactories 这个三级 cache。这个 cache 的类型是 ObjectFactory,定义如下:

public interface ObjectFactory<T> {

T getObject() throws BeansException;

}

这个接口在下面被引用

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {

Assert.notNull(singletonFactory, “Singleton factory must not be null”);

synchronized (this.singletonObjects) {

if (!this.singletonObjects.containsKey(beanName)) {

this.singletonFactories.put(beanName, singletonFactory);

this.earlySingletonObjects.remove(beanName);

this.registeredSingletons.add(beanName);

}

}

}

这里就是解决循环依赖的关键,这段代码发生在 createBeanInstance 之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以 Spring 此时将这个对象提前曝光出来让大家认识,让大家使用。

这样做有什么好处呢?让我们来分析一下“A 的某个 field 或者 setter 依赖了 B 的实例对象,同时 B 的某个 field 或者 setter 依赖了 A 的实例对象”这种循环依赖的情况。A 首先完成了初始化的第一步,并且将自己提前曝光到 singletonFactories 中,此时进行初始化的第二步,发现自己依赖对象 B,此时就尝试去 get(B),发现 B 还没有被 create,所以走 create 流程,B 在初始化第一步的时候发现自己依赖了对象 A,于是尝试 get(A),尝试一级缓存 singletonObjects(肯定没有,因为 A 还没初始化完全),尝试二级缓存 earlySingletonObjects(也没有),尝试三级缓存 singletonFactories,由于 A 通过 ObjectFactory 将自己提前曝光了,所以 B 能够通过 ObjectFactory.getObject 拿到 A 对象(虽然 A 还没有初始化完全,但是总比没有好呀),B 拿到 A 对象后顺利完成了初始化阶段 1、2、3,完全初始化之后将自己放入到一级缓存 singletonObjects 中。此时返回 A 中,A 此时能拿到 B 的对象顺利完成自己的初始化阶段 2、3,最终 A 也完成了初始化,进去了一级缓存 singletonObjects 中,而且更加幸运的是,由于 B 拿到了 A 的对象引用,所以 B 现在 hold 住的 A 对象完成了初始化。

知道了这个原理时候,肯定就知道为啥 Spring 不能解决“A 的构造方法中依赖了 B 的实例对象,同时 B 的构造方法中依赖了 A 的实例对象”这类问题了!因为加入 singletonFactories 三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决

【4】、来自网友【大唐梦归】的最佳回答:

这个问题还有意义么

以上就是关于问题【Spring 如何解决循环依赖的问题?】的全部回答,希望能对大家有所帮助,内容收集于网络仅供参考,如要实行请慎重,任何后果与本站无关!

喜欢 (0)