点击上方“方志朋”,选择“设为星标”

回复”666“获取新整理的面试资料

作者:Mythsman

原文:https://blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/

转载自:猿天地

1

-

前言

这两天工作遇到了一个挺有意思的Spring循环依赖的问题,但是这个和以往遇到的循环依赖问题都不太一样,隐藏的相当隐蔽,网络上也很少看到有其他人遇到类似的问题。这里权且称他非典型Spring循环依赖问题。但是我相信我肯定不是第一个踩这个坑的,也一定不是最后一个,可能只是因为踩过的人比较少、鲜有记录罢了。因此这里权且记录一下这个坑,方便后人查看。

正如鲁迅(我)说过,“这个世上本没有坑,踩的人多了,也便成了坑”。

-

2

-

典型场景

经常听很多人在Review别人代码的时候有如下的评论:“你在设计的时候这些类之间怎么能有循环依赖呢?你这样会报错的!”。

其实这句话前半句当然没有错,出现循环依赖的确是设计上的问题,理论上应当将循环依赖进行分层,抽取公共部分,然后由各个功能类再去依赖公共部分。

但是在复杂代码中,各个manager类互相调用太多,总会一不小心出现一些类之间的循环依赖的问题。可有时候我们又发现在用Spring进行依赖注入时,虽然Bean之间有循环依赖,但是代码本身却大概率能很正常的work,似乎也没有任何bug。

很多敏感的同学心里肯定有些犯嘀咕,循环依赖这种触犯因果律的事情怎么能发生呢?没错,这一切其实都并不是那么理所当然。

-

3

-

什么是依赖

其实,不分场景地、笼统地说A依赖B其实是不够准确、至少是不够细致的。我们可以简单定义一下什么是依赖

所谓A依赖B,可以理解为A中某些功能的实现是需要调用B中的其他功能配合实现的。这里也可以拆分为两层含义:

  1. A强依赖B。创建A的实例这件事情本身需要B来参加。对照在现实生活就像妈妈生你一样。

  2. A弱依赖B。创建A的实例这件事情不需要B来参加,但是A实现功能是需要调用B的方法。对照在现实生活就像男耕女织一样。

那么,所谓循环依赖,其实也有两层含义:

  1. 强依赖之间的循环依赖。

  2. 弱依赖之间的循环依赖。

讲到这一层,我想大家应该知道我想说什么了。

-

4

-

什么是依赖调解

对于强依赖而言,A和B不能互相作为存在的前提,否则宇宙就爆炸了。因此这类依赖目前是无法调解的。

对于弱依赖而言,A和B的存在并没有前提关系,A和B只是互相合作。因此正常情况下是不会出现违反因果律的问题的。

那什么是循环依赖的调解呢?我的理解是:

将 原本是弱依赖关系的两者误当做是强依赖关系的做法 重新改回弱依赖关系的过程。

基于上面的分析,我们基本上也就知道Spring是怎么进行循环依赖调解的了(仅指弱依赖,强依赖的循环依赖只有上帝能自动调解)。

-

5

-

为什么要依赖注入

网上经常看到很多手撸IOC容器的入门科普文,大部分人只是将IOC容器实现成一个“存储Bean的map”,将DI实现成“通过注解+反射将bean赋给类中的field”。实际上很多人都忽视了DI的依赖调解的功能。而帮助我们进行依赖调解本身就是我们使用IOC+DI的一个重要原因。

在没有依赖注入的年代里,很多人都会将类之间的依赖通过构造函数传递(实际上是构成了强依赖)。当项目越来越庞大时,非常容易出现无法调解的循环依赖。这时候开发人员就被迫必须进行重新抽象,非常麻烦。而事实上,我们之所以将原本的弱依赖弄成了强依赖,完全是因为我们将类的构造类的配置类的初始化逻辑三个功能耦合在构造函数之中。

而DI就是帮我们将构造函数的功能进行了解耦。

那么Spring是怎么进行解耦的呢?

-

6

-

Spring的依赖注入模型

这一部分网上有很多相关内容,我的理解大概是上面提到的三步:

  1. 类的构造,调用构造函数、解析强依赖(一般是无参构造),并创建类实例。

  2. 类的配置,根据Field/GetterSetter中的依赖注入相关注解、解析弱依赖,并填充所有需要注入的类。

  3. 类的初始化逻辑,调用生命周期中的初始化方法(例如@PostConstruct注解或InitializingBeanafterPropertiesSet方法),执行实际的初始化业务逻辑。

这样,构造函数的功能就由原来的三个弱化为了一个,只负责类的构造。并将类的配置交由DI,将类的初始化逻辑交给生命周期。

