spring解决循环依赖

前言算法

你可能会有以下问题:缓存

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

二、看过几遍源码,没办法完全理解,没什么感受,没过一阵子又忘了并发

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

先看一个循环依赖问题源码分析

现象prototype

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

 

 

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

  • 构造器循环依赖xml

  • field属性注入循环依赖

 

直接上代码:

构造器循环依赖

 

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

 

2.field属性注入循环依赖

 

结果:项目启动成功

 

 

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

 

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

 

 

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

分析缘由

分析缘由也就是在发现SpringIOC的过程,若是对源码不感兴趣能够关注每段源码分析以后的总结和循环依赖问题的分析便可。

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

简单一段代码做为入口

 

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

看到第二行就已经能够直接获取bean的实例了,因此第一行构造方法时,就已经完成了对全部bean的加载。

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

 

BeanDefinition在IOC容器中的注册

接下来简要分析一下loadBeanDefinitions。

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

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

一、初始化了BeanDefinitionReader

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

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

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

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

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

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

Spring建立Bean的过程

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

 

 

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

 

 

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

程序=数据+算法,以前的BeanDefinition就是“数据”,依赖注入也就是在BeanDefinition准备好状况下进行进行的,这个过程不简单,由于Spring提供了不少参数配置,每个参数都表明了IOC容器的特性,这些特性的实现须要在Bean的生命周期中完成。

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

 

 

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

循环依赖问题分析

咱们先总结一下以前的结论:

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

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

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

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

  2. 为何prototype类型的和构造器类型的Spring没法解决循环依赖呢?

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

 

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

分析:

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

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

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

 

 

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

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

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

 

 

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

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

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

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

prototypeBean有一个关键的属性:

 

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

 

而且会循环依赖时检查beanName是否处于建立状态,若是是就抛出异常:

 

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

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

再解决一个问题

如今你们已经对Spring整个流程有点感受了,咱们再来解决一个简单的常见的问题:

考虑一下以下的singleton代码:

 

一个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")方法,我写了一个简单的工具类:

 

对于这个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的流程有一个更深入的认识!