1、案发情况

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

这两段代码中定义了两个Service类:TestService1TestService2,在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不是号称能解决循环依赖问题吗,怎么还会出现?」

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

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

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

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

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

2.什么是循环依赖?

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

3.循环依赖的N种场景

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

单例的setter注入

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

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

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

spring内部有三级缓存:

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

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

图1

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

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

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

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Autowiredprivate TestService3 testService3;public void test1() {}
}
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}
@Service
public class 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)
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {@Autowiredprivate TestService1 testService1;public void test2() {}
}

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

为什么呢?

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

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

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

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

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

@Service
public class TestService3 {@Autowiredprivate TestService1 testService1;
}

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

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

果然出现了循环依赖。

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

构造器注入

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

@Service
public class TestService1 {public TestService1(TestService2 testService2) {}
}
@Service
public class TestService2 {public TestService2(TestService1 testService1) {}
}

运行结果:

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

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

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

单例的代理对象setter注入

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

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

@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
@Service
public class 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,其他的都不变。

@Service
publicclass 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")
@Service
public class TestService1 {@Autowiredprivate TestService2 testService2;public void test1() {}
}
@DependsOn(value = "testService1")
@Service
public class 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注解解决。

Spring是如何解决循环依赖的?相关推荐

  1. Spring 是如何解决循环依赖的?

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

  2. 互相引用 spring_听说你还不知道Spring是如何解决循环依赖问题的?

    作者:Vt 前言 Spring如何解决的循环依赖,是近两年流行起来的一道Java面试题. 其实笔者本人对这类框架源码题还是持一定的怀疑态度的. 如果笔者作为面试官,可能会问一些诸如"如果注入 ...

  3. Spring是如何解决循环依赖的

    在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的.这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够 ...

  4. 万字长文带你吃透Spring是怎样解决循环依赖的

    在Spring框架中,处理循环依赖一直是一个备受关注的话题.这是因为Spring源代码中为了解决循环依赖问题,进行了大量的处理和优化.同时,循环依赖也是Spring高级面试中的必考问题,回答得好可以成 ...

  5. Spring三级缓存解决循环依赖问题详解

    spring三级缓存解决循环依赖问题详解 前言 这段时间阅读了spring IOC部分的源码.在学习过程中,自己有遇到过很多很问题,在上网查阅资料的时候,发现很难找到一份比较全面的解答.现在自己刚学习 ...

  6. Spring的getBean解决循环依赖

    Spring是如何解决循环依赖的? 通过三级缓存提前暴露对象解决的. 三级缓存存放了哪些对象信息? 一级缓存存放的是完整对象. 二级缓存存放的是那些属性还没赋值的对象. 三级缓存存放的是ObjectF ...

  7. Spring IOC 如何解决循环依赖?

    前言 假设对象A.B 之间相互依赖,Spring IOC是如何解决A.B两个对象的实例化的?答案是三级缓存. 三级缓存 SpringIOC 通过三级缓存来解决循环依赖问题,三级缓存指的是三个Map: ...

  8. 框架源码专题:Spring是如何解决循环依赖的?

    文章目录 1.什么是循环依赖? 2.解决循环依赖思路 3. 使用了三级缓存还有什么问题?怎么解决的? 4. 手写伪代码解决缓存依赖 5. 二级缓存能否解决循环依赖,三级缓存存在的意义 6. Sprin ...

  9. Spring三级缓存解决循环依赖

    1. 前言 循环依赖:就是N个类循环(嵌套)引用. 通俗的讲就是N个Bean互相引用对方,最终形成闭环.用一副经典的图示可以表示成这样(A.B.C都代表对象,虚线代表引用关系): 其实可以N=1,也就 ...

最新文章

  1. java工程中的.classpathaaaaaaaaaaaaaaaa转载
  2. 单片机红外通信c语言,基于C语言的计算机与多单片机红外无线串口通信的实现.doc...
  3. Old ST-LINK firmware detected.do you want to upgrade it?已解决,stlink升级
  4. torch.nn.Embedding()的固定化
  5. C++11特性补充笔记1
  6. 人生苦短,请用 Chrome!
  7. centos7上mycat安装_Mysql+Mycat实现数据库主从同步与读写分离
  8. 超级详细的iptables介绍
  9. 修炼系列(八),你真的会写注释吗
  10. 日记侠:知识付费创富没赶上,看别人赚钱你急不急?
  11. aspose实现word,excel在线预览
  12. 共克时艰|链下思考系列之一区块链能做点什么
  13. 中国移动光宽带 光猫 超级用户管理员账号密码
  14. 11.0_[Java 继承]-继承/重写/抽象类/抽象方法/ final 修饰符
  15. Lytro发布光场影片,让你感受真正的沉浸式视频体验
  16. platform设备驱动简介
  17. C#--浮点数取小数点后两位和保留两位
  18. SDWAN的技术流派:纯软型、资源型、设备型、广域网优化型
  19. Ruby on rails 实战圣经:Part 1: 入门导览-Ruby on Rails 简介
  20. 网站推广的29种常用方法

热门文章

  1. 数据湖:用以分析客户数据的一种更好的方式
  2. 继承中构造和析构顺序
  3. C语言的组成及一些基本术语概念
  4. java计费模块_学校管理软件的计费模块(费用)[关闭]
  5. 在cmd中输入javac不行_cmd运行java可以javac不行
  6. 解决slideUp/slideDown...动画反复执行问题
  7. Django框架(十一):用户认证组件
  8. 2021年育婴员(五级)考试及育婴员(五级)最新解析
  9. chm是什么格式文件?怎么打开
  10. CAD二次开发环境配置