在Spring框架中,处理循环依赖一直是一个备受关注的话题。这是因为Spring源代码中为了解决循环依赖问题,进行了大量的处理和优化。同时,循环依赖也是Spring高级面试中的必考问题,回答得好可以成为面试中的必杀技。因此,本文旨在为大家提供深入了解Spring的循环依赖及其解决方案的资料,让读者能够在日后的面试中更有把握地回答相关问题!

一、什么是循环依赖

循环依赖其实就是循环引用,也就是一个或多个以上的对象互相持有对方,最终形成闭环,形成一个无限循环的依赖关系。比如 A依赖于A本身(左图),A依赖于B,B也依赖与A(中),A依赖B,B依赖C,C又依赖A(右图)。

如果不考虑Spring,循环依赖并不是问题,因为对象之间相互依赖是很正常的事情,后面我们都以A和B的相互循环依赖进行举例:

A a = new A();B b = new B();a.b = b;b.a = a;

然而,Spring的循环依赖通常被单独拎出来谈论,也经常在面试中被提及。这是因为 Spring 中对象的创建和管理是由 IOC 控制的,一个对象的创建不仅仅是简单地调用 new,而是经过了一系列 Bean 的生命周期。因此,循环依赖问题也就会随之而来。当然,Spring 中存在许多场景会导致循环依赖,有些场景 Spring 能够解决,而有些场景则需要程序员手动解决。

要深刻理解 Spring 中的循环依赖问题,首先需要理解 Spring 中 Bean 的生命周期。

二、Bean的生命周期

Spring Bean的生命周期来说,可以分为四个主要阶段:实例化、属性复制、初始化、销毁,其中经过初始化以后这个bean就被创建完成可以供使用了。

具体的步骤:

  • 实例化:实例化一个 Bean 对象
  • 属性赋值:为 Bean 设置相关属性和依赖注入
  • 初始化:初始化的阶段的步骤比较多,5和6 步是进行真正的初始化,而第 3和4 步为在初始化前执行,第 7 步在初始化后执行,如果原始对象中的某个方法被 AOP 了,那么这一步需要根据原始对象生成一个代理对象,最终生成的代理对象放入单例池,Bean 就可以被使用了。
  • 销毁:第 8~10 步,第 8 步其实也可以算到销毁阶段,但不是真正意义上的销毁,而是先在使用前注册了销毁的相关调用接口,为了后面第 9、10 步真正销毁 Bean 时再执行相应的方法

我们可以发现,在第2步中,Spring 需要给对象中的属性进行依赖注入,那么这个注入过程是怎样的?

还是以A和B两个类相互依赖来举例子,A 类中存在一个 B 类的 b 属性,所以当 A 类生成了一个原始对象之后,就需要去给 b 属性去赋值(依赖注入),此时就会根据 b 属性的类型和属性名去 BeanFactory 中去获取 B 类所对应的单例bean。如果此时 BeanFactory 中存在 B 对应的 Bean,那么直接拿来赋值给 b 属性就好了;但是如果此时 BeanFactory 中不存在 B 对应的 Bean,则需要生成一个 B 对应的 Bean,然后赋值给 b属性。

问题的关键点就在于第二种情况,此时 B 类在 BeanFactory 中还没有生成对应的 Bean,那么就需要去生成,就会经过 B 的 Bean 的生命周期。那么在创建 B 类的 Bean 的过程中,如果 B 类中还存在一个 A 类的 a 属性,那么在创建 B 的 Bean 的过程中就需要 A 类对应的 Bean。但是,B 类 Bean创建完成的条件是 A 类 Bean 在创建过程中进行依赖注入,所以这里就出现了循环依赖

在Spring中,是通过三级缓存来解决循环依赖问题的,那么什么是三级缓存?

三、三级缓存

Spring的三级缓存是指在使用Spring框架进行Bean的创建和管理时,Spring在其内部维护了三级缓存,用于提高Bean的创建和获取效率。这三级缓存分别是singletonObjects、earlySingletonObjects和singletonFactories:singletonObjects用于缓存完全初始化后的单例Bean实例,earlySingletonObjects用于缓存尚未完全初始化的单例Bean实例,而singletonFactories则用于缓存Bean工厂对象,即可以生成Bean实例的工厂方法。

