通过循环依赖问题彻底理解SpringIOC的精华

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 前言你可能会有如下问题:1、想看Spring源码,但是不知道应当如何入手去看,对整个Bean的流程没有概念,碰到相关问题也没有头绪如何下手2、看过几遍源码,没办法彻底理解,没什么感觉,没过一阵子又忘了本文将结合实际问题,由问题引出源码,并在解释时会尽量以图表的形式让你一步一步彻底理解Spring Bean的IOC、DI、生命周期、作用域等。

前言

你可能会有如下问题:

1、想看Spring源码,但是不知道应当如何入手去看,对整个Bean的流程没有概念,碰到相关问题也没有头绪如何下手

2、看过几遍源码,没办法彻底理解,没什么感觉,没过一阵子又忘了

本文将结合实际问题,由问题引出源码,并在解释时会尽量以图表的形式让你一步一步彻底理解Spring Bean的IOC、DI、生命周期、作用域等。

先看一个循环依赖问题

现象

循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

img_e22421b0a454877452700d7f7db522fd.jpe

如何理解“依赖”呢,在Spring中有:

构造器循环依赖

field属性注入循环依赖

直接上代码:

构造器循环依赖


img_46cae556363d2aca909c234017dc2fe8.png


img_7b2bbab5394d5306602e12affc9879db.png


img_0cf60664b9a8439e63f3cb9362736322.png

结果:项目启动失败,发现了一个cycle。

img_6a30c22a5e34d407f47e3f831b1abba6.png

 2.field属性注入循环依赖

img_8f5af19749855fda74cbb5be6011e3f4.png

结果:项目启动成功

img_73757c47a319367d2a10f9fc3b2261b6.jpe

 3.field属性注入循环依赖(prototype)

img_87ce277f182ec1de5f851f812bee8426.png

结果:项目启动失败,发现了一个cycle。

img_3a4c5ef45b043c1879d18daccbfc5d27.jpe

现象总结:同样对于循环依赖的场景,构造器注入和prototype类型的属性注入都会初始化Bean失败。因为@Service默认是单例的,所以单例的属性注入是可以成功的。

分析原因

分析原因也就是在发现SpringIOC的过程,如果对源码不感兴趣可以关注每段源码分析之后的总结和循环依赖问题的分析即可。

SpringBean的加载流程(源码分析)

简单一段代码作为入口

img_4340f667c1df3f07c1c5b777f3fd79e6.png

ClassPathXmlApplicationContext是一个加载XML配置文件的类,与之相对的还有AnnotationConfigWebApplicationContext,这两个类大差不差的,只是ClassPathXmlApplicationContext的Resource是XML文件而AnnotationConfigWebApplicationContext是Scan注解获得的。

看到第二行就已经可以直接获取bean的实例了,所以第一行构造方法时,就已经完成了对所有bean的加载。

ClassPathXmlApplicationContext举例,他里面储存的东西如下:


img_1126e5dd5ece73526949c85098b2b85d.png


img_c34e3a0389ecea7e4c77b818f957d986.png

构造方法如下:

img_48cf3a921366efb48347263d4ad454b8.jpe

接下来大概看看refresh方法:

img_9e70322813bf272014eef1d291c3854a.jpe

子方法先不看,先看看refresh方法的结构,其实就有几点值得学习:

1、方法为什么加锁? 是为了避免多线程的场景下同时刷新Spring上下文

2、虽然整个方法是加锁的,但是却用了Synchronized关键字的对象锁startUpShutdownMonitor,这样做有两个好处:

(1)关闭资源的时候会调用close()方法,close()方法也使用了同样的对象锁,而关闭资源的close和refresh的两个冲突的方法,这样可以避免冲突

(2)此处对象锁相对于整个方法加锁的话,同步的范围更小了,锁的粒度更小,效率更高

3、这个方法refresh定义了整个Spring IOC的流程,每一个方法名字都清晰易懂,可维护性、可读性很强