想到这一层,忽然解决了我堵在心头已久的问题。在刚开始学Spring的时候,我一直想不通:

  • 为什么Spring除了构造函数之外还要在Bean生命周期里有一个额外的初始化方法?

  • 这个初始化方法和构造函数到底有什么区别?

  • 为什么Spring建议将初始化的逻辑写在生命周期里的初始化方法里?

现在,把依赖调解结合起来看,解释就十分清楚了:

  1. 为了进行依赖调解,Spring在调用构造函数时是没有将依赖注入进来的。也就是说构造函数中是无法使用通过DI注入进来的bean(或许可以,但是Spring并不保证这一点)。

  2. 如果不在构造函数中使用依赖注入的bean而仅仅使用构造函数中的参数,虽然没有问题,但是这就导致了这个bean强依赖于他的入参bean。当后续出现循环依赖时无法进行调解。

-

7

-

非典型问题

结论?

根据上面的分析我们应该得到了以下共识:

  • 通过构造函数传递依赖的做法是有可能造成无法自动调解的循环依赖的。

  • 纯粹通过Field/GetterSetter进行依赖注入造成的循环依赖是完全可以被自动调解的。

因此这样我就得到了一个我认为正确的结论。这个结论屡试不爽,直到我发现了这次遇到的场景:

在Spring中对Bean进行依赖注入时,在纯粹只考虑循环依赖的情况下,只要不使用构造函数注入就永远不会产生无法调解的循环依赖。

当然,我没有任何“不建议使用构造器注入”的意思。相反,我认为能够“优雅地、不引入循环依赖地使用构造器注入”是一个要求更高的、更优雅的做法。贯彻这一做法需要有更高的抽象能力,并且会自然而然的使得各个功能解耦合。

问题

将实际遇到的问题简化后大概是下面的样子(下面的类在同一个包中):

@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 {@Autowiredprivate BeanA beanA;@Autowiredprivate BeanB beanB;
}
public class ConfigurationA {@Autowiredpublic BeanB beanB;@Beanpublic BeanA beanA() {return new BeanA();}
}
public class BeanA {
}
public class BeanB {@Autowiredpublic 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是单线程的)。

解释

相信你已经发现问题了,没错,问题的症结就在于ConfigurationA这个配置类。

配置类和普通的Bean有一个区别,就在于除了同样作为Bean被管理之外,配置类也可以在内部声明其他的Bean。

这样就存在一个问题,配置类中声明的其他Bean的构造过程其实是属于配置类的业务逻辑的一部分的。也就是说我们只有先将配置类的依赖全部满足之后才可以创建他自己声明的其他的Bean。(如果不加这个限制,那么在创建自己声明的其他Bean的时候,如果用到了自己的依赖,则有空指针的风险。)

这样一来,BeanA对ConfigurationA就不再是弱依赖,而是实打实的强依赖了(也就是说ConfigurationA的初始化不仅影响了BeanA的依赖填充,也影响了BeanA的实例构造)。

有了这样的认识,我们再来分别分析两种初始化的路径。

先加载BeanA

  1. 当Spring在试图加载ServiceA时,先构造了ServiceA,然后发现他依赖BeanA,于是就试图去加载BeanA;

  2. Spring想构造BeanA,但是发现BeanA在ConfigurationA内部,于是又试图加载ConfigurationA(此时BeanA仍未构造);

  3. Spring构造了ConfigurationA的实例,然后发现他依赖BeanB,于是就试图去加载BeanB。

  4. Spring构造了BeanB的实例,然后发现他依赖BeanA,于是就试图去加载BeanA。

  5. Spring发现BeanA还没有实例化,此时Spring发现自己回到了步骤2。。。GG。。。

先加载BeanB

  1. 当Spring在试图加载ServiceA时,先构造了ServiceA,然后发现他依赖BeanB,于是就试图去加载BeanB;

  2. Spring构造了BeanB的实例,然后发现他依赖BeanA,于是就试图去加载BeanA。

  3. Spring发现BeanA在ConfigurationA内部,于是试图加载ConfigurationA(此时BeanA仍未构造);

  4. Spring构造了ConfigurationA的实例,然后发现他依赖BeanB,并且BeanB的实例已经有了,于是将这个依赖填充进ConfigurationA中。

  5. Spring发现ConfigurationA已经完成了构造、填充了依赖,于是想起来构造了BeanA。

  6. Spring发现BeanA已经有了实例,于是将他给了BeanB,BeanB填充的依赖完成。

  7. Spring回到了为ServiceA填充依赖的过程,发现还依赖BeanA,于是将BeanA填充给了ServiceA。

  8. Spring成功完成了初始化操作。

结论

总结一下这个问题,结论就是:

除了构造注入会导致强依赖以外,一个Bean也会强依赖于暴露他的配置类。

代码坏味道

