spring如何解决循环依赖_城堡斗士的博客-程序员宝宝_spring如何解决循环依赖

技术标签: spring  java  


这里我们主要分析Spring bean的循环依赖,以及Spring的解决方式。 通过这种解决方式,我们可以应用在我们实际开发项目中。

1、什么是循环依赖?
2、怎么检测循环依赖
3、循环依赖的N种场景
3、Spring怎么解决循环依赖
4、Spring对于循环依赖无法解决的场景
5、Spring解决循环依赖的方式我们能够学到什么?


以下基于spring5.0.x版本源码进行分析

1、什么是循环依赖?

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

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。

Spring中循环依赖场景主要有以下两种:
(1)field属性的循环依赖
(2)构造器的循环依赖
(3)DependsOn循环依赖

2、怎么检测循环依赖

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

3、Spring怎么解决循环依赖

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

Spring的单例对象的初始化主要分为三步:

  1. 实例化:其实也就是调用对象的构造方法实例化对象
  2. 注入:填充属性,这一步主要是对bean的依赖属性进行填充
  3. 初始化:属性注入后,执行自定义初始化

在这里插入图片描述
从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一、第二步。也就是构造器循环依赖和field循环依赖。

那么我们要解决循环引用也应该从bean初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。

首先我们看源码,三级缓存主要指:

	/** Cache of singleton objects: bean name --> bean instance */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name --> ObjectFactory */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name --> bean instance */
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

这三级缓存分别指:
singletonObjects:单例对象的cache
singletonFactories : 单例对象工厂的cache
earlySingletonObjects :提前暴光的单例对象的Cache

我们在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。主要调用方法就是:

	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    
			synchronized (this.singletonObjects) {
    
				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;
	}

上面的代码需要解释两个参数:

  • allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象
  • isSingletonCurrentlyInCreation()判断当前单例bean是否正在创建中,也就是没有初始化完成(比如A的构造器依赖了B对象所以得先去创建B对象, 或则在A的populateBean过程中依赖了B对象,得先去创建B对象,这时的A就是处于创建中的状态。)

分析getSingleton()的整个过程,Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则:
从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。

	this.earlySingletonObjects.put(beanName, singletonObject);
	// 从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。
	this.singletonFactories.remove(beanName);

从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。这个cache的类型是ObjectFactory,定义如下:

@FunctionalInterface
public interface ObjectFactory<T> {
    

	/**
	 * Return an instance (possibly shared or independent)
	 * of the object managed by this factory.
	 * @return the resulting instance
	 * @throws BeansException in case of creation errors
	 */
	T getObject() throws BeansException;
}

调用createBeanInstance实例化后,如果bean是单例,且允许从singletonFactories获取bean,并且当前bean正在创建中,那么就把beanName放入三级缓存(singletonFactories)中:
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
		isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    
	if (logger.isDebugEnabled()) {
    
		logger.debug("Eagerly caching bean '" + beanName +
				"' to allow for resolving potential circular references");
	}
	addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

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的实例对象”这种循环依赖的情况:
在这里插入图片描述
spring初始化bean的过程如下:
1>首先尝试从一级缓存中获取serviceA实例,发现不存在并且serviceA不在创建过程中;
2>serviceA完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
3>将自己(serviceA)提前曝光到singletonFactories中;
4>此时进行初始化的第二步(注入属性serviceB),发现自己依赖对象serviceB,此时就尝试去get(B),发现B还没有被实create,所以走create流程;
5>serviceB完成了初始化的第一步(实例化:调用createBeanInstance方法,即调用默认构造方法实例化);
6>将自己(serviceB)提前曝光到singletonFactories中;
7>此时进行初始化的第二步(注入属性serviceA);
8>于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀);
9>B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中;
10>此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中;

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

4、循环依赖的N种场景

在这里插入图片描述
这里主要讲解spring是如何解决单例setter注入时的循环依赖问题,其他四种循环依赖场景后文再一一分析讲解

3.1、单例的setter注入

这种注入方式应该是spring用的最多的,代码如下:

@Service
public class ServiceA {
    

	@Autowired
	private ServiceB serviceB;
}

@Service
public class ServiceB {
    

	@Autowired
	private ServiceA serviceA;
}

spring内部有三级缓存:

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

下面用一张图告诉你,spring是如何解决循环依赖的:
图1
图1
上面(3、Spring怎么解决循环依赖)中已经讲解过spring解决单例循环依赖的过程,这里细心的朋友可能会发现在这种场景中第二级缓存(earlySingletonObjects)作用不大。
那么问题来了,为什么要用第二级缓存呢?
试想一下,如果出现以下这种情况,我们要如何处理?

@Service
public class ServiceA {
    

	@Autowired
	private ServiceB serviceB;
	
	@Autowired
	private ServiceB serviceC;
}

@Service
public class ServiceB {
    

	@Autowired
	private ServiceA serviceA;
}

