Spring是如何解决循环依赖的
前言
在面试的时候这两年有一个非常高频的关于spring的问题,那就是spring是如何解决循环依赖的。这个问题听着就是轻描淡写的一句话,其实考察的内容还是非常多的,主要还是考察的应聘者有没有研究过spring的源码。但是说实话,spring的源码其实非常复杂的,研究起来并不是个简单的事情,所以我们此篇文章只是为了解释清楚Spring是如何解决循环依赖的这个问题。
什么样的依赖算是循环依赖?
用过Spring框架的人都对依赖注入这个词不陌生,一个Java类A中存在一个属性是类B的一个对象,那么我们就说类A的对象依赖类B,而在Spring中是依靠的IOC来实现的对象注入,也就是说创建对象的过程是IOC容器来实现的,并不需要自己在使用的时候通过new关键字来创建对象。
那么当类A中依赖类B的对象,而类B中又依赖类C的对象,最后类C中又依赖类A的对象的时候,这种情况最终的依赖关系会形成一个环,这就是循环依赖。
循环依赖的类型
根据注入的时机可以分为两种:
- 构造器循环依赖
依赖的对象是通过构造方法传入的,在实例化bean的时候发生。 - 赋值属性循环依赖
依赖的对象是通过setter方法传入的,对象已经实例化,在属性赋值和依赖注入的时候发生。
构造器循环依赖,本质上是无解的,实例化A的时候调用A的构造器,发现依赖了B,又去实例化B,然后调用B的构造器,发现又依赖的C,然后调用C的构造器去实例化,结果发起C的构造器里依赖了A,这就是个死循环无解。所以Spring也是不支持构造器循环依赖的,当发现存在构造器循环依赖时,会直接抛出BeanCurrentlyInCreationException
异常。
赋值属性循环依赖,Spring只支持bean在单例模式下的循环依赖,其他模式下的循环依赖Spring也是会抛出BeanCurrentlyInCreationException
异常的。Spring通过对还在创建过程中的单例bean,进行缓存并提前暴露该单例,使得其他实例可以提前引用到该单例bean。
Spring为什么只支持单例模式下的bean的赋值情况下的循环依赖
在prototype的模式下的bean,使用了一个ThreadLocal变量prototypesCurrentlyInCreation
来记录当前线程正在创建中的bean,这个变量在AbtractBeanFactory
类里。在创建前用beanName记录bean,在创建完成后删除bean。在prototypesCurrentlyInCreation
里采用了一个Set对象来存储正在创建中的bean。我们都知道Set是不允许存在重复对象的,这样就能保证同一个bean在一个线程中只能有一个正在创建。
下面是prototypesCurrentlyInCreation
变量在删除bean时的操作,在AbtractBeanFactory
的beforePrototypeCreation
操作里。
protected void afterPrototypeCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
if (curVal instanceof String) {
this.prototypesCurrentlyInCreation.remove();
}
else if (curVal instanceof Set) {
Set<String> beanNameSet = (Set<String>) curVal;
beanNameSet.remove(beanName);
if (beanNameSet.isEmpty()) {
this.prototypesCurrentlyInCreation.remove();
}
}
}
从上面的代码中看出,当变量为一个的时候采用了一个String对象来存储,节省了一些内存空间。
在AbstractBeanFactory
类的doGetBean
方法里先判断是否为单例对象,不是单例对象,则直接判断当前线程是否已经存在了正在创建的bean。存在的话直接抛出异常。
这个isPrototypeCurrentlyInCreation()
方法的实现代码如下:
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
Object curVal = this.prototypesCurrentlyInCreation.get();
return curVal != null && (curVal.equals(beanName) || curVal instanceof Set && ((Set)curVal).contains(beanName));
}
因为有了这个机制,spring在原型模式下是解决不了bean的循环依赖的,当发现有循环依赖的时候会直接抛出BeanCurrentlyInCreationException
异常的。
那么为什么spring在单例模式下的构造赋值也不支持循环依赖呢?
其实原理和原型模式下的情况类似,在单例模式下,bean也会用一个Set集合来保存正在创建中的bean,在创建前保存,创建完成后删除。
这个对象在DefaultSingletonBeanRegistry
类下变量名为:singletonsCurrentlyInCreation
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16));
}
判定代码在DefaultSingletonBeanRegistry
类的beforeSingletonCreation
方法下。
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
在上面这个方法中,判定singletonsCurrentlyInCreation
是否能成功的保存一个单例bean。如果不能成功保存,那么就会直接抛出BeanCurrentlyInCreationException
异常。
单例模式下的Setter赋值循环依赖
终于到了我们的重点,Spring是如何解决单例模式下的Setter赋值的循环依赖了。
其实主要的就是靠提前暴露创建中的单例实例。
那么具体是一个怎样的过程呢?
例如:上面那个图的例子,A依赖B,B依赖C,C又依赖B。
过程如下:
创建A,调用构造方法,完成构造,进行属性赋值注入,发现依赖B,去实例化B
。创建B,调用构造方法,完成构造,进行属性赋值注入,发现依赖C,去实例化C
。- 创建C,调用构造方法,完成构造,进行属性赋值注入,发现依赖A。
这个时候就是解决循环依赖的关键了,因为A已经通过构造方法已经构造完成了,也就是说已经将Bean的在堆中分配好了内存,这样即使A再填充属性值也不会更改内存地址了,所以此时可以提前拿出来A的引用,来完成C的实例化。
这样上面创建C过程就会变成了: 创建C,调用构造方法,完成构造,进行属性赋值注入,发现依赖A,A已经构造完成,直接引用,完成C的实例化
。C完成实例化后,注入B,B也完成了实例化,然后B注入A,A也完成了实例化
。
为了能获取到创建中单例bean,spring提供了三级缓存来将正在创建中的bean提前暴露。
在类DefaultSingletonBeanRegistry
下,即下图红框中的三个Map对象。
这三个缓存Map的作用如下:- 一级缓存,
singletonObjects
单例缓存,存储已经实例化的单例bean。 - 二级缓存,
earlySingletonObjects
提前暴露的单例缓存,这里存储的bean是刚刚构造完成,但还会通过属性注入bean。 - 三级缓存,
singletonFactories
生产单例的工厂缓存,存储工厂。
首先在创建bean的时候会先创建一个和bean同名的单例工厂,并将bean先放入到单例工厂中。代码在AbstractAutowireCapableBeanFactory
类的doCreateBean
方法中。
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
......
this.addSingletonFactory(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
return AbstractAutowireCapableBeanFactory.this.getEarlyBeanReference(beanName, mbd, bean);
}
});
.....
}
而上面的代码中的addSingletonFactory
方法的代码如下:
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
Map var3 = this.singletonObjects;
synchronized(this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
addSingletonFactory
方法的作用通过代码就可以看到是将存在了正在创建中的bean的单例工厂,放在三级缓存里,这样保证了在循环依赖查找的时候是可以找到bean的引用的。
具体读取缓存获取bean的过程在类DefaultSingletonBeanRegistry
的getSingleton
方法里。
如下源码:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
Map var4 = this.singletonObjects;
synchronized(this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject != NULL_OBJECT ? singletonObject : null;
}
通过上面的源码我们可以看到,在获取单例Bean的时候,会先从一级缓存singletonObjects
里获取,如果没有获取到(说明不存在或没有实例化完成),会去第二级缓存earlySingletonObjects
中去找,如果还是没有找到的话,就会三级缓存中获取单例工厂singletonFactory
,通过从singletonFactory
中获取正在创建中的引用,将singletonFactory
存储在earlySingletonObjects
二级缓存中,这样就将创建中的单例引用从三级缓存中升级到了二级缓存中,二级缓存earlySingletonObjects
,是会提前暴露已完成构造,还可以执行属性注入的单例bean的。
这个时候如何还有其他的bean也是需要属性注入,那么就可以直接从earlySingletonObjects
中获取了。
上面的例子中的过程中的A,在注入C的时候,其实并没有真正的初始化完成,等到顺利的注入了B才算是真正的初始化完成。
整个过程如下图:
Spring是如何解决循环依赖的的更多相关文章
- spring: 我是如何解决循环依赖的?
1.由同事抛的一个问题开始 最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到.平时自认为对spring循环依赖问题还是比较了解的,直到遇到这个和后面的 ...
- Spring 是如何解决循环依赖的?
前言 相信很多小伙伴在工作中都会遇到循环依赖,不过大多数它是这样显示的: 还会提示这么一句: Requested bean is currently in creation: Is there an ...
- 听说你还不知道Spring是如何解决循环依赖问题的?
Spring如何解决的循环依赖,是近两年流行起来的一道Java面试题. 其实笔者本人对这类框架源码题还是持一定的怀疑态度的. 如果笔者作为面试官,可能会问一些诸如"如果注入的属性为null, ...
- Spring三级缓存解决循环依赖
前提知识 1.解决循环依赖的核心依据:实例化和初始化步骤是分开执行的 2.实现方式:三级缓存 3.lambda表达式的延迟执行特性 spring源码执行逻辑 核心方法refresh(), popula ...
- 浅谈Spring解决循环依赖的三种方式
引言:循环依赖就是N个类中循环嵌套引用,如果在日常开发中我们用new 对象的方式发生这种循环依赖的话程序会在运行时一直循环调用,直至内存溢出报错.下面说一下Spring是如果解决循环依赖的. 第一种: ...
- Spring 如何解决循环依赖问题?
在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的. 这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能 ...
- Spring ioc(4)---如何解决循环依赖
前面说到对象的创建,那么在创建的过程中Spring是怎么又是如何解决循环依赖的呢.前面提到有个三级缓存.就是利用这个来解决循环依赖.打个比方说实例化A的时候,先将A创建(早期对象)放入一个池子中.这个 ...
- Spring解决循环依赖,你真的懂了吗?
导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...
- Spring如何解决循环依赖,你真的懂了?
导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...
随机推荐
- PHP 是什么?简介下
PHP 是服务器端脚本语言. 您应当具备的基础知识 在继续学习之前,您需要对以下知识有基本的了解: HTML CSS PHP 是什么? PHP(全称:PHP:Hypertext Preprocesso ...
- Skill 扫描list中是否含有某元素
https://www.cnblogs.com/yeungchie/ code procedure(ycInListp(scan keylist) prog((times) times = 0 for ...
- EACCES: permission denied,mkdir … npm install 安装依赖问题解决
强哥最近在用hugeGraph图库做二次开发的时候,在打包的时遇到前端项目打包失败的问题: cwebp-bin@4.0.0 postinstall /home/hugegraph/my-hugegra ...
- C 语言学习 -1
头文件 stdio.h stdlib.h sting.h 先学习上面三个头文件: 1: stdio.h 这个头文件包含了 程序与外界数据交互的各种函数 说白了就是 用来处理 输入/输 ...
- K近邻算法(一)
K 近邻算法思想: 寻找该点周围最近的K个点.根据这K 个点的类别来判断该点的类别: 核心: 数据归一化.(在必要的时候必须进行数据归一化处理,防止某一特征在计算数据时占比较重) 计算欧拉距离 . 使 ...
- Java语言特性
Java的语言特性: 1.语法相对简单 2.面向对象 3.分布性 4.可移植性 5.安全性 6.健壮性 7.解释性 8.多线程 9.动态性与并发性 Java中的面向对象编程: 面向对象程序设计(Obj ...
- Linux下 flash工具的使用
使用命令前用cat /proc/mtd 查看一下mtdchar字符设备:或者用ls -l /dev/mtd* #cat /proc/mtd dev: size erasesize name ...
- 从零搭建Spring Boot脚手架(1):开篇以及技术选型
1. 前言 目前Spring Boot已经成为主流的Java Web开发框架,熟练掌握Spring Boot并能够根据业务来定制Spring Boot成为一个Java开发者的必备技巧,但是总是零零碎碎 ...
- 极简 Node.js 入门 - 1.3 调试
极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...
- Linux 安装 PostgreSQL
Linux 安装 PostgreSQL CentOS 7 安装 PostgreSQL 10 步骤 官网安装步骤,选择服务器和数据库版本,会给出相应的安装命令 # 安装 yum install -y h ...