/** Cache of singleton objects: bean name --> bean instance */private final Map singletonObjects = new ConcurrentHashMap(256);/** Cache of early singleton objects: bean name --> bean instance */private final Map earlySingletonObjects = new HashMap(16);/** Cache of singleton factories: bean name --> ObjectFactory */private final Map<String, ObjectFactory一级缓存 

singletonObjects:key -> beanName,value -> 完整bean对象

一级缓存的是经历了完整的Bean生命周期的Bean。

二级缓存

earlySingletonObjects:key -> beanName,value -> 不完整(属性信息未填充完毕)bean对象

二级缓存用于存放未完成Bean生命周期的半成品Bean。在出现循环依赖时,这些Bean已经被提前放入二级缓存。如果需要进行AOP处理,则其代理对象也已经被放入缓存。

三级缓存

singletonFactories:key -> beanName,value -> bean工厂对象

缓存的是ObjectFactory , 也就是一个Lambda表达式 , 这就一个方法getObject,返回bean对象或bean代理对象,用于解决被代理增强的循环依赖

四、循环依赖的解决(源码分析)

我们已经知道,问题的关键在于A在创建的时候需要将B注入到A中,而注入B需要先创建B,创建B的时候发现需要将A注入到B中,产生了先有鸡还有现有蛋的问题。Spring解决这个问题的思想就是在实例化过程中,提前办成品bean放入缓存,在依赖注入的时候允许将半成品进行注入:
实例化A -> a的半成品写入缓存 -> 属性注入B -> B还没有实例化,需要先进行实例化B -> 实例化B -> 没有A,但是有A的半成品 -> 注入A的半成品 -> 实例化B成功 -> 实例化A成功

上面就是核心思想,下面将会结合源码进行具体分析其中的原理。

4.1 普通循环依赖

还是使用之前的例子来举例:

@Componentpublic class A {// A中注入了B@Autowiredprivate B b;}@Componentpublic class B {// B中也注入了A@Autowiredprivate A a;}

A和B进行初始化的过程大致如下:

  1. 使用getBean(A.class),首先获取容器内的单例A(若beanA不存在,就会走A的创建流程,有的话会直接从缓存里面获取),显然初次获取beanA是不存在的,因此会去创建A
  2. 实例化bean A,并将实例化后的A放入到三级缓存中,此时beanA.b == null
  3. 对A的属性进行依赖注入:@Autowired依赖注入beanB(此时需要去容器内获取beanB),通过getBean(B)去容器内找B,如果能找到B的实例的话就可以直接注入。但此时B在容器内或者缓存内不存在,因此需要创建B的bean。
  4. 实例化bean B,并将实例化后的A放入到二级缓存中,此时beanB.A == null
  5. 对B属性注入:@Autowired依赖注入beanA(此时需要去容器内获取beanA),这时会调用getBean(A)去容器内找到beanA。一级缓存不存在A,在二级缓存中也不存在A,三级缓存中存在A的ObjectFactory,此时会调用getEarlyBeanReference()方法得到A的实例化后的结果,将这个半成品bean放入到二级缓存中,同时将三级缓存中A的ObjectFactory删除掉。
  6. 将二级缓存中的半成品A注入到B中,B完成后续的初始化过程,最终放入到一级缓存中。
  7. A也在缓存中可以拿到了B的bean,将B注入到A中。
  8. A和B都创建完成。


到这里为止有两个问题需要讨论一下,包括我可能存在疑问:

在上面的第6步中,将一个没有经过初始化的A类型对象提前注入B中不会有问题吗?不应该注入一个完整的A吗?

这样做是不会出问题的,虽然给B注入的是一个还未初始化的A对象,也就是半成品A,但是在创建A的流程中一直使用的是注入到B中的A对象的引用,之后会根据这个引用对A进行初始化,通过这个引用最后获取到的还是成品的A,所以这是没有问题的。

为什么三级缓存中存放的是ObjectFactory而不是bean呢?直接将半成品的bean放入到缓存中不可以么?为什么要对此一举再使用三级缓存存放ObjectFactory呢?

这个其实涉及到了下一个要讨论的问题,主要是因为三级缓存实际上跟Spring中的AOP相关,我们继续往下看吧。

4.2 有AOP的循环依赖

到目前为止,发现似乎不需要三级缓存,直接使用二级缓存貌似也能解决问题:

  • 实例化A,将实例化后的半成品放入到二级缓存
  • 实例化B,从缓存中获取半成品A,完成依赖注入
  • 初始化B,得到B的bean
  • 将B注入A,完成A和B的初始化

还记得第二部分Bean的生命周期吗,在生命周期的初始化过程中,如果有AOP的话需要执行相应的方法。还是A和B两个类相互依赖的场景,但是A类中多了一层AOP,也就是A类的原始对象赋值给B时 , 进行了AOP , 那么A进行AOP之后,它的真实对象是代理对象 , 那么B中A的值是原始对象的值 ,那么就会产生B的A值和A的实际值不符的问题,Spring解决的办法就是使用三级缓存。如果我们使用了三级缓存,就可以实现对A的提前AOP,将B真实依赖的A注入到B中。

那改变Bean的生命周期可以么?先AOP再放入缓存中呢?

如果这么做了,就把AOP中创建代理对象的时机提前了,不管是否发生循环依赖,都在doCreateBean方法中完成了AOP的代理。不仅没有必要,而且违背了Spring在结合AOP跟Bean的生命周期的设计。

五、哪种情况的循环可以解决

循环依赖问题在Spring中主要有三种情况:

  • 通过构造方法进行依赖注入时产生的循环依赖问题。
  • 通过setter方法进行依赖注入且是在多例模式下产生的循环依赖问题。
  • 通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

在Spring中,只有第三种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。

多例模式不可以:只有单例bean才有支持循环依赖的可能,非单例的bean不支持循环依赖,会陷入死循环。

构造方法注入不可以:如果主bean对象通过构造函数方式注入所依赖的bean对象,则无论所依赖的bean对象通过何种方式注入主bean,都无法解决循环依赖问题,程序无法启动。主要原因是主bean对象通过构造函数注入所依赖bean对象时,无法创建该所依赖的bean对象,获取该所依赖bean对象的引用。

@Async导致无法支持循环依赖:@Async 标记的类是通过 AbstractAdvisingBeanPostProcessor 来生成代理的,AbstractAdvisingBeanPostProcessor 没有实现 SmartInstantiationAwareBeanPostProcessor

六、源码分析

现在进入源码分析部分。下面按照方法的调用顺序,依次来看一下循环依赖相关的代码。

获取单例Bean的源码

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {...@Override@Nullablepublic Object getSingleton(String beanName) {return getSingleton(beanName, true);}@Nullableprotected 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 getEarlyBeanReference(beanName, mbd, bean));}Object exposedObject = bean; //exposedObject 是最终返回的对象...// 填充属于,解决@Autowired依赖populateBean(beanName, mbd, instanceWrapper);// 执行初始化回调方法们exposedObject = initializeBean(beanName, exposedObject, mbd);// earlySingletonExposure:如果你的bean允许被早期暴露出去 也就是说可以被循环引用那这里就会进行检查// 此段代码非常重要,但大多数人都忽略了它if (earlySingletonExposure) {// 此时一级缓存肯定还没数据,但是呢此时候二级缓存earlySingletonObjects也没数据//注意,注意:第二参数为false表示不会再去三级缓存里查了// 此处非常巧妙的一点:因为上面各式各样的实例化、初始化的后置处理器都执行了,如果你在上面执行了这一句//((ConfigurableListableBeanFactory)this.beanFactory).registerSingleton(beanName, bean);// 那么此处得到的earlySingletonReference 的引用最终会是你手动放进去的Bean最终返回,完美的实现了"偷天换日" 特别适合中间件的设计// 我们知道,执行完此doCreateBean后执行addSingleton()其实就是把自己再添加一次,再一次强调,完美实现偷天换日Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {// 这个意思是如果经过了initializeBean()后,exposedObject还是木有变,那就可以大胆放心的返回了// initializeBean会调用后置处理器,这个时候可以生成一个代理对象,那这个时候它哥俩就不会相等了 走else去判断吧if (exposedObject == bean) {exposedObject = earlySingletonReference;} // allowRawInjectionDespiteWrapping这个值默认是false// hasDependentBean:若它有依赖的bean 那就需要继续校验了(若没有依赖的 就放过它)else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {// 拿到它所依赖的Bean们,下面会遍历一个一个的去看String[] dependentBeans = getDependentBeans(beanName);Set actualDependentBeans = new LinkedHashSet(dependentBeans.length);// 一个个检查它所以Bean// removeSingletonIfCreatedForTypeCheckOnly这个放见下面在AbstractBeanFactory里面// 简单的说,它如果判断到该dependentBean并没有在创建中的了的情况下,那就把它从所有缓存中移除, 并且返回true// 否则(比如确实在创建中) 那就返回false 进入我们的if里面~表示所谓的真正依赖//(解释:就是真的需要依赖它先实例化,才能实例化自己的依赖)for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}// 若存在真正依赖,那就报错(不要等到内存移除你才报错,那是非常不友好的) // 这个异常是BeanCurrentlyInCreationException,报错日志也稍微留意一下,方便定位错误if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}}return exposedObject;}// 虽然是remove方法 但是它的返回值也非常重要// 该方法唯一调用的地方就是循环依赖的最后检查处protected boolean removeSingletonIfCreatedForTypeCheckOnly(String beanName) {// 如果这个bean不在创建中比如是ForTypeCheckOnly的那就移除掉if (!this.alreadyCreated.contains(beanName)) {removeSingleton(beanName);return true;}else {return false;}}}protected  T doGetBean(...){... // 标记beanName a是已经创建过至少一次的,它会一直存留在缓存里不会被移除(除非抛出了异常)// 参见缓存Set alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap(256))if (!typeCheckOnly) {markBeanAsCreated(beanName);}// 此时a不存在任何一级缓存中,且不是在创建中所以此处返回null// 此处若不为null,然后从缓存里拿就可以了(主要处理FactoryBean和BeanFactory情况吧)Object beanInstance = getSingleton(beanName, false);...// 这个getSingleton方法非常关键。//1、标注a正在创建中//2、调用singletonObject = singletonFactory.getObject();(实际上调用的是createBean()方法)因此这一步最为关键//3、此时实例已经创建完成会把a移除整整创建的缓存中//4、执行addSingleton()添加进去。(备注:注册bean的接口方法为registerSingleton,它依赖于addSingleton方法)sharedInstance = getSingleton(beanName, () -> { ... return createBean(beanName, mbd, args); });}