总结:看源码需要找准入口,看的时候多思考,学习Spring的巧妙的设计。ApplicationContext的构造方法中最关键是方法是refresh,其中有一些比价好的设计。

obtainFreshBeanFactory方法

这个方法作用是获取刷新Spring上下文的Bean工厂:


img_4336546cf77ede747be0350456e54b18.png


img_ce56b8adb138d897532a151f182dfaa9.png

这断代码的核心是DefaultListableBeanFactory,核心类我们再整理一下,以图表格式:

下面有三个加粗的Map,这些个Map是解决问题的关键。。。我们之后详细分析

img_067f15b9c8a86483aebbc54f1e16758c.png


img_29195a85d4434ff366c54248ab0f6dfc.png

BeanDefinition在IOC容器中的注册

接下来简要分析一下loadBeanDefinitions。

对于这个BeanDefinition,我是这么理解的: 它是SpringIOC过程中间的一个产物,可以看成是对Bean定义的抽象,里面封装的数据都是与Bean定义相关的,封装了一些基本的bean的Property、initi-method、destroy-method等。

这里的主要方法是loadBeanDefinitions,这里不详细展开说,它主要做了几件事:

1、初始化了BeanDefinitionReader

2、通过BeanDefinitionReader获取Resource,也就是xml配置文件的位置,并且把文件转换成一个叫Document的对象

3、接下来需要将Document对象转化成容器内部的数据结构(也就是BeanDefinition),也即是将Bean定义的List、Map、Set等各种元素进行解析,转换成Managed类(Spring对BeanDefinition数据的封装)放在BeanDefinition中;这个方法是RegisterBeanDefinition(),也就是解析的过程。

4、解析完成后,会把解析的结果放到BeanDefinition对象中并设置到一个Map中

以上这个过程就是BeanDefinition在IOC容器中的注册。

再回到Refresh方法,总结每一步如下图:

总结:这一部分步骤主要是Spring如何加载Xml文件或者注解,并把它解析成BeanDefinition。

Spring创建Bean的过程

先回到之前的refresh方法(也就是在构造ApplicationContext时的方法),我们跳过不重要的部分:

img_987c89a664a9146713d64fc3c64ef09d.jpe

我们直接看finishBeanFactoryInitialization里面的preInstantiateSingletons方法,顾名思义初始化所有的单例bean,截取部分如下:

img_27bf44e88ccf896c1203fdd8a5ffcf1a.jpe

现在来看核心的getBean方法,对于所有获取Bean对象是实例,都是用这个getBean方法,这个方法最终调用的是doGetBean方法,这个方法就是所谓的DI(依赖注入)发生的地方。

程序=数据+算法,之前的BeanDefinition就是“数据”,依赖注入也就是在BeanDefinition准备好情况下进行进行的,这个过程不简单,因为Spring提供了很多参数配置,每一个参数都代表了IOC容器的特性,这些特性的实现需要在Bean的生命周期中完成。

代码比较多,就不贴了,大家可以自行查看AbstractBeanFactory里面的doGetBean方法,这里直接上图,这个图就是依赖注入的整个过程:

img_b5369c6931b2c8423999ecf91a286a14.jpe

总结:Spring创建好了BeanDefinition之后呢,会开始实例化Bean,并且对Bean的依赖属性进行填充。实例化时底层使用了CGLIB或Java反射技术。上图中instantiateBean核PupulateBean方法很重要!

循环依赖问题分析

我们先总结一下之前的结论:

1、构造器注入和prototype类型的field注入发生循环依赖时都无法初始化

2、field注入单例的bean时,尽管有循环依赖,但bean仍然可以被成功初始化

针对这几个结论,提出问题

单例的设值注入bean是如何解决循环依赖问题呢?如果A中注入了B,那么他们初始化的顺序是什么样子的?

为什么prototype类型的和构造器类型的Spring无法解决循环依赖呢?

之前在DefaultListableBeanFactory类中,列出了一个表格;现在我把关键的精华属性列出来:

img_827ed3eb0f7660f62a5b1dba3dab7488.png


