由浅入深剖析高频面试题:Spring 如何解决循环依赖?


在参加互联网公司的面试时,面试官问道:“请问在Spring中,如何通过解决循环依赖的问题?

这是一个典型的高频面试题,因为一方面可以看是否对整个创建过程和原理了解,另一方面也可以看是否有自己的思考,还可以可以就此来深入源码讨论。

「前边的当我没说,其实最主要是考察下八股文背的好不好 =_= 」

今天主要针对这个问题,从是什么,为什么,怎么解决,然后结合源码进行讲述。最后补充一下有哪些注意事项。

引:循环依赖是什么

你依赖我,我又依赖你,形成了依赖环路。
在这里插入图片描述
以下是一个简单的循环依赖图示例:

class A {
    private B b;
    public A(B b) {
        this.b = b;
    }
}

class B {
    private A a;
    public B(A a) {
        this.a = a;
    }
}

A a = new A(new B(new A(null)));

在这个示例中,A和B类相互依赖,创建A实例时需要B实例,而创建B实例时需要A实例。

what 循环依赖会产生什么问题

这种循环依赖会导致创建实例时出现死循环,因为创建A实例需要B实例,而创建B实例需要A实例,两者互相等待对方创建完成,导致无法正常创建实例。

why 循环依赖为什么会产生这个问题

我们看下Spring 对象的创建,前边的博客里我们有提到,创建过程中包含「对象创建」和「属性填充」。

可以理解为第一步对象创建是 A a = new A() 第二步属性填充是 a.setB()。

第一步里,我们是通过对象的实例化是通过反射实现的。

第二步里,在a.setB()的过程,发现B还没有呢,就去创建B。然后创建B的过程,又发现需要A。而A也还没创建完成呢。 这咋办?

how 能解决么

分析不同情况

这个问题要分情况讨论了。

先说结论:当Bean 都是单例,并且是通过set方式注入的情况是可以解决的。分析如下:

单例多例情况

如果是Bean 都是单例

我可以通过缓存来解决。怎么理解呢,就是A和B都只有一个,A用B的时候,B虽然依赖A A没有创建好,但是没关系,B已经知道A 就这一个,我知道A 在哪,那B 就可以先用上没创建好的A 。流程照样可以进行

如果Bean 是多例

那这个问题就没法解决了。因为多例不能用缓存。每次创建都需要一个新的。这里不理解也没关系,后边会仔细讲。

构造方式和注入方式

构造方式

那也没办法解决,因为在使用构造注入方式时,对象的实例化和属性的填充是同时进行的,没办法用上缓存了。

set方式

使用set注入方式时,对象的实例化和属性的填充是分开进行的,因此可以通过缓存来解决循环依赖问题。

怎么通过缓存解决这个问题

前提:再次强调一下,经过上面的分析,这里我们讨论的范畴集中在 Spring如何通过三级缓存来解决

单例Ben】 且【用set方式】导致的循环依赖的问题。因为别的场景,它解决不了。

那这个三级缓存怎么用的?

首先,单例的对象一定有一个缓存池,存放已经创建好的对象。这样当我们第二次创建这个对象的时候,它就直接可以从这个缓存池里来拿对象了。即一级:存放成品对象的池子

其次,如果我去拿单例A的时候,发现没有成品A,那我就去创建么?不,我还需要检查一下这个A 目前是不是已经在创建中了,比如这个对象已经初始化了,但是还没设置属性。这样我就可以先用这个创建中的A,然后后续等它属性设置好不就行了。即二级: 就是存放对象初始化好但是还没有给属性赋值的对象。

然后,如果我发现这个对象既没有一个成品,也没有在创建中。那我就去创建么?不。我还要再去看看创建这个对象的工厂是不是有了。如果已经有了,那我也先用这个工厂。即三级:存放创建这个对象的工厂。

流程

那么这三个缓存怎么配合使用呢?如图所示:
在这里插入图片描述

1 、是尝试从缓存中获取目标对象,如果没有获取到,则尝试获取半成品的目标对象;如果第一个步骤没有获取到目标对象的实例,那么就进入第2个步骤

2 、尝试创建目标对象,并且为该对象注入其所依赖的属性。

这里其实就是主干逻辑。

创建A对象的实例,然后递归的调用获取been, 尝试获取B对象的实例以注入到A对象中。

3 、此时由于Spring容器中也没有B对象的成品或半成品,因而会走到方框里,创建B对象的实例