getSingleton(beanName, singletonFactory)源码

public Object getSingleton(String beanName, ObjectFactory singletonFactory) {Assert.notNull(beanName, "Bean name must not be null");synchronized (this.singletonObjects) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {// ....// 省略异常处理及日志// ....// 在单例对象创建前先做一个标记// 将beanName放入到singletonsCurrentlyInCreation这个集合中// 标志着这个单例Bean正在创建// 如果同一个单例Bean多次被创建,这里会抛出异常beforeSingletonCreation(beanName);boolean newSingleton = false;boolean recordSuppressedExceptions = (this.suppressedExceptions == null);if (recordSuppressedExceptions) {this.suppressedExceptions = new LinkedHashSet();}try {// 上游传入的lambda在这里会被执行,调用createBean方法创建一个Bean后返回singletonObject = singletonFactory.getObject();newSingleton = true;}// ...// 省略catch异常处理// ...finally {if (recordSuppressedExceptions) {this.suppressedExceptions = null;}// 创建完成后将对应的beanName从singletonsCurrentlyInCreation移除afterSingletonCreation(beanName);}if (newSingleton) {// 添加到一级缓存singletonObjects中addSingleton(beanName, singletonObject);}}return singletonObject;}}

doCreateBean源码

protected Object doCreateBean(){...// 使用构造器/工厂方法 instanceWrapper是一个BeanWrapperinstanceWrapper = createBeanInstance(beanName, mbd, args);// 此处bean为"原始Bean" 也就是这里的A实例对象:beanA@1final Object bean = instanceWrapper.getWrappedInstance();...// 是否要提前暴露(允许循环依赖)现在此处A是被允许的boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));// 允许暴露,就把A绑定在ObjectFactory上,注册到三级缓存`singletonFactories`里面去保存着// Tips:这里后置处理器的getEarlyBeanReference方法会被促发,自动代理创建器在此处创建代理对象(注意执行时机 为执行三级缓存的时候)if (earlySingletonExposure) {addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}...// exposedObject 为最终返回的对象,此处为原始对象bean也就是beanA@1,下面会有用处Object exposedObject = bean; // 给A@1234属性完成赋值,@Autowired在此处起作用// 因此此处会调用getBean("b"),so 会重复上面步骤创建B类的实例// 此处我们假设B已经创建好了 为beanB@2// 需要注意的是在populateBean("b")的时候依赖有beanA,所以此时候调用getBean("a")最终会调用getSingleton("a"),//此时候上面说到的getEarlyBeanReference方法就会被执行。这也解释为何我们@Autowired是个代理对象,而不是普通对象的根本原因populateBean(beanName, mbd, instanceWrapper);// 实例化。这里会执行后置处理器BeanPostProcessor的两个方法// 此处注意:postProcessAfterInitialization()是有可能返回一个代理对象的,这样exposedObject 就不再是原始对象了需要特别注意// 比如处理@Aysnc的AsyncAnnotationBeanPostProcessor它就是在这个时间里生成代理对象的(有坑,请小心使用@Aysnc)exposedObject = initializeBean(beanName, exposedObject, mbd);... // 至此,相当于beanA@1已经实例化完成、初始化完成(属性也全部赋值了)// 这一步我把它理解为校验:校验:校验是否有循环引用问题if (earlySingletonExposure) {// 注意此处第二个参数传的false,表示不去三级缓存里singletonFactories再去调用一次getObject()方法了// 上面建讲到了由于B在初始化的时候,会触发A的ObjectFactory.getObject()所以a此处已经在二级缓存earlySingletonObjects里了// 因此此处返回A的实例:beanA@1Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {// 这个等式表示,exposedObject若没有再被代理过,这里就是相等的// 显然此处我们的a对象的exposedObject它是没有被代理过的所以if会进去// 这种情况至此,就全部结束了if (exposedObject == bean) {exposedObject = earlySingletonReference;}// 继续以A为例,比如方法标注了@Aysnc注解,exposedObject此时候就是一个代理对象,因此就会进到这里来//hasDependentBean(beanName)是肯定为true,因为getDependentBeans(beanName)得到的是["b"]这个依赖else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {String[] dependentBeans = getDependentBeans(beanName);Set actualDependentBeans = new LinkedHashSet(dependentBeans.length);// beanA@1依赖的是["b"],所以此处去检查b// 如果最终存在实际依赖的bean:actualDependentBeans不为空 那就抛出异常证明循环引用了for (String dependentBean : dependentBeans) {// 这个判断原则是:如果此时候b并还没有创建好,this.alreadyCreated.contains(beanName)=true表示此bean已经被创建过,就返回false// 若该bean没有在alreadyCreated缓存里,就是说没被创建过(其实只有CreatedForTypeCheckOnly才会是此仓库)if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}}}

七、常见面试题

7.1 Spring是如何解决循环依赖的

Spring使用了三级缓存来解决循环依赖。其中一级缓存存放完整bean对象,二级缓存存放半成品bean,三级缓存为对象工厂。

当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂并添加到三级缓存中。如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,通过这个工厂获取到的就是A实例化的对象。

当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束。

7.2 为什么需要三级缓存,二级缓存可以么?

不可以,主要为了解决AOP生产代理对象问题的。如果存在代理,三级没有问题,二级就不行了。因为三级缓存中放的是⽣成具体对象的匿名内部类,获取 Object 的时候,它可以⽣成代理对象,也可以返回普通对象。使⽤三级缓存主要是为了保证不管什么时候使⽤的都是⼀个对象。

如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则。

7.3 Spring能自动解决全部的循环依赖情况么?

  1. 只有单例bean才有支持循环依赖的可能,非单例的bean不会出现循环依赖。
  2. 如果存在循环依赖,且都是通过构造函数依赖的,这种情况下的循环依赖是无法解决的。

参考文献:

Spring 轻度解析之循环依赖源码解析
面试必杀技,讲一讲Spring中的循环依赖
Spring中的循环依赖
聊透Spring循环依赖