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

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

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

@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是如何解决循环依赖的:

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

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

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

@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的流程如图:

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

这样不是有问题?

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

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

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

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

答案就在AbstractAutowireCapableBeanFactorydoCreateBean方法的这段代码中:

它定义了一个匿名内部类,通过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注解解决。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

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是如何解决循环依赖的?

    1.案发情况 @Service public class TestService1 {@Autowiredprivate TestService2 testService2;@Asyncpublic ...

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

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

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

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

最新文章

  1. 在线直播 | 是事实还是贩卖焦虑?IT行业也偏爱“小鲜肉”​
  2. Solaris10 for x86网卡替换配置
  3. Android NDK的文件夹
  4. strcpy函数_错误更正(拷贝赋值函数的正确使用姿势)
  5. 扩增子和宏基因组数据分析流程和可视化方案—刘永鑫(南京,2020年10月27日)
  6. 基础才是重中之重~如何整理BLL与DAL层的文件
  7. hive查看表中列的信息命令_走近大数据之Hive入门(四、Hive的管理)
  8. linux内核装载vfs过程
  9. Linux实训项目——第八章:配置网络接口及远程管理
  10. python下批量修改图片格式和大小
  11. http+json 格式的接口开发实践与思考
  12. [UE5] 部署 Rider For Unreal Engine 编辑器
  13. 用u盘进不了pe计算机意外地,u盘装系统启动不了无法进入pe怎么办
  14. YARN动态资源池配置案例
  15. Windows 服务(1) (from MSDN)
  16. python实现翻转金字塔图案
  17. Java程序员的职业规划(五年)
  18. JAVA最新中国手机号段匹配
  19. 王者s19服务器维护,王者S19丨4个必须知道的调整!最后一个不知没法玩!
  20. comma是什么键(trailing comma是什么意思)

热门文章

  1. java工程如何跑起来的_你编写的Java代码是咋跑起来的?
  2. android 桌面视频播放器,ZY-Player ,一款跨平台桌面端视频资源播放器
  3. java integer == int_Java中int和Integer的区别详解
  4. python后端接口怎么写_看看人家那后端API接口写得,那叫一个优雅!
  5. 计组之中央处理器:5、微程序控制器(组成、原理、概念对比)
  6. 计算机网络之网络层:10、IP组播、IGMP协议、组播路由选择协议
  7. 【专栏必读】王道考研408数据结构万字笔记(有了它不需要你再做笔记了):各章节内容概述导航和思维导图
  8. (王道408考研操作系统)第二章进程管理-第四节1:死锁相关概念
  9. 使用栈解决的一类经典问题:表达式转换及求值;中缀表达式;前缀表达式,后缀表达式,中缀转前缀;中缀转后缀;后缀表达式求值;波兰式,逆波兰式
  10. Python爬虫之pyppeteer去除Chrome正受到自动测试软件的控制(反爬策略)