这是笔者从两道面试题出发的思考,如果有不对的地方,还请指正,仅供参考
Q:讲一讲spring 的循环依赖
循环依赖(Circular Dependency)指的是在对象之间互相依赖的情况。例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环。Spring 框架中主要处理的是单例(singleton)作用域的循环依赖问题,而不能处理原型(prototype)作用域的循环依赖,也无法解决构造器注入的循环依赖。原因后面分析。
Spring 解决循环依赖的方法:三级缓存
一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化)。单例池,为"Spring 的单例属性"⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。 三级缓存(singletonFactories):存放ObjectFactory,ObjectFactory的getObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。 三级缓存如何解决循环依赖?
首先要清楚 Spring 创建 Bean 的流程:
先去 一级缓存 singletonObjects 中获取,存在就返回;Q:讲一讲spring 的循环依赖
循环依赖(Circular Dependency)指的是在对象之间互相依赖的情况。例如,Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,形成了一个循环。Spring 框架中主要处理的是单例(singleton)作用域的循环依赖问题,而不能处理原型(prototype)作用域的循环依赖,也无法解决构造器注入的循环依赖。原因后面分析。
Spring 解决循环依赖的方法:三级缓存
- 一级缓存(singletonObjects) :存放最终形态的 Bean(已经实例化、属性填充、初始化)。单例池,为"Spring 的单例属性"⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
- 二级缓存(earlySingletonObjects) :存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中
ObjectFactory
产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()
都是会产生新的代理对象的。 - 三级缓存(singletonFactories) :存放
ObjectFactory
,ObjectFactory
的getObject()
方法(最终调用的是getEarlyBeanReference()
方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
三级缓存如何解决循环依赖?
首先要清楚 Spring 创建 Bean 的流程:
- 先去 一级缓存
singletonObjects
中获取,存在就返回;
- 如果不存在或者对象正在创建中,于是去 二级缓存
earlySingletonObjects
中获取;
- 如果还没有获取到,就去 三级缓存
singletonFactories
中获取,通过执行ObjectFacotry
的getObject()
就可以获取该对象,并在获取成功之后将该对象加入到二级缓存中。
然后来看如何解决循环依赖:
当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;
那么此时就去三级缓存中调用 getObject()
方法去获取 A 的 前期暴露的对象 ,也就是调用上边加入的 getEarlyBeanReference()
方法,生成一个 A 的 前期暴露对象;
然后就将这个 ObjectFactory
从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期引用对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址)注入到依赖,来支持循环依赖。
然后B(或者,其代理对象)完成初始化后,进入一级缓存。
A再获取B,按照上述流程就会从一级缓存中获取初始化好的B,注入,完成A的初始化,进入一级缓存。
Q:为什么要有三级依赖?两级行不行?
三级缓存的引入主要用于处理AOP 动态代理对象的创建问题,即某些 Bean 在实例化之后可能需要生成一个代理对象。如果仅仅使用两级缓存,Spring 无法在实例化 Bean 后,动态地将代理对象暴露给其他依赖它的 Bean。
要理解这个问题,我们需要先明确两点:
-
代理对象的创建时机:在 Spring 中,某些 Bean 在创建后(即,实例化并初始化后)需要生成代理对象,比如通过 AOP 增强功能(如事务管理、方法拦截等)。
-
Spring 创建(或者说,获取) Bean 的流程 :
- 先去 一级缓存
singletonObjects
中获取,存在就返回;
- 如果不存在或者对象正在创建中,于是去 二级缓存
earlySingletonObjects
中获取;
- 如果还没有获取到,就去 三级缓存
singletonFactories
中获取,通过执行ObjectFacotry
的getObject()
就可以获取该对象,并在获取成功之后将该对象加入到二级缓存中。
- 先去 一级缓存
下面是分析:
如果 A 和 B 只是普通的 Bean,没有涉及代理的问题(即不需要 AOP 之类的功能),那么只有两级缓存(即,第一和第三级缓存)是够用的:
- 由于最开始一级缓存没有A,所以创建A时候,会到第三级缓存拿到创建A的工厂,将A实例化,放入一级缓存
- 实例化A之后,Spring尝试初始化A。Spring在初始化阶段为A注入属性时,需要B,于是Spring开始创建B,同样是从三级缓存调用创建B的工厂方法,实例化B,放入一级缓存。然后Spring尝试初始化B,B也依赖于A,那么Spring会在初始化B的过程中去获取A。此时,会在一级缓存获取到A的未初始化的实例,完成B的初始化。
- B初始化完成,Spring继续完成A的初始化,最终一级缓存里的A和B都正常地完成了初始化。
两级缓存(保留第一、第三级缓存)就能解决循环依赖的问题。
但假设 B 是一个需要 AOP 代理的对象,比如带有事务功能的 @Transactional
注解。那么 Spring 还需要在 B 被创建之后生成它的代理对象(这个代理对象负责增强:拦截方法调用、实现事务等功能)。
那么,问题出现了:如果没有三级缓存机制(也就是,没有第二级缓存),当 A 依赖 B 时,Spring 可能在初始化过程中只能获取到 B 的原始对象,而不是代理对象,导致 A 没有正确拿到 B 的最终版本(即代理对象)。这时,就可能导致 AOP 功能失效。
这里二级缓存和三级缓存的核心区别在于:有三级缓存(第二级缓存)时,相当于多了一个中间层来处理代理对象,Spring 可以判断完是否需要创建代理对象再将实例(或该实例的代理对象)放入一级缓存。
有了三级缓存之后,需要代理的场景才能得到很好的解决:
-
由于最开始一二级缓存都没有A,所以创建A时候,会到第三级缓存拿到创建A的工厂,将A实例化,放入二级缓存。
-
实例化A之后,Spring尝试初始化A。Spring在初始化阶段为A注入属性时,需要B,于是Spring开始创建B,同样是从三级缓存调用创建B的工厂方法,实例化B,放入二级缓存。然后Spring尝试初始化B,B也依赖于A,那么Spring会在初始化B的过程中去获取A。此时,会在二级缓存获取到A的未初始化的实例,完成B的初始化。
-
到这里完成了B的实例化和初始化,接下来查看是否需要生成代理对象。
- 如果B被标记为需要代理(例如,有
@Transactional
注解),Spring就会为B创建代理对象。原始的初始化好的B会被包装在代理对象中。这样,就完成了B的代理对象的实例化,初始化,然后B的代理对象被放入一级缓存; - 如果B没有被标记为需要代理,那么将初始化好的B放入一级缓存。
- 如果B被标记为需要代理(例如,有
-
现在,初始化A所需的B(或者,B的代理对象)就已经被创建好了,A也初始化完成。检查A是否需要被代理,并将初始化好的A(或A的代理对象放入一级缓存)