spring如何解决循环依赖

2年前 (2022) 程序员胖胖胖虎阿
223 0 0

文章目录

  • 1、什么是循环依赖?
  • 2、怎么检测循环依赖
  • 3、Spring怎么解决循环依赖
  • 4、循环依赖的N种场景
    • 3.1、单例的setter注入
    • 3.2、多例的setter注入
    • 3.3、构造器注入
    • 3.4、单例的代理对象setter注入
    • 3.5、DependsOn循环依赖
  • 5、出现循环依赖如何解决?
    • 5.1、生成代理对象产生的循环依赖
    • 5.2、DependsOn循环依赖
    • 5.3、多例循环依赖
    • 5.3、构造器循环依赖

这里我们主要分析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如何解决循环依赖

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

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

2、怎么检测循环依赖

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

3、Spring怎么解决循环依赖

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

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

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

spring如何解决循环依赖
从上面讲述的单例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如何解决循环依赖
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如何解决循环依赖
这里主要讲解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
spring如何解决循环依赖
上面(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
spring如何解决循环依赖
serviceA注入到serviceC又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的。这样不是有问题?

为了解决这个问题,spring引入的第二级缓存。上面图1其实serviceA对象的实例已经被添加到第二级缓存中了,而在serviceA注入到TestService3时,只用从第二级缓存中获取该对象即可。
图3
spring如何解决循环依赖
还有个问题,第三级缓存中为什么要添加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()方法:
spring如何解决循环依赖
红框的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始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

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

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

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

如果这时候把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先加载就没问题呢?答案在下面这张图中:
spring如何解决循环依赖
这种情况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方法的这段代码中:
spring如何解决循环依赖
初始化bean前,它会检查dependsOn的实例有没有循环依赖,如果有循环依赖则抛异常。

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

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

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

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

5.2、DependsOn循环依赖

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

5.3、多例循环依赖

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

5.3、构造器循环依赖

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

版权声明:程序员胖胖胖虎阿 发表于 2022年10月12日 上午1:56。
转载请注明:spring如何解决循环依赖 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...