写到这,我已经觉得有点恶心了。谁在写代码的时候没事做还要这么分析依赖,太容易出锅了吧!那到底有没有什么方法能避免分析这种恶心的问题呢?

方法其实是有的,那就是遵守下面的代码规范————不要对有@Configuration注解的配置类进行Field级的依赖注入

没错,对配置类进行依赖注入,几乎等价于对配置类中的所有Bean增加了一个强依赖,极大的提高了出现无法调解的循环依赖的风险。我们应当将依赖尽可能的缩小,所有依赖只能由真正需要的Bean直接依赖才行。

参考资料

Circular Dependencies in Spring

Spring-bean的循环依赖以及解决方式

Factory method injection should be used in "@Configuration" classes

热门内容:   

    

  • 看完知乎轮子哥的编程之路,我只想说,收下我的膝盖...

  • 这是我读过写得最好的【秒杀系统架构】分析与实战!

  • Springboot总结,核心功能,优缺点

  • 如何设计 API 接口,实现统一格式返回?

  • 阿里巴巴为什么能抗住90秒100亿?看完这篇你就明白了!

  • 日均 5 亿查询量的京东订单中心,为什么舍 MySQL 用 ES ?

  • 区块链入门教程

  • Redis 到底是怎么实现“附近的人”这个功能的呢?

  • Java 的 JSP 已经被淘汰了吗?

  • Java:如何更优雅的处理空值?

  • 为什么阿里巴巴要禁用Executors创建线程池?

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

这个Spring循环依赖的坑,90%以上的人都不知道相关推荐

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

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

  2. Spring循环依赖的三种方式,你都清楚吗?

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 来源:22j.co/bUdX 引言:循环依赖就是N个类中循环嵌套引用,如果 ...

  3. 一起来踩踩 Spring 中这个循环依赖的坑!

    作者:Mythsman blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/ 1. 前言 2. 典型场景 3. 什么是依赖 4. 什么是依赖调解 5. 为什 ...

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

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

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

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

  6. 【Spring源码:循环依赖】一文弄懂Spring循环依赖

    1. 什么是循坏依赖 很简单,其实就是互相依赖对方,比如,有一个A对象依赖了B对象,B对象又依赖了A对象. // A依赖了B public class A{private B b; }// B依赖了A ...

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

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

  8. 【spring容器启动】之bean的实例化和初始化(文末附:spring循环依赖原理)

    本次我们通过源码介绍ApplicationContext容器初始化流程,主要介绍容器内bean的实例化和初始化过程.ApplicationContext是Spring推出的先进Ioc容器,它继承了旧版 ...

  9. 终于有人把 Spring 循环依赖讲清楚了!

    网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...

最新文章

  1. 把ACL论文「几乎一字不落」抄到AAAI 2021上,作者回应:属借鉴
  2. ---Pcie基本概念普及(扫盲篇--巨适合新手)
  3. MATLAB从入门到精通-新增返回数组高、宽数字特征的全新方式
  4. 渗透技巧——利用netsh抓取连接文件服务器的NTLMv2 Hash
  5. Pytorch的BatchNorm层使用中容易出现的问题
  6. 信息学奥赛一本通 2041:【例5.9】新矩阵
  7. 自己专属的Ubuntu系统伪装Mac
  8. UVa 11998 破碎的键盘(数组实现链表)
  9. 微软发布了Visual Stduio 2010 RTM版本的虚拟机vhd文件,包含样例和动手实验(免费)...
  10. js初化加载页面时ajax会调用两次的原因_在前端开发中,有哪些因素会导致页面卡顿
  11. 艺术留学|工业设计专业2019大学新排名
  12. 删除linux下的.文件,Linux删除文件命令汇总
  13. 国内外反垃圾邮件技术
  14. 如何让我们的软件跳过360和金山毒霸的“随意拦截”?
  15. 学校计算机怎么连接自己的热点,笔记本电脑怎么连接手机热点(手机热点开启及连接方法)...
  16. 使用hutool生成excel遇到的问题:
  17. Hadoop中怎么解决Starting secondary namenodes [0.0.0.0]
  18. Word2016写论文之题注功能——公式自动编号右对齐等操作
  19. Codec2类的解析
  20. 狂神css视频笔记1-15课

热门文章

  1. docker 笔记 (6)搭建本地registry
  2. Hadoop学习笔记(1) ——菜鸟入门
  3. HDU4080 Stammering Aliens(二分 + 后缀数组)
  4. C#中静态方法的运用和字符串的常用方法(seventh day)
  5. java I/O总结(收藏)
  6. 使用bitblt提高GDI+绘图的效率(转)
  7. java实现局域网内单对单和多对多通信的设计思路
  8. 【组队学习】【31期】IOS开发
  9. 02 Scratch等级考试(二级)模拟题
  10. 利用 createTrackbar 进行二值化