01 spring循环依赖
作者:Mythsman原文:https://blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/转载自:猿天地1 什么是依赖
原文来自:https://mp.weixin.qq.com/s/pIawXWHCGqIHi0INHDgawQ
其实,不分场景地、笼统地说A依赖B其实是不够准确、至少是不够细致的。我们可以简单定义一下什么是依赖。
所谓A依赖B,可以理解为A中某些功能的实现是需要调用B中的其他功能配合实现的。这里也可以拆分为两层含义:
A强依赖B。创建A的实例这件事情本身需要B来参加。对照在现实生活就像妈妈生你一样。
A弱依赖B。创建A的实例这件事情不需要B来参加,但是A实现功能是需要调用B的方法。对照在现实生活就像男耕女织一样。
那么,所谓循环依赖,其实也有两层含义:
强依赖之间的循环依赖。
弱依赖之间的循环依赖。
2 什么是依赖调解
对于强依赖而言,A和B不能互相作为存在的前提,否则宇宙就爆炸了。因此这类依赖目前是无法调解的。
对于弱依赖而言,A和B的存在并没有前提关系,A和B只是互相合作。因此正常情况下是不会出现违反因果律的问题的。
那什么是循环依赖的调解呢?我的理解是:
将 原本是弱依赖关系的两者误当做是强依赖关系的做法 重新改回弱依赖关系的过程。
基于上面的分析,我们基本上也就知道Spring是怎么进行循环依赖调解的了(仅指弱依赖,强依赖的循环依赖只有上帝能自动调解)。
3 为什么要依赖注入
网上经常看到很多手撸IOC容器的入门科普文,大部分人只是将IOC容器实现成一个“存储Bean的map”,将DI实现成“通过注解+反射将bean赋给类中的field”。实际上很多人都忽视了DI的依赖调解的功能。而帮助我们进行依赖调解本身就是我们使用IOC+DI的一个重要原因。
在没有依赖注入的年代里,很多人都会将类之间的依赖通过构造函数传递(实际上是构成了强依赖)。当项目越来越庞大时,非常容易出现无法调解的循环依赖。这时候开发人员就被迫必须进行重新抽象,非常麻烦。而事实上,我们之所以将原本的弱依赖弄成了强依赖,完全是因为我们将类的构造、类的配置、类的初始化逻辑三个功能耦合在构造函数之中。
而DI就是帮我们将构造函数的功能进行了解耦。
那么Spring是怎么进行解耦的呢?
4 spring的依赖注入模型
这一部分网上有很多相关内容,我的理解大概是上面提到的三步:
类的构造,调用构造函数、解析强依赖(一般是无参构造),并创建类实例。
类的配置,根据Field/GetterSetter中的依赖注入相关注解、解析弱依赖,并填充所有需要注入的类。
类的初始化逻辑,调用生命周期中的初始化方法(例如
@PostConstruct注解或InitializingBean的afterPropertiesSet方法),执行实际的初始化业务逻辑。
这样,构造函数的功能就由原来的三个弱化为了一个,只负责类的构造。并将类的配置交由DI,将类的初始化逻辑交给生命周期。
想到这一层,忽然解决了我堵在心头已久的问题。在刚开始学Spring的时候,我一直想不通:
为什么Spring除了构造函数之外还要在Bean生命周期里有一个额外的初始化方法?
这个初始化方法和构造函数到底有什么区别?
为什么Spring建议将初始化的逻辑写在生命周期里的初始化方法里?
现在,把依赖调解结合起来看,解释就十分清楚了:
为了进行依赖调解,Spring在调用构造函数时是没有将依赖注入进来的。也就是说构造函数中是无法使用通过DI注入进来的bean(或许可以,但是Spring并不保证这一点)。
如果不在构造函数中使用依赖注入的bean而仅仅使用构造函数中的参数,虽然没有问题,但是这就导致了这个bean强依赖于他的入参bean。当后续出现循环依赖时无法进行调解。
5 非典型问题 5.1 结论
根据上面的分析我们应该得到了以下共识:
通过构造函数传递依赖的做法是有可能造成无法自动调解的循环依赖的。
纯粹通过Field/GetterSetter进行依赖注入造成的循环依赖是完全可以被自动调解的。
因此这样我就得到了一个我认为正确的结论。这个结论屡试不爽,直到我发现了这次遇到的场景:
在Spring中对Bean进行依赖注入时,在纯粹只考虑循环依赖的情况下,只要不使用构造函数注入就永远不会产生无法调解的循环依赖。
当然,我没有任何“不建议使用构造器注入”的意思。相反,我认为能够“优雅地、不引入循环依赖地使用构造器注入”是一个要求更高的、更优雅的做法。贯彻这一做法需要有更高的抽象能力,并且会自然而然的使得各个功能解耦合。
5.2 问题
将实际遇到的问题简化后大概是下面的样子(下面的类在同一个包中):
@SpringBootApplication
@Import({ServiceA.class, ConfigurationA.class, BeanB.class})
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
public class ServiceA {
@Autowired
private BeanA beanA;
@Autowired
private BeanB beanB;
}
public class ConfigurationA {
@Autowired
public BeanB beanB;
@Bean
public BeanA beanA() {
return new BeanA();
}
}
public class BeanA {
}
public class BeanB {
@Autowired
public BeanA beanA;
}
首先声明一点,我没有用@Component、@Configuration之类的注解,而是采用@Import手动扫描Bean是为了方便指定Bean的初始化顺序。Spring会按照我@Import的顺序依次加载Bean。同时,在加载每个Bean的时候,如果这个Bean有需要注入的依赖,则会试图加载他依赖的Bean。
简单梳理一下,整个依赖链大概是这样:

