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

Spring里面Bean的生命周期和循环依赖问题

什么是循环依赖?

循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:
注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。
Spring中循环依赖场景有: 
(1)构造器的循环依赖 
(2)field属性的循环依赖
其中,构造器的循环依赖问题无法解决,只能拋出BeanCurrentlyInCreationException异常,在解决属性循环依赖时,spring采用的是提前暴露对象的方法。

怎么检测是否存在循环依赖

检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

Spring怎么解决循环依赖

Spring的循环依赖的理论依据基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的。(但是构造器必须是在获取引用之前)。

完整实例bean,需要通过上面三个步骤:

1.  createBeanInstance方法: 得到一个实例bean,但是没有进行属性值的注入

2.  populateBean方法:就是对bean进行属性值注入。

3.  initializeBean方法:如果配置文件里面有init方法,需要执行init方法。

可以看出第一步和第二步比较重要,这里面就是解决bean依赖的关键。

我们可以看出进行createBeanInstance方法,得到了bean实例对象,但是没有属性注入。把没有完全实例化的bean,放到addSinletonFactory方法里面去,这样相当于就是提前暴露bean,接下来addSinletonFactory方法,这里面使用三个Map类型的,及三级缓存来解决循环依赖。接下我们看一下addSingletonFactory方法实现。

singleObjects--》单例对象的cache:一级缓存

earlySingletonObjects --》提前暴光的单例对象的Cache :二级缓存

singletonFactories --》单例对象工厂的cache :三级缓存

首先解释两个参数:

  • isSingletonCurrentlyInCreation 判断对应的单例对象是否在创建中,当单例对象没有被初始化完全(例如A定义的构造函数依赖了B对象,得先去创建B对象,或者在populatebean过程中依赖了B对象,得先去创建B对象,此时A处于创建中)
  • allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象

在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。

Spring解决循环依赖的诀窍就在于singletonFactories这个cache,这个cache中存的是类型为ObjectFactory,其定义如下

public interface ObjectFactory<T> {
T getObject() throws BeansException;}

在bean创建过程中,有两处比较重要的匿名内部类实现了该接口。一处是

new ObjectFactory<Object>() {
@Override public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
} }

另一处就是

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

这里就是解决循环依赖的关键,这段代码发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。

这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。

知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。

为啥要三级缓存

必须使用三级缓存吗 两级不行吗 提前暴露时直接放到二级缓存earlySingletonObjects里会有什么问题呢?

answer:如果不存在循环依赖,那么就没必要提早曝光放到earlySingletonObjects里。如果像你说的只有两级缓存,那简单的A依赖B,A在第一步初始化未注入field的时候就放到了earlySingletonObjects,显然不符合常理,只能说放到三级缓存中预防循环依赖的产生。singletonFactories 并没有提早曝光的意思,只是为了缓存,earlySingletonObjects才为了提早曝光,所以才要三级的。

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

  10. 3.1 spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖

    本次博客的目标 1. 手写spring循环依赖的整个过程 2. spring怎么解决循环依赖 3. 为什么要二级缓存和三级缓存 4. spring有没有解决构造函数的循环依赖 5. spring有没有 ...

随机推荐

  1. Spring依赖

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...

  2. 项目Alpha冲刺--4/10

    项目Alpha冲刺--4/10 1.团队信息 团队名称:基于云的胜利冲锋队 成员信息 队员学号 队员姓名 个人博客地址 备注 221500201 孙文慈 https://www.cnblogs.com ...

  3. Program type already present:okio.AsyncTimeout$Watchdog Message{kind=ERROR, text=Program type :okio

    在app中的build.gradle中加入如下代码, configurations { all*.exclude group: 'com.google.code.gson' all*.exclude ...

  4. sqlserver 中通配符%和_的使用

    --以a开头的数据 SELECT * FROM BCUSTOMER_MZN WHERE CST_NAME LIKE 'A%' --以Z结尾的数据 SELECT * FROM BCUSTOMER_MZN ...

  5. Tp3.2 复合查询

    我们常常有这样的需求,比如搜索. 搜索出,标题,子标题,内容中包含某某关键字. 这就要and,or结合使用了. $where = ['is_show'=>1,'status'=>1]; / ...

  6. Arrays常用方法

    传送:https://blog.csdn.net/u013256816/article/details/50924762

  7. ASP.NET中Page_Load()与Page_Init()的区别

    Page_Init()事件:aspx初始化时触发,只执行一次,常用于页面初始化,并且执行在page_load之前,如果在aspx的程序中需要使用该方法,那么该方法的类需要继承 System.Web.U ...

  8. github Bash教程

    1.只在本地使用 配置github git config --global user.name 你的英文名 git config --global user.email 你的邮箱 git config ...

  9. 百万并发中间件系统的内核设计看Java并发性能优化

    “ 这篇文章,给大家聊聊一个百万级并发的中间件系统的内核代码里的锁性能优化. 很多同学都对Java并发编程很感兴趣,学习了很多相关的技术和知识.比如volatile.Atomic.synchroniz ...

  10. 力扣 (LeetCode)657. 机器人能否返回原点

    在二维平面上,有一个机器人从原点 (0, 0) 开始.给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束. 移动顺序由字符串表示.字符 move[i] 表示其第 i 次移动.机器 ...