@Service
public class ServiceC {
    

	@Autowired
	private ServiceA serviceA;
}

serviceA依赖于serviceB和serviceC,而serviceB依赖于serviceA,同时serviceC也依赖于serviceA。按照上图的流程可以把serviceA注入到serviceB,并且sServiceA的实例是从第三级缓存中获取的。假设不用第二级缓存,serviceA注入到serviceC的流程如图:
图2
在这里插入图片描述
serviceA注入到serviceC又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的。这样不是有问题?

为了解决这个问题,spring引入的第二级缓存。上面图1其实serviceA对象的实例已经被添加到第二级缓存中了,而在serviceA注入到TestService3时,只用从第二级缓存中获取该对象即可。
图3
在这里插入图片描述
还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?
答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。
针对这种场景spring是怎么做的呢?
答案就在AbstractAutowireCapableBeanFactory类doCreateBean方法的这段代码中:

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
		isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    
	if (logger.isDebugEnabled()) {
    
		logger.debug("Eagerly caching bean '" + beanName +
				"' to allow for resolving potential circular references");
	}
	addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

它定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象,其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象。

3.2、多例的setter注入

这种注入方法偶然会有,具体代码如下:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceA {
    

	@Autowired
	private ServiceB serviceB;
}

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceB {
    

	@Autowired
	private ServiceA serviceA;
}

可能你会认为这种情况spring容器启动会报错,其实是不会,为什么呢?其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了beanFactory.preInstantiateSingletons()方法:
在这里插入图片描述
红框的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean。而多例即SCOPE_PROTOTYPE类型的类,非单例,不会被提前初始化bean,所以程序能够正常启动。如何让它提前初始化bean呢?只需要再定义一个单例的类,在它里面注入serviceA:

@Service
public class ServiceC {
    

    @Autowired
    private ServiceA serviceA;
}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

3.3、构造器注入

这种注入方式现在其实用的已经非常少了,但是我们还是有必要了解一下,看看如下代码:

@Service
public class ServiceA {
    

	public ServiceA(ServiceB serviceB) {
    
	}
}

@Service
public class ServiceB {
    

	public ServiceB(ServiceA serviceA) {
    
	}
}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?
在这里插入图片描述
从图中的流程看出构造器注入没能添加到三级缓存,也没有使用缓存,所以也无法解决循环依赖问题。

3.4、单例的代理对象setter注入

这种注入方式其实也比较常用,比如平时使用:@Async注解的场景,会通过AOP自动生成代理对象。

@Service
@EnableAsync
public class ServiceA {
    

	@Autowired
	private ServiceB serviceB;

	@Async
	public void test1() {
    
		System.out.println("async");
	}
}

@Service
public class ServiceB {
    

	@Autowired
	private ServiceA serviceA;
}

程序启动会报错,出现了循环依赖:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name ‘serviceA’: Bean with name ‘serviceA’ has been injected into other beans [serviceB] in its raw version as part of a circular reference

为什么会循环依赖呢?答案就在下面这张图中:
在这里插入图片描述
说白了,bean初始化完成之后,后面还有一步去检查:第二级缓存是否存在, 代理对象和原始对象是否相等。
在这里插入图片描述
到此发现第二级缓存存在并且代理对象和原始对象是不相等,因此抛出上面异常。

如果这时候把ServiceA改个名字,改成:ServiceC,其他的都不变。

@Service
public class ServiceB {
    

	@Autowired
	private ServiceC serviceC;
}

@Service
@EnableAsync
public class ServiceC {
    

	@Autowired
	private ServiceB serviceB;

	@Async
	public void test1() {
    
		System.out.println("async");
	}
}

再重新启动一下程序,成功了,没有报错了!!!
这又是为什么?
这就要从spring的bean加载顺序说起了,默认情况下,spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以ServiceA比ServiceB先加载,而改了文件名称之后,ServiceB比ServiceC先加载。为什么ServiceB比ServiceC先加载就没问题呢?答案在下面这张图中:
在这里插入图片描述
这种情况ServiceC第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

3.5、DependsOn循环依赖

还有一种有些特殊的场景,比如我们需要在实例化Bean serviceA之前,先实例化Bean serviceB,这个时候就可以使用@DependsOn注解。

@Service
@DependsOn("serviceB")
public class ServiceA {
    
	
}

@Service
@DependsOn("serviceA")
public class ServiceB {
    
	
}

程序启动之后,执行结果:

Circular depends-on relationship between ‘serviceB’ and ‘serviceA’

这个例子中本来如果ServiceA和ServiceB都没有加@DependsOn注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?

答案在AbstractBeanFactory类的doGetBean方法的这段代码中:
在这里插入图片描述
初始化bean前,它会检查dependsOn的实例有没有循环依赖,如果有循环依赖则抛异常。

5、出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是spring默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:
在这里插入图片描述

