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

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

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

@Service
publicclass TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic void test1() {}
}
@Service
publicclass 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不是号称能解决循环依赖问题吗,怎么还会出现?」

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

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

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

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

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

2.什么是循环依赖?

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

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

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

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

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

3.循环依赖的N种场景

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

单例的setter注入

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

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

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

spring内部有三级缓存:

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例

  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例

  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

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

图1

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

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

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

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

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

为什么呢?

其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了preInstantiateSingletons方法 标红的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean。

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

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

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

@Service
publicclass TestService3 {@Autowiredprivate TestService1 testService1;
}

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

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

果然出现了循环依赖。

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

构造器注入

这种注入方式是spring4.x以上的版本中官方推荐的方式,具体如下代码:

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

运行结果:

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

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

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

单例的代理对象setter注入

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

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

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

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

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

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

这又是为什么?

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

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

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

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

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

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

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

使用@DependsOn产生的循环依赖

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

多例循环依赖

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

构造器循环依赖

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

当然最好的解决循环依赖问题最佳方案是从代码设计上规避,但是复杂的系统中有可能没法避免。

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

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

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

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

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

15张图带你彻底明白spring循环依赖,再也不用怕了相关推荐

  1. OMG!步步图解让你明白Spring循环依赖!看这一篇就够了

    前言 本份文档内容涵盖: Java.MyBatis.ZooKeeper.Dubbo.Elasticsearch.Memcached.Redis.MySQL. Spring.Spring Boot.Sp ...

  2. Java面试官:步步图解让你明白Spring循环依赖!看完这篇彻底明白了

    前言 不知道你们发现没有,在很多互联网公司基本上都是80后,90后居多,很少还有超过40岁的程序员.可能很多人心里都有一个疑问,那就是这些40多岁的程序员都干嘛去了呢?创业显然只是极少数的人,至于管理 ...

  3. Java开发常见面试题详解(LockSupport,AQS,Spring循环依赖,Redis)

    总览 问题 详解 String.intern()的作用 link LeetCode的Two Sum题 link 什么是可重入锁? link 谈谈LockSupport link 谈谈AQS link ...

  4. Java开发常见面试题详解(LockSupport,AQS,Spring循环依赖,Redis)_3

    Java开发常见面试题详解(LockSupport,AQS,Spring循环依赖,Redis)_3 总览 问题 详解 String.intern()的作用 link LeetCode的Two Sum题 ...

  5. 这个Spring循环依赖的坑,90%以上的人都不知道

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:Mythsman 原文:https://blog.myths ...

  6. 烂大街的Spring循环依赖该如何回答?

    什么是循环依赖? 从字面上来理解就是A依赖B的同时B也依赖了A,就像上面这样,或者C依赖与自己本身.体现到代码层次就是这个样子 @Component public class A {// A中注入了B ...

  7. spring处理循环依赖时序图_spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖...

    本次博客的目标 1. 手写spring循环依赖的整个过程 2. spring怎么解决循环依赖 3. 为什么要二级缓存和三级缓存 4. spring有没有解决构造函数的循环依赖 5. spring有没有 ...

  8. 这个 Spring 循环依赖的坑,90% 以上的人都不知道

    点击上方"后端技术精选",选择"置顶公众号" 技术文章第一时间送达! 作者:Mythsman blog.mythsman.com/post/5d838c7c2d ...

  9. Spring循环依赖的三种方式以及解决办法

    Spring循环依赖的三种方式以及解决办法 [转]https://www.cnblogs.com/liuqing576598117/p/11227007.html 示例 https://github. ...

  10. 聊透Spring循环依赖

    本文聊一下和依赖注入密切相关,并且在实际开发中很常见,面试也很喜欢问的一个问题:Spring是怎么解决循环依赖的?  之前就被问过Spring是怎么解决循环依赖的问题,当时年少无知,对Spring源码 ...

最新文章

  1. Rendering failed with a known bug ,Please try a rebuild
  2. shell脚本重启tomcat
  3. 内置h5 调用safari系统浏览器打开_开发教我做设计:移动端H5页面中的橡皮筋效果...
  4. 启动计算机引导win10,教您win10引导项丢失怎么办
  5. 五、Elasticsearch中的API的简单使用(Python版本)
  6. Flutter 15: 图解 ListView 不同样式 item 及 Widget 显隐性
  7. 如何将ListT转换相应的Html(xsl动态转换)(二)
  8. 这些面试中的智力题,你都会了吗
  9. Linux进阶之路———Shell 编程入门
  10. 简述oracle数据库特殊状态,【OracleDB】 01 概述和基本操作
  11. Redis集群如何安装
  12. socket tcp java_用JAVA写一个SOCKET 接收TCP发来的消息
  13. Spring MVC-页面重定向示例(转载实践)
  14. android扫码支付宝ofo,ofo等六大单车接入支付宝扫码,一半共享单车可分享支付宝流量...
  15. 基于Docker swarm 集群搭建SSR 学习
  16. Xilinx 7系列FPGA DDR3硬件设计规则
  17. 达芬奇密码 第三十章
  18. JS原生编写飞机大战小蜜蜂游戏
  19. 9个超好用的学习网站,都是充实课余知识的首选
  20. 实体店也可以代办?外卖市场还需严加监管

热门文章

  1. C/C++[codeup 1944]八进制
  2. 算法:回溯四 Combination Sum II组合总数II
  3. jsoup html to text,Jsoup和htmlunit结合使用。
  4. cmake和make区别
  5. 机器学习系列(19)_通用机器学习流程与问题解决架构模板
  6. 2018_08_10_生活记录_关于我和这个博客的说明
  7. indesign用于产品排班_2019年机器人行业十大新品盘点,过去一年最受关注的产品都在这...
  8. java csv api_CSVAPIforJava
  9. 简单的idea非maven项目引入jar包
  10. 进程外Session(用数据库)