前面三个Map,我们称为单例初始化的三级缓存,理解这个问题,我们目前只需关注“三级”,也就是singletonFactories

分析:

对于问题1,单例的设值注入,如果A中注入了B,B应该是A中的一个属性,那么猜想应该是A已经被instantiate(实例化)之后,在populateBean(填充A中的属性)时,对B进行初始化。

对于问题2,instantiate(实例化)其实就是理解成new一个对象的过程,而new的时候肯定要执行构造方法,所以猜想对于应该是A在instantiate(实例化)时,进行B的初始化。

有了分析和猜想之后呢,围绕关键的属性,根据从上图的doGetBean方法开始到populateBean所有的代码,我整理了如下图:

img_2caa12b6b01e4fcb3a3cdecb5ba8c2f8.jpe

上图是整个过程中关键的代码路径,感兴趣的可以自己debug几回,最关键的解决循环依赖的是如上的两个标红的方法,第一个方法getSingleton会从singletonFactories里面拿Singleton,而addSingletonFactory会把Singleton放入singletonFactories。

对于问题1:单例的设值注入bean是如何解决循环依赖问题呢?如果A中注入了B,那么他们初始化的顺序是什么样子的?

假设循环注入是A-B-A:A依赖B(A中autowire了B),B又依赖A(B中又autowire了A):

img_760964d075e26b43de5809af6924674b.jpe

本质就是三级缓存发挥作用,解决了循环。

对于当时问题2,instantiate(实例化)其实就是理解成new一个对象的过程,而new的时候肯定要执行构造方法,所以猜想对于应该是A在instantiate(实例化)时,进行B的初始化。

答案也很简单,因为A中构造器注入了B,那么A在关键的方法addSingletonFactory()之前就去初始化了B,导致三级缓存中根本没有A,所以会发生死循环,Spring发现之后就抛出异常了。至于Spring是如何发现异常的呢,本质上是根据Bean的状态给Bean进行mark,如果递归调用时发现bean当时正在创建中,那么久抛出循环依赖的异常即可。

那么prototype的Bean是如何初始化的呢?

prototypeBean有一个关键的属性:

/** Names of beans that are currently in creation */

private final ThreadLocal prototypesCurrentlyInCreation =

new NamedThreadLocal("Prototype beans currently in creation");

保存着正在创建的prototype的beanName,在流程上并没有暴露任何factory之类的缓存。并且在beforePrototypeCreation(String beanName)方法时,把每个正在创建的prototype的BeanName放入一个set中:

img_e798568c856c72801e7648fc42cb6988.png

并且会循环依赖时检查beanName是否处于创建状态,如果是就抛出异常:

img_e06c53ccb8029bc21856b9d62f27bcd5.png

从流程上就可以查看,无论是构造注入还是设值注入,第二次进入同一个Bean的getBean方法是,一定会在校验部分抛出异常,因此不能完成注入,也就不能实现循环引用。

总结:Spring在InstantiateBean时执行构造器方法,构造出实例,如果是单例的话,会将它放入一个singletonBeanFactory的缓存中,再进行populateBean方法,设置属性。通过一个singletonBeanFactory的缓存解决了循环依赖的问题。

再解决一个问题

现在大家已经对Spring整个流程有点感觉了,我们再来解决一个简单的常见的问题:

考虑一下如下的singleton代码:

img_0efc0bb8bf66a89716a048f6adde959f.png


img_f77a689130420031908ed97b394f27e3.png

一个Singleton的Bean中Autowired了一个prototype的Bean,那么问题来了,每次调用SingletonBean.doSomething()时打印的对象是不是同一个呢?

有了之前的知识储备,我们简单分析一下:因为Singleton是单例的,所以在项目启动时就会初始化,prototypeBean本质上只是它的一个Property,那么ApplicationContex中只存在一个SingletonBean和一个初始化SingletonBean时创建的一个prototype类型的PrototypeBean。