创建完成之后,尝试获取其所依赖的A的实例作为其属性,此时需要注意的是,在前面由于已经有了一个半成品的A对象的实例,因而这个时候,再尝试获取A对象的实例的时候,从三级缓存中找到了一个半成品的A对象的实例,然后将该实例返回,并且将其注入到B对象的属性a中

此时B对象实例化完成。

4 、然后,将实例化完成的B对象递归的返回,此时就会将该实例注入到A对象中,这样就得到了一个成品的A对象。

源码

对应源码部分:
在这里插入图片描述

具体代码:抓住主干不迷路
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

doGetBean 入口

—>getSingleton

—>createBean

———> doCreateBean(beanName, mbdToUse, args)

整体 doGetBean

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
	@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    ...
    // 1 检查3个缓存中是否有
    Object sharedInstance = getSingleton(beanName);
    ...
	// 2 没有且是单例时 创建
	if (mbd.isSingleton()) {
		sharedInstance = getSingleton(beanName, () -> {
			...
		return createBean(beanName, mbd, args);
			...
		});
	    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
	}
      ...
    return (T) bean;
}

第1步:从缓存获取的逻辑 getSingleton

/**
	 * Return the (raw) singleton object registered under the given name.
	 * <p>Checks already instantiated singletons and also allows for an early
	 * reference to a currently created singleton (resolving a circular reference).
	 * @param beanName the name of the bean to look for
	 * @param allowEarlyReference whether early references should be created or not
	 * @return the registered singleton object, or {@code null} if none found
	 */
	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
       // 一级缓存 singletonObjects
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
           // 二级缓存 earlySingletonObjects
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
            // 三级缓存 singletonFactories
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

第2步: createBean→ doCreateBean

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
  ...
  Object beanInstance = doCreateBean(beanName, mbdToUse, args);
  ...
  return beanInstance;
}

第2.1 doCreateBean 处理缓存并递归调用

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {
   // 实例化当前尝试获取的bean对象,比如A对象和B对象都是在这里实例化的
   // Instantiate the bean.
	BeanWrapper instanceWrapper = null;
	if (mbd.isSingleton()) {
		instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
      //判断Spring是否配置了支持提前暴露目标bean,也就是是否支持提前暴露半成品的bean
   if (instanceWrapper == null) {
		instanceWrapper = createBeanInstance(beanName, mbd, args);
	}
 ...
    // 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));
    // 如果支持,这里就会将当前生成的半成品的bean放到singletonFactories中,这个singletonFactories
    // 就是前面第一个getSingleton()方法中所使用到的singletonFactories属性,也就是说,这里就是
    // 封装半成品的bean的地方。而这里的getEarlyBeanReference()本质上是直接将放入的第三个参数,也就是
    // 目标bean直接返回
   if (earlySingletonExposure) {
		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
	}

   // Initialize the bean instance.
	Object exposedObject = bean;
	try {
   // 在初始化实例之后,这里就是判断当前bean是否依赖了其他的bean,如果依赖了,
   // 就会递归的调用getBean()方法尝试获取目标bean
	populateBean(beanName, mbd, instanceWrapper);
	exposedObject = initializeBean(beanName, exposedObject, mbd);
	}
    ...
    return exposedObject;
 }

引申

为什么需要三级缓存,二级缓存不行么?

循环依赖是否一定需要三级缓存来解决?
答:不一定,但三级缓存会更合适,风险更小。这是为什么呢?

首先:二级缓存能否解决循环依赖? 可以,但风险比三级缓存更大

重点看:第二级缓存用来干嘛的? 存放半成品的引用,可能产生多对象循环依赖,第三级缓存产生引用后,后续的就可以直接注入该引用

多例、构造器注入为什么不能解决循环依赖?

从根本上说,因为循环依赖的原理的实例化后提前暴露的引用,这两种情况都还还没实例化

其他建议

另外为了避免循环依赖导致启动问题而又不会解决,有如下建议:

业务代码中为了简洁,尽量使用field注入而非setter方法注入
若你注入的同时,立马需要处理一些逻辑(一般见于框架设计中,业务代码中不太可能出现),可以使用setter方法注入辅助完成

总结

本篇博客主要讲一个经典面试题,Spring如何解决三级缓存问题。

Spring在实例化bean时,会先创建当前bean对象,放入缓存中,然后以递归的方式获取所依赖的属性。当注入属性时,如果出现了循环依赖则会从缓存中获取依赖对象。

另外从流程到源码做了分析,希望对大家有所帮助。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值