作者:Mythsman原文:https://blog.mythsman.com/post/5d838c7c2db8a452e9b7082c/转载自:猿天地
原文来自:https://mp.weixin.qq.com/s/pIawXWHCGqIHi0INHDgawQ
1 什么是依赖

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

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

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

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

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

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

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

2 什么是依赖调解

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

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

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

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

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

3 为什么要依赖注入

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

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

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

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

4 spring的依赖注入模型

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

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

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

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

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

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

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

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

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

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

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

  2. 如果不在构造函数中使用依赖注入的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

  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。。。

5.5 先加载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成功完成了初始化操作。

5.6 结论

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

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

5.7 代码坏味道

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

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

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

01 spring循环依赖的更多相关文章

  1. spring循环依赖问题分析

    新搞了一个单点登录的项目,用的cas,要把源码的cas-webapp改造成适合我们业务场景的项目,于是新加了一些spring的配置文件. 但是在项目启动时报错了,错误日志如下: 一月 , :: 下午 ...

  2. Spring 循环依赖

    循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环.此处不 ...

  3. Springboot源码分析之Spring循环依赖揭秘

    摘要: 若你是一个有经验的程序员,那你在开发中必然碰到过这种现象:事务不生效.或许刚说到这,有的小伙伴就会大惊失色了.Spring不是解决了循环依赖问题吗,它是怎么又会发生循环依赖的呢?,接下来就让我 ...

  4. Spring 循环依赖的三种方式(三级缓存解决Set循环依赖问题)

    本篇文章解决以下问题: [1] . Spring循环依赖指的是什么? [2] . Spring能解决哪种情况的循环依赖?不能解决哪种情况? [3] . Spring能解决的循环依赖原理(三级缓存) 一 ...

  5. Spring循环依赖的解决

    ## Spring循环依赖的解决 ### 什么是循环依赖 循环依赖,是依赖关系形成了一个圆环.比如:A对象有一个属性B,那么这时候我们称之为A依赖B,如果这时候B对象里面有一个属性A.那么这时候A和B ...

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

    1. 前言 这两天工作遇到了一个挺有意思的Spring循环依赖的问题,但是这个和以往遇到的循环依赖问题都不太一样,隐藏的相当隐蔽,网络上也很少看到有其他人遇到类似的问题.这里权且称他非典型Spring ...

  7. Spring — 循环依赖

    读完这篇文章你将会收获到 Spring 循环依赖可以分为哪两种 Spring 如何解决 setter 循环依赖 Spring 为何是三级缓存 , 二级不行 ? Spring 为啥不能解决构造器循环依赖 ...

  8. 帮助你更好的理解Spring循环依赖

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

  9. spring 循环依赖的一次 理解

    前言: 在看spring 循环依赖的问题中,知道原理,网上一堆的资料有讲原理. 但今天在看代码过程中,又产生了疑问. 疑问点如下: // 疑问点: 先进行 dependon 判断String[] de ...

随机推荐

  1. 安装Erlang使用RabbitMQ

    首先登陆官网进行下载:https://www.erlang.org/downloads/20.3 本次下载的版本是20.3,rabbitmq准备使用3.7.17版本 现在开始安装 因为是使用c#语言, ...

  2. Matlab 多个版本的安装包下载、安装和激活教程 + 多套数学建模视频教程

    目录 1. 关键词 1.1. 说明 2. 下载地址 2.1. OneDrive高速云盘 2.1.1. 多版本的安装包 2.1.2. 多套数学建模的视频教程 2.2. 百度云 3. 安装教程 1. 关键 ...

  3. Linux 内核层和 用户层 配置 GPIO 引脚

    Linux BSP 开发的基础就是和GPIO打交道, 下面总结下这几天对某家开发板的GPIO控制的知识. 公司的开发板用的是 DTB  模式 ,首先,进入 dts,dtsi文件查看关于GPIO 的模块 ...

  4. git Octotree:提供项目目录,方便用户在线快速浏览项目结构【转载】

    很好奇的是,GitHub 作为代码托管平台,竟然没有提供项目目录,方便用户在线快速浏览项目结构.所以,在线分析项目源码就会变得很繁琐,必须一层一层点击,然后再一次一次地向上返回.要知道,本来 GitH ...

  5. css3 :enabled与:disabled伪类选择器

    css :enabled和:disabled伪类选择器 在Web表单中,有些表单元素(如输入框.密码框.复选框等)有“可用”和“不可用”这2种状态.默认情况下,这些表单元素都处在可用状态. 在CSS3 ...

  6. Git常用命令的操作

    Git命令 一.创建版本库 初始化一个Git仓库,使用git init命令. 添加文件到Git仓库,分两步: 使用命令git add <file>,注意,可反复多次使用,添加多个文件: 使 ...

  7. Linux下安装Python,以及环境变量的配置

    1.安装环境   centos7 + vmware + xshell 2.安装Python3 2.1下载Python资源包 网址:https://www.python.org/downloads/re ...

  8. C++ decltype

    #include <iostream> using namespace std; int main() { int ia{3}; decltype(ia) varr[3]={1,2,3}; ...

  9. [BZOJ2600] ricehub

    问题描述 乡间有一条笔直而长的路称为"米道".沿着这条米道上 R 块稻田,每块稻田的坐标均为一个 1 到 L 之间(含 1 和 L)的整数.这些稻田按照坐标以不减的顺序给出,即对于 ...

  10. 【大量干货】史上最完整的Tengine HTTPS原理解析、实践与调试

    本文邀请阿里云CDN HTTPS技术专家金九,分享Tengine的一些HTTPS实践经验.内容主要有四个方面:HTTPS趋势.HTTPS基础.HTTPS实践.HTTPS调试. 一.HTTPS趋势 这一 ...