那么每次调用SingletonBean.doSomething()时,Spring会从ApplicationContex中获取SingletonBean,每次获取的SingletonBean是同一个,所以即便PrototypeBean是prototype的,但PrototypeBean仍然是同一个。每次打印出来的内存地址肯定是同一个。

那这个问题如何解决呢?

解决办法也很简单,这种情况我们不能通过注入的方式注入一个prototypeBean,只能在程序运行时手动调用getBean("prototypeBean")方法,我写了一个简单的工具类:

img_39c3a2ca66f48cc505bd32b5257fcdbe.png


对于这个ApplicationContextAware接口:

在某些特殊的情况下,Bean需要实现某个功能,但该功能必须借助于Spring容器才能实现,此时就必须让该Bean先获取Spring容器,然后借助于Spring容器实现该功能。为了让Bean获取它所在的Spring容器,可以让该Bean实现ApplicationContextAware接口。

感兴趣的读者自己可以试试。

总结:

回到循环依赖的问题,有的人可能会问singletonBeanFactory只是一个三级缓存,那么一级缓存和二级缓存有什么用呢?

其实大家只要理解整个流程就可以切入了,Spring在初始化Singleton的时候大致可以分几步,初始化——设值——销毁,循环依赖的场景下只有A——B——A这样的顺序,但在并发的场景下,每一步在执行时,都有可能调用getBean方法,而单例的Bean需要保证只有一个instance,那么Spring就是通过这些个缓存外加对象锁去解决这类问题,同时也可以省去不必要的重复操作。Spring的锁的粒度选取也是很吊的,这里暂时不深入研究了。

解决此类问题的关键是要对SpringIOC和DI的整个流程做到心中有数,看源码一般情况下不要求每一行代码都了解透彻,但是对于整个的流程和每个流程中在做什么事需要了然,这样实际遇到问题时才可以很快的切入进行分析解决。

希望这篇文章可以帮助你对Spring的IOC和DI的流程有一个更深刻的认识!

欢迎工作一到五年的Java工程师朋友们加入Java填坑之路:860113481

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

相关文章
|
12月前
|
缓存 Java Spring
[读书笔记]Spring中的循环依赖详解
[读书笔记]Spring中的循环依赖详解
54 0
|
2月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
155 24
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
4月前
|
Java Spring
Spring循环依赖问题之构造器内的循环依赖如何解决
Spring循环依赖问题之构造器内的循环依赖如何解决
|
6月前
|
缓存 算法 Java
开发必懂的Spring循环依赖图解 Spring 循环依赖
开发必懂的Spring循环依赖图解 Spring 循环依赖
57 1
|
6月前
|
缓存 Java Spring
面试题 : Spring循环依赖问题及其解决方案
面试题 : Spring循环依赖问题及其解决方案
117 0
|
缓存 Java 开发者
一文详解Spring Bean循环依赖
本文主要梳理了Spring解决bean循环依赖的思路。
43542 31
|
缓存 Java Spring
一张图彻底搞懂Spring循环依赖
循环依赖就是指循环引用,是两个或多个Bean相互之间的持有对方的引用。BeanA 类依赖了Bean B类,同时Bean B类又依赖了Bean A类。这种依赖关系形成了一个闭环,我们把这种依赖关系就称之为循环依赖。Bean A类依赖了Bean B类,Bean B类依赖了Bean C类,Bean C类依赖了Bean A类,如此,也形成了一个依赖闭环。自己引用了自己,自己和自己形成了依赖关系。同样也是一个依赖闭环。
420 1
|
存储 Java Spring
Spring循环依赖,竟然有这样不可思议的坑!
Spring循环依赖,竟然有这样不可思议的坑!
|
存储 缓存 Java
spring框架-循环依赖问题(二)
spring框架-循环依赖问题(二)
阿里三面:什么是循环依赖?你说一下Spring解决循环依赖的流程
Spring循环依赖的原理解析 1、什么是循环依赖?# ​ 我们使用Spring的时候,在一个对象中注入另一个对象,但是另外的一个对象中也包含该对象。如图: