Spring解决循环依赖,你真的懂了吗?
导读
- 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图:
- 这个就是典型的构造器依赖,详情请看上面两篇文章,这里不再详细赘述了。本篇文章将会从源码深入解析Spring是如何解决循环依赖的?为什么不能解决构造器的循环依赖?
什么是循环依赖
- 简单的说就是A依赖B,B依赖C,C依赖A这样就构成了循环依赖。
- 循环依赖分为构造器依赖和属性依赖,众所周知的是Spring能够解决属性的循环依赖(set注入)。下文将从源码角度分析Spring是如何解决属性的循环依赖。
思路
- 如何解决循环依赖,Spring主要的思路就是依据三级缓存,在实例化A时调用doGetBean,发现A依赖的B的实例,此时调用doGetBean去实例B,实例化的B的时候发现又依赖A,如果不解决这个循环依赖的话此时的doGetBean将会无限循环下去,导致内存溢出,程序奔溃。spring引用了一个早期对象,并且把这个"早期引用"并将其注入到容器中,让B先完成实例化,此时A就获取B的引用,完成实例化。
三级缓存
- Spring能够轻松的解决属性的循环依赖正式用到了三级缓存,在AbstractBeanFactory中有详细的注释。
/**一级缓存,用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**三级缓存 存放 bean 工厂对象,用于解决循环依赖*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/**二级缓存 存放原始的 bean 对象(尚未填充属性),用于解决循环依赖*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
- 一级缓存:singletonObjects,存放完全实例化属性赋值完成的Bean,直接可以使用。
- 二级缓存:earlySingletonObjects,存放早期Bean的引用,尚未属性装配的Bean
- 三级缓存:singletonFactories,三级缓存,存放实例化完成的Bean工厂。
开撸
- 先上一张流程图看看Spring是如何解决循环依赖的
- 上图标记蓝色的部分都是涉及到三级缓存的操作,下面我们一个一个方法解析
【1】 getSingleton(beanName):源码如下:
//查询缓存
Object sharedInstance = getSingleton(beanName);
//缓存中存在并且args是null
if (sharedInstance != null && args == null) {
//.......省略部分代码
//直接获取Bean实例
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
//getSingleton源码,DefaultSingletonBeanRegistry#getSingleton
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//先从一级缓存中获取已经实例化属性赋值完成的Bean
Object singletonObject = this.singletonObjects.get(beanName);
//一级缓存不存在,并且Bean正处于创建的过程中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//从二级缓存中查询,获取Bean的早期引用,实例化完成但是未赋值完成的Bean
singletonObject = this.earlySingletonObjects.get(beanName);
//二级缓存中不存在,并且允许创建早期引用(二级缓存中添加)
if (singletonObject == null && allowEarlyReference) {
//从三级缓存中查询,实例化完成,属性未装配完成
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
//二级缓存中添加
this.earlySingletonObjects.put(beanName, singletonObject);
//从三级缓存中移除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
- 从源码可以得知,doGetBean最初是查询缓存,一二三级缓存全部查询,如果三级缓存存在则将Bean早期引用存放在二级缓存中并移除三级缓存。(升级为二级缓存)
【2】addSingletonFactory:源码如下
//中间省略部分代码。。。。。
//创建Bean的源码,在AbstractAutowireCapableBeanFactory#doCreateBean方法中
if (instanceWrapper == null) {
//实例化Bean
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//允许提前暴露
if (earlySingletonExposure) {
//添加到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
try {
//属性装配,属性赋值的时候,如果有发现属性引用了另外一个Bean,则调用getBean方法
populateBean(beanName, mbd, instanceWrapper);
//初始化Bean,调用init-method,afterproperties方法等操作
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
//添加到三级缓存的源码,在DefaultSingletonBeanRegistry#addSingletonFactory
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
//一级缓存中不存在
if (!this.singletonObjects.containsKey(beanName)) {
//放入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
//从二级缓存中移除,
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
- 从源码得知,Bean在实例化完成之后会直接将未装配的Bean工厂存放在三级缓存中,并且移除二级缓存
【3】addSingleton:源码如下:
//获取单例对象的方法,DefaultSingletonBeanRegistry#getSingleton
//调用createBean实例化Bean
singletonObject = singletonFactory.getObject();
//。。。。中间省略部分代码
//doCreateBean之后才调用,实例化,属性赋值完成的Bean装入一级缓存,可以直接使用的Bean
addSingleton(beanName, singletonObject);
//addSingleton源码,在DefaultSingletonBeanRegistry#addSingleton方法中
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
//一级缓存中添加
this.singletonObjects.put(beanName, singletonObject);
//移除三级缓存
this.singletonFactories.remove(beanName);
//移除二级缓存
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
- 总之一句话,Bean添加到一级缓存,移除二三级缓存。
扩展
【1】为什么Spring不能解决构造器的循环依赖?
- 从流程图应该不难看出来,在Bean调用构造器实例化之前,一二三级缓存并没有Bean的任何相关信息,在实例化之后才放入三级缓存中,因此当getBean的时候缓存并没有命中,这样就抛出了循环依赖的异常了。
【2】为什么多实例Bean不能解决循环依赖?
- 多实例Bean是每次创建都会调用doGetBean方法,根本没有使用一二三级缓存,肯定不能解决循环依赖。
总结
- 根据以上的分析,大概清楚了Spring是如何解决循环依赖的。假设A依赖B,B依赖A(注意:这里是set属性依赖)分以下步骤执行:
- A依次执行doGetBean、查询缓存、createBean创建实例,实例化完成放入三级缓存singletonFactories中,接着执行populateBean方法装配属性,但是发现有一个属性是B的对象。
- 因此再次调用doGetBean方法创建B的实例,依次执行doGetBean、查询缓存、createBean创建实例,实例化完成之后放入三级缓存singletonFactories中,执行populateBean装配属性,但是此时发现有一个属性是A对象。
- 因此再次调用doGetBean创建A的实例,但是执行到getSingleton查询缓存的时候,从三级缓存中查询到了A的实例(早期引用,未完成属性装配),此时直接返回A,不用执行后续的流程创建A了,那么B就完成了属性装配,此时是一个完整的对象放入到一级缓存singletonObjects中。
- B创建完成了,则A自然完成了属性装配,也创建完成放入了一级缓存singletonObjects中。
- Spring三级缓存的应用完美的解决了循环依赖的问题,下面是循环依赖的解决流程图。
- 如果觉得作者写的好,有所收获的话,点个关注推荐一下哟!!!
Spring解决循环依赖,你真的懂了吗?的更多相关文章
- Spring解决循环依赖
1.Spring解决循环依赖 什么是循环依赖:比如A引用B,B引用C,C引用A,它们最终形成一个依赖环. 循环依赖有两种 1.构造器循环依赖 构造器注入导致的循环依赖,Spring是无法解决的,只能抛 ...
- 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 浅谈Spring解决循环依赖的三种方式
引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下Spring是如果解决循环依赖的. 第一种: ...
- 建议收藏!利用Spring解决循环依赖,深入源码给你讲明白!
前置知识 只有单例模式下的bean会通过三级缓存提前暴露来解决循环依赖的问题.而非单例的bean每次获取都会重新创建,并不会放入三级缓存,所以多实例的bean循环依赖问题不能解决. 首先需要明白处于各 ...
- Spring如何解决循环依赖问题
目录 1. 什么是循环依赖? 2. 怎么检测是否存在循环依赖 3. Spring怎么解决循环依赖 本文主要是分析Spring bean的循环依赖,以及Spring的解决方式. 通过这种解决方式,我们可 ...
- 一张图彻底理解Spring如何解决循环依赖!!
写在前面 最近,在看Spring源码,看到Spring解决循环依赖问题的源码时,不得不说,源码写的太烂了.像Spring这种顶级的项目源码,竟然存在着这种xxx的代码.看了几次都有点头大,相信很多小伙 ...
- 彻底理解Spring如何解决循环依赖
Spring bean生命周期 可以简化为以下5步. 1.构建BeanDefinition 2.实例化 Instantiation 3.属性赋值 Populate 4.初始化 Initializati ...
- 【Spring】 Spring如何解决循环依赖的问题?
https://mp.weixin.qq.com/s/FtbzTMxHgzL0G1R2pSlh-A 通常来说,如果问Spring内部如何解决循环依赖,一定是单默认的单例Bean中,属性互相引用的场景. ...
- 听说你还不知道Spring是如何解决循环依赖问题的?
Spring如何解决的循环依赖,是近两年流行起来的一道Java面试题. 其实笔者本人对这类框架源码题还是持一定的怀疑态度的. 如果笔者作为面试官,可能会问一些诸如"如果注入的属性为null, ...
随机推荐
- Java 9 新特性 – 内部类的方块操作符
方块操作符 ( <> ) 在 Java 7 中就引入了,目的是为了使代码更可读. 但是呢,这个操作符一直不能在匿名内部类中使用 Java 9 修正了这个问题,就是可以在匿名内部类中使用方块 ...
- [LC] 17. Letter Combinations of a Phone Number
Given a string containing digits from 2-9 inclusive, return all possible letter combinations that th ...
- iPhone X会成为苹果最短命的旗舰机型吗?
最近,有媒体报道有凯基证券分析师郭明琪在他的最新报告指出,iPhone X将在今年中结束生产.因为苹果已计划下半年推出新款iPhone,价格也比iPhone X会低并有新功能发布.所以他预计iPhon ...
- jQuery、js全页面刷新方法
下面介绍全页面刷新方法:有时候可能会用到 window.location.reload()刷新当前页面. parent.location.reload()刷新父亲对象(用于框架) opener.loc ...
- C++求解N阶幻方
由一道数学题的联想然后根据网上的做法瞎jb乱打了一下,居然对了代码精心附上了注释,有兴趣的童鞋可以看一看..不说了,上代码!(自认为结构很清晰易懂) 1234567891011121314151617 ...
- PHP正则表达式-修饰符
我们在PHP正则表达式的学习中会碰到修饰符,那么关于PHP正则表达式修饰符的理解以及使用我们需要注意什么呢?那么我们来具体的看看它的概念以及相关内容.在学习PHP正则表达式修饰符之前先来理解下贪婪模式 ...
- Python知识点汇总
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- 制作UEFI(64位)下的WinPE + Ubuntu + Acronis多启动U盘
最近研究了一下如何制作一个多启动U盘,其中想包含的功能是WinPE(这里选择WEPE),Ubuntu 18.04,Acronis True Image 2018的ISO恢复盘.这里分享一下制作的经验和 ...
- Java对接微信登录
今天我们来对接微信开放平台的网站应用登录 首先上文档链接:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login ...
- MySQL占用CPU超过百分之100解决过程
本文转载自: https://www.93bok.com 访问网页504 Gateway Time-out,登陆服务器查看,内存正常,CPU使用率达到了400%,因为是4核,所以到了400%,几乎全部 ...