我们可以发现,BeanA,BeanB,ConfigurationA之间有一个循环依赖,不过莫慌,所有的依赖都是通过非构造函数注入的方式实现的,理论上似乎可以自动调解的。
但是实际上,这段代码会报下面的错:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'beanA': Requested bean is currently in creation: Is there an unresolvable circular reference?
这显然是出现了Spring无法调解的循环依赖了。
这已经有点奇怪了。但是,如果你尝试将ServiceA类中声明的BeanA,BeanB调换一下位置,你就会发现这段代码突然就跑的通了!!!
显然,调换这两个Bean的依赖的顺序本质是调整了Spring加载Bean的顺序(众所周知,Spring创建Bean是单线程的)。
5.3 解释
相信你已经发现问题了,没错,问题的症结就在于ConfigurationA这个配置类。
配置类和普通的Bean有一个区别,就在于除了同样作为Bean被管理之外,配置类也可以在内部声明其他的Bean。
这样就存在一个问题,配置类中声明的其他Bean的构造过程其实是属于配置类的业务逻辑的一部分的。也就是说我们只有先将配置类的依赖全部满足之后才可以创建他自己声明的其他的Bean。(如果不加这个限制,那么在创建自己声明的其他Bean的时候,如果用到了自己的依赖,则有空指针的风险。)
这样一来,BeanA对ConfigurationA就不再是弱依赖,而是实打实的强依赖了(也就是说ConfigurationA的初始化不仅影响了BeanA的依赖填充,也影响了BeanA的实例构造)。
有了这样的认识,我们再来分别分析两种初始化的路径。
5.4 先加载BeanA
当Spring在试图加载ServiceA时,先构造了ServiceA,然后发现他依赖BeanA,于是就试图去加载BeanA;
Spring想构造BeanA,但是发现BeanA在ConfigurationA内部,于是又试图加载ConfigurationA(此时BeanA仍未构造);
Spring构造了ConfigurationA的实例,然后发现他依赖BeanB,于是就试图去加载BeanB。
Spring构造了BeanB的实例,然后发现他依赖BeanA,于是就试图去加载BeanA。
Spring发现BeanA还没有实例化,此时Spring发现自己回到了步骤2。。。GG。。。
5.5 先加载BeanB
当Spring在试图加载ServiceA时,先构造了ServiceA,然后发现他依赖BeanB,于是就试图去加载BeanB;
Spring构造了BeanB的实例,然后发现他依赖BeanA,于是就试图去加载BeanA。
Spring发现BeanA在ConfigurationA内部,于是试图加载ConfigurationA(此时BeanA仍未构造);
Spring构造了ConfigurationA的实例,然后发现他依赖BeanB,并且BeanB的实例已经有了,于是将这个依赖填充进ConfigurationA中。
Spring发现ConfigurationA已经完成了构造、填充了依赖,于是想起来构造了BeanA。
Spring发现BeanA已经有了实例,于是将他给了BeanB,BeanB填充的依赖完成。
Spring回到了为ServiceA填充依赖的过程,发现还依赖BeanA,于是将BeanA填充给了ServiceA。
Spring成功完成了初始化操作。
5.6 结论
总结一下这个问题,结论就是:
除了构造注入会导致强依赖以外,一个Bean也会强依赖于暴露他的配置类。
5.7 代码坏味道
写到这,我已经觉得有点恶心了。谁在写代码的时候没事做还要这么分析依赖,太容易出锅了吧!那到底有没有什么方法能避免分析这种恶心的问题呢?
方法其实是有的,那就是遵守下面的代码规范————不要对有@Configuration注解的配置类进行Field级的依赖注入。
没错,对配置类进行依赖注入,几乎等价于对配置类中的所有Bean增加了一个强依赖,极大的提高了出现无法调解的循环依赖的风险。我们应当将依赖尽可能的缩小,所有依赖只能由真正需要的Bean直接依赖才行。
01 spring循环依赖的更多相关文章
- spring循环依赖问题分析
新搞了一个单点登录的项目,用的cas,要把源码的cas-webapp改造成适合我们业务场景的项目,于是新加了一些spring的配置文件. 但是在项目启动时报错了,错误日志如下: 一月 , :: 下午 ...
- Spring 循环依赖
循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环.此处不 ...
- Springboot源码分析之Spring循环依赖揭秘
摘要: 若你是一个有经验的程序员,那你在开发中必然碰到过这种现象:事务不生效.或许刚说到这,有的小伙伴就会大惊失色了.Spring不是解决了循环依赖问题吗,它是怎么又会发生循环依赖的呢?,接下来就让我 ...
- Spring 循环依赖的三种方式(三级缓存解决Set循环依赖问题)
本篇文章解决以下问题: [1] . Spring循环依赖指的是什么? [2] . Spring能解决哪种情况的循环依赖?不能解决哪种情况? [3] . Spring能解决的循环依赖原理(三级缓存) 一 ...
- Spring循环依赖的解决
## Spring循环依赖的解决 ### 什么是循环依赖 循环依赖,是依赖关系形成了一个圆环.比如:A对象有一个属性B,那么这时候我们称之为A依赖B,如果这时候B对象里面有一个属性A.那么这时候A和B ...
- 这个 Spring 循环依赖的坑,90% 以上的人都不知道
1. 前言 这两天工作遇到了一个挺有意思的Spring循环依赖的问题,但是这个和以往遇到的循环依赖问题都不太一样,隐藏的相当隐蔽,网络上也很少看到有其他人遇到类似的问题.这里权且称他非典型Spring ...
- Spring — 循环依赖
读完这篇文章你将会收获到 Spring 循环依赖可以分为哪两种 Spring 如何解决 setter 循环依赖 Spring 为何是三级缓存 , 二级不行 ? Spring 为啥不能解决构造器循环依赖 ...
- 帮助你更好的理解Spring循环依赖
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
- spring 循环依赖的一次 理解
前言: 在看spring 循环依赖的问题中,知道原理,网上一堆的资料有讲原理. 但今天在看代码过程中,又产生了疑问. 疑问点如下: // 疑问点: 先进行 dependon 判断String[] de ...
随机推荐
- 自定义、操作cookie
/** * 读取所有cookie * 注意二.从客户端读取Cookie时,包括maxAge在内的其他属性都是不可读的,也不会被提交.浏览器提交Cookie时只会提交name与value属性.maxAg ...
- linux命令截取文件最后n行(所有命令)
linux命令截取文件最后n行(所有命令) tail -n a.txt > b.txt 联想:系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) una ...
- JavaScript 的执行机制
一.关于javascript javascript是一门单线程语言,在最新的HTML5中提出了Web Worker,但javascript是单线程这一核心仍未改变. 为什么js是单线程的语言?因为最初 ...
- day01-html
HTML概述: HTML: Hyper Text Markup Language 超文本标记语言 超文本: 比普通文本功能更加强大,可以添加各种样式 标记语言: 通过一组标签.来对内容进行描述. &l ...
- 二、MyBatis-HelloWorld
环境准备 1.创建数据库表 create table tbl_employee ( id ) primary key AUTO_INCREMENT comment "ID", la ...
- 覆盖(Override)和重写(Overload)的区别
Overload Overload是重载的意思. 重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同). Overload对我们来说可能比较 ...
- 如何在mac上使用iMessage发送信息
在Mac上你也可以像iPhone上一样使用iMessage 来发送iMessage 与 普通的短信息. 并且你需要在iPhone上设置中的信息的信息转发中激活对电脑的支持.此时, 你的电脑也可以向你的 ...
- Android9.0特性
这篇文章,是Android官方文档的中文版本. 注意事项(AndroidP 特性): (1),android.os.Build.VERSION.RELEASE ,需要当做字符串类型处理. (2),依赖 ...
- php接受post传值的方法
这段时间在研究php的接口,利用jmeter模拟发送数据给php服务器,看php如何接收post传输过去的数据,遇到了几个问题,经过一番度娘之后终于有所理解,记录一下: 这里记录常用的两种post方式 ...
- POJ 2502 Subway ( 最短路 && 最短路建图 )
题意 : 给出二维平面上的两个点代表起点以及终点,接下来给出若干条地铁线路,除了在地铁线路上行进的速度为 40km/h 其余的点到点间都只能用过步行且其速度为 10km/h ,现问你从起点到终点的最短 ...