5.1、生成代理对象产生的循环依赖

生成代理对象产生的循环依赖这类循环依赖问题解决方法很多,主要有:
1、使用@Lazy注解,延迟加载
2、使用@DependsOn注解,指定加载先后关系
3、修改文件名称,改变循环依赖类的加载顺序

5.2、DependsOn循环依赖

使用@DependsOn产生的循环依赖这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

5.3、多例循环依赖

多例循环依赖这类循环依赖问题可以通过把bean改成单例的解决。

5.3、构造器循环依赖

构造器循环依赖这类循环依赖问题可以通过使用@Lazy注解解决。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wujun2412/article/details/123392678

智能推荐

互联网短信网关接口协议_binggongchang的博客-程序员宝宝

          中国移动通信互联网短信网关接口协议(China Mobile Peer to Peer, CMPP)(V2.0)              中国移动通信集团公司2002年 4  月     目   录前   言.............

ImageView&amp; 使用第三方库加载网络图片_周末的丢的博客-程序员宝宝

ImageViewActivity和它对应的xml文件页面布局&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddin

Bit Twiddling Hacks_weixin_33971130的博客-程序员宝宝

http://graphics.stanford.edu/~seander/bithacks.htmlBit Twiddling HacksBy Sean Eron [email protected], the code snippets here are in the public domain (unless otherwis...

3-2. On the Phone_OliveLao的博客-程序员宝宝

3-2. On the Phone Linda: Hello, this is Lindaspeaking. How can I help you? Eric: Hello, I would liketo speak to Ms. Johnson, please! L: May I ask who’s calling,please? E: This is Eric

C++排序算法_HQF1998的博客-程序员宝宝

C++排序算法冒泡排序选择排序插入排序归并排序快速排序堆排序完整代码GitHub冒泡排序/*冒泡排序*/void BriSort(int data[], int len){ bool flag = false;//实测加标志变量对于随机数排序时间变多了,对于交换变量是很多内容的结构体时加标志变量会缩短时间 for(int i = 0; i &lt; len - 1; i++) {...

随便推点

Docker专题系列之一:安装+使用+常用命令_行者张良的博客-程序员宝宝_docker专题系列之

1.docker 安装Docker 要求 CentOS 系统的内核版本高于 3.10通过 uname -r 命令查看你当前的内核版本[[email protected] /]# uname -r3.10.0-1062.1.2.el7.x86_64安装一些必要的系统工具:命令:sudo yum install -y yum-utils device-mapper-persistent-data lvm2添加软件源信息:sudo yum-config-manager --add-re

阿里研究院崔瀚文:新零售的上下左右前后_weixin_34345560的博客-程序员宝宝

新零售走过一年,新物种、新业态如盒马鲜生、无人货架、天猫小店等不断涌现,层出不穷。无论是最初的“线上线下+物流”,还是阿里研究院报告中的“大数字驱动的人、货、场重构”、“数字驱动的、以消费者为中心的泛零售业态”,都让人们对于新零售抱有更大的兴趣。众多快速变化的现象和新零售布局的背后,是否有共通之处?新零售革命究竟因何起,如何承,何处转,怎样合?阿里提出...

学习笔记(04):150讲轻松搞定Python网络爬虫-bs4-find和find_all方法_kingx3的博客-程序员宝宝

【课程介绍】 本课程总体分成五大模块,分别是网络请求、数据解析、数据存储、爬虫进阶、Scrapy框架和分布式爬虫,包含了一个爬虫工程师需要掌握的几乎所有技能,知识点非常体系。实战部分都是紧贴知识点,即学即用,紧跟潮流。课程还配有许多作业,通过作业可以让学生实现真正把技术转成自己的技能的目的。 【课程内容包括】 共150讲课程+...

本地服务注册远程nacos遇到的坑,浏览器可以访问nacos控制页面,但本地服务无法注册_ybp500234的博客-程序员宝宝_本地服务注册远程nacos

我是将nacos部署到服务器上,然后将本地服务往远程nacos上注册,本地服务配置如下:spring: application: name: gateway1 profiles: active: dev cloud: nacos: config: server-addr: 远程IP:8848 file-extension: yml启动本地服务的时候老是报错,但是我之前nacos服务中心在本地启动这样配置就没有问题:202

正则库相关及windows-VS-C++环境下pcre && pcre++的编译和使用_songmeixu的博客-程序员宝宝_c++ pcre++ include

C++标准缺少正则的良好支持,需要自己安装库,比较流行的库有GNU Regex Library、Boost.Regex、PCRE、GRETA,对这些库的介绍网上很多,可以参考点击打开链接;这些库的性能比较,我认为没有绝对的优劣,可以参考点击打开链接我个人选择使用已经顺手的perl兼容的“PCRE”,但其为C编写,使用不方便,好在有人为其包了C++的interface,即PCRE++。但

推荐文章

热门文章

相关标签