创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~
如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列
一、前言
这一篇博文主要讲一下我们spring
是怎么解决循环依赖的问题的。
二、什么是循环依赖
首先我们需要明确,什么是循环依赖呢?这里举一个简单的例子:
1 | java复制代码@Service |
以这个例子来看,我们声明了a
、b
两个bean
,且a
中需要注入一个b
,b
中需要注入一个a
。
结合我们上篇博文的bean
生命周期的知识,我们来模拟一下这两个bean
创建的流程:
如果没有缓存的设计,我们的虚线所示的分支将永远无法到达,导致出现无法解决的循环依赖问题….
三、三级缓存设计
1. 自己解决循环依赖问题
现在,假如我们是spring
的架构师,我们应该怎么解决这个循环依赖问题呢?
1.1. 流程设计
首先如果要解决这个问题,我们的目标应该是要把之前的级联的无限创建流程切到,也就是说我们的流程要变为如下所示:
也就是说,我们需要在B
实例创建后,注入A
的时候,能够拿到A
的实例,这样才能打破无限创建实例的情况。
而B
实例的初始化流程,是在A
实例创建之后,在populateBean
方法中进行依赖注入时触发的。那么如果我们B
实例化过程中,想要拿到A
的实例,那么A
实例必须在createBeanInstance
创建实例后(实例都没有就啥也别说了)、populateBean
方法调用之前,就暴露出去,让B
能通过getBean
获取到!(同学们认真想一下这个流程,在现有的流程下改造,是不是只能够这样操作?自己先想清楚这个流程,再去结合spring
源码验证,这一块的知识点你以后想忘都忘不掉)
那么结合我们的思路,我们再修改一下流程图:
1.2. 伪代码实现
流程已经设计好了,那么我们其实也可以出一下这个流程的伪代码(伪代码就不写加锁那些流程了):
1 | java复制代码// 正真已经初始化完成的map |
可以看到,如果我们自己实现一个缓存结构来解决循环依赖的问题的话,可能只需要两层结构就可以了,但是spring
却使用了3级缓存,它有哪些不一样的考量呢?
2. Spring
源码
我们已经知道该怎么解决循环依赖问题了,那么现在我们就一起看一下spring
源码,看一下我们的分析是否正确。
由于之前我们已经详细讲过整个bean
的生命周期了,所以这里就只挑三级缓存相关的代码段来讲了,会跳过比较多的代码,同学们如果有点懵,可以温习一下万字长文讲透bean的生命周期。
2.1. Spring
的三级缓存设计
2.1.1. 三级缓存源码
首先,在我们的AbstractBeanFactory#doGetBean
的逻辑中:
1 | java复制代码// 初始化是通过getBean触发bean创建的,依赖注入最终也会使用getBean获取依赖的bean的实例 |
可以看到,如果我们使用getSingleton(beanName)
直接获取到bean实例了,是会直接把bean实例返回的,我们一起看一下这个方法(这个方法属于DefaultSingletonBeanRegistry
):
1 | java复制代码// 一级缓存,缓存正常的bean实例 |
一二级缓存都好理解,其实就可以理解为我们伪代码里面的那两个Map
,但是这个三级缓存是怎么回事?ObjectFactory
又是个什么东西?我们就先看一下这个ObjectFactory
的结构:
1 | java复制代码@FunctionalInterface |
我们回到这个三级缓存的结构,二级缓存是是在getSingleton
方法中put
进去的,这跟我们之前分析的,创建bean
实例之后放入,好像不太一样?那我们是不是可以推断一下,其实创建bean
实例之后,是放入三级缓存的呢(总之实例创建之后是需要放入缓存的)?我们来跟一下bean
实例化的代码,主要看一下上一篇时刻意忽略掉的地方:
1 | java复制代码// 代码做了很多删减,只把主要的逻辑放出来的 |
可以看到,初始化一个bean
是,创建bean
实例之后,如果这个bean是单例bean
&&允许循环依赖&&当前bean
正在创建,那么将会调用addSingletonFactory
加入三级缓存:
1 | java复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { |
也就是说我们伪代码中的这一段有了:
1 | java复制代码// 创建实例 |
那么接下来,完全实例化完成的bean
又是什么时候塞入我们的实例Map
(一级缓存)singletonObjects
的呢?
这个时候我们就要回到调用createBean
方法的这一块的逻辑了:
1 | java复制代码if (mbd.isSingleton()) { |
可以看到,我们的createBean
创建逻辑是通过一个lamdba
语法传入getSingleton
方法了,我们进入这个方法看一下:
1 | java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { |
哈哈,加入实例Map
(一级缓存)singletonObjects
的逻辑明显就是在这个addSingleton
中了:
1 | java复制代码protected void addSingleton(String beanName, Object singletonObject) { |
也就是说,我们伪代码的这一块在spring
里面也有对应的体现,完美:
1 | java复制代码// 初始化方法调用 |
就这样,spring通过缓存设计解决了循环依赖的问题。
2.1.2. 三级缓存解决循环依赖流程图
什么,看完代码之后还是有点模糊?那么把我们的流程图再改一下,按照spring
的流程来:
2.1.3. 三级缓存解决循环依赖伪代码
看完图还觉得不清晰的话,我们把所有spring
中三级缓存相关的代码汇总到一起,用伪代码的方式,拍平成一个方法,大家应该感觉会更清晰了:
1 | java复制代码// 一级缓存 |
把所有逻辑放到一起之后会清晰很多,同学们只需要自行模拟一遍,再populateBean
中再次调用getBean
逻辑进行依赖注入,应该就能捋清楚了。
2.1.4. 标记当前bean正在创建
在我们刚刚看到的将bean
实例封装成ObjectFactory
并放入三级缓存的流程中,有一个判断是当前bean是正在创建,这个状态又是怎么判断的呢:
1 | java复制代码// 重点是这里了,如果是单例bean&&允许循环依赖&&当前bean正在创建 |
我们看一下这个isSingletonCurrentlyInCreation
的逻辑:
1 | java复制代码private final Set<String> singletonsCurrentlyInCreation = |
可以看到额,其实就是判断当前beanName
是不是在这个singletonsCurrentlyInCreation
容器中,那么这个容器中的值又是什么时候操作的呢?
希望同学们还记得getSingleton(beanName, singletonFactory)
中有调用的beforeSingletonCreation
和afterSingletonCreation
:
1 | java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { |
我们现在来看一下这两个方法的逻辑:
1 | java复制代码protected void beforeSingletonCreation(String beanName) { |
可以看到,我们这两个方法主要就是对singletonsCurrentlyInCreation
容器进行操作的,inCreationCheckExclusions
这个容器可以不用管它,这名称一看就是一些白名单之类的配置。
这里需要主要的是beforeSingletonCreation
中,如果singletonsCurrentlyInCreation.add(beanName)
失败的话,是会抛出BeanCurrentlyInCreationException
的,这代表spring
遇到了无法解决的循环依赖问题,此时会抛出异常中断初始化流程,毕竟单例的bean
不允许被创建两次。
2.2. 为什么要设计为三级结构?
2.2.1. 只做两级缓存会有什么问题?
其实到这里,我们已经清楚,三级缓存的设计已经成功的解决了循环依赖的问题。
可是按我们自己的设计思路,明明只需要两级缓存就可以解决,spring
却使用了三级缓存,难道是为了炫技么?
这个时候,就需要我们再细致的看一下bean初始化过程了:
1 | java复制代码protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) |
仔细观察,initializeBean
方法是可能返回一个新的对象,从而把createBeanInstance
创建的bean实例替换掉的:
1 | java复制代码protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { |
可以看到,我们的postProcessBeforeInitialization
和postProcessAfterInitialization
的埋点方法都是有可能把我们的bean
替换掉的。
那么结合整个流程来看,由于我们放入缓存之后,initializeBean
方法中可能存在替换bean
的情况,如果只有两级缓存的话:
这会导致B
中注入的A
实例与singletonObjects
中保存的AA
实例不一致,而之后其他的实例注入a
时,却会拿到singletonObjects
中的AA
实例,这样肯定是不符合预期的。
2.2.2. 三级缓存是如何解决问题的
那么这个问题应该怎么解决呢?
这个时候我们就要回到添加三级缓存的地方看一下了。addSingletonFactory
的第二个参数就是一个ObjectFactory
,并且这个ObjectFactory
最终将会放入三级缓存,现在我们再回头看调用addSingletonFactory
的地方:
1 | java复制代码 // 加入三级缓存 |
熟悉lamdba
语法的同学都知道,getEarlyBeanReference
其实就是放入三级缓存中的ObjectFactory
的getObject
方法的逻辑了,那我们一起来看一下,这个方法是做了什么:
1 | java复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { |
咦,这里也有个埋点,可以替换掉bean
的引用。
原来为了解决initializeBean
可能替换bean
引用的问题,spring
就设计了这个三级缓存,他在第三级里保存了一个ObjectFactory
,其实具体就是getEarlyBeanReference
的调用,其中提供了一个getEarlyBeanReference
的埋点方法,通过这个埋点方法,它允许开发人员把需要替换的bean
,提早替换出来。
比如说如果在initializeBean
方法中希望把A
换成AA
(这个逻辑肯定是通过某个beanPostProcessor
来做的),那么你这个beanPostProcessor
可以同时提供getEarlyBeanReference
方法,在出现循环依赖的时候,可以提前把A->AA
这个逻辑做了,并且initializeBean
方法不再做这个A->AA
的逻辑,并且,当我们的循环依赖逻辑走完,A
创建->注入B
->触发B
初始化->注入A
->执行缓存逻辑获取AA
实例并放入二级缓存->B
初始化完成->回到A
的初始化逻辑时,通过以下代码:
1 | java复制代码protected Object doCreateBean(...) { |
这样就能保证当前bean
中注入的AA
和singletonObjects
中的AA
实例是同一个对象了。
将会把二级缓存中的AA
直接返回,这是就能保证B
中注入的AA
实例与spring
管理起来的最终的AA
实例是同一个了。
整个流程梳理一下就是这样:
2.2.3. 三级缓存的实际应用
既然设计了这个三级缓存,那么肯定是有实际需求的,我们上面分析了一大堆,现在正好举一个例子看一下,为什么spring
需要三级缓存。
我们都知道,Spring
的AOP
功能,是通过生成动态代理类来实现的,而最后我们使用的也都是代理类实例而不是原始类实例。而AOP
代理类的创建,就是在initializeBean
方法的postProcessAfterInitialization
埋点中,我们直接看一下getEarlyBeanReference
和postProcessAfterInitialization
这两个埋点吧(具体类是AbstractAutoProxyCreator
,之后讲AOP
的时候会细讲):
1 | java复制代码public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport |
就这样,Spring
巧妙的使用三级缓存来解决了这个不同实例的问题。当然,如果我们需要自己开发类似代理之类的可能改变bean
引用的功能时,也需要遵循getEarlyBeanReference
方法的埋点逻辑,学习AbstractAutoProxyCreator
中的方式,才能让spring
按照我们的预期来工作。
四、三级缓存无法解决的问题
1. 构造器循环依赖
刚刚讲了很多三级缓存的实现,以及它是怎么解决循环依赖的问题的。
但是,是不是使用了三级缓存,就能解决所有的循环依赖问题呢?
当然不是的,有一个特殊的循环依赖,由于java
语言特性的原因,是永远无法解决的,那就是构造器循环依赖。
比如以下两个类:
1 | java复制代码public class A { |
抛开Spring
来讲,同学们你们有办法让这两个类实例化成功么?
该不会有同学说,这有何难看我的:
1 | java复制代码// 你看,这样不行么~ |
不好意思,这个真的不行,不信可以去试试。从语法上来讲,java
的语言特性决定了不允许使用未初始化完成的变量。我们只能无限套娃:
1 | java复制代码// 这样明显就没有解决问题,是个无限套娃的死循环 |
所以,连我们都无法解决的问题,就不应该强求spring
来解决了吧~
1 | java复制代码@Service |
启动之后,果然报错了:
1 | shell复制代码Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference? |
2. Spring
真的对构造器循环依赖束手无策么?
难道,spring
对于这种循环依赖真的束手无策了么?其实不是的,spring
还有@Lazy
这个大杀器…只需要我们对刚刚那两个类小小的改造一下:
1 | java复制代码@Service |
都说了成功了,运行结果同学们也能猜到了吧:
1 | shell复制代码in a prt |
(同学们也可以自己尝试一下~
3. @Lazy
原理
这个时候我们必须要想一下,spring
是怎么通过 @Lazy
来绕过我们刚刚解决不了的无限套娃问题了。
因为这里涉及到之前没有细讲的参数注入时候的参数解析问题,我这边就不带大家从入口处一步一步深入了,这边直接空降到目标代码DefaultListableBeanFactory#resolveDependency
:
1 | java复制代码public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, |
我们直接看一下这个getLazyResolutionProxyIfNecessary
,这个方法就是获取LazyProxy
的地方了:
1 | java复制代码public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver { |
同学们可能对TargetSource
和ProxyFactory
这些不熟悉,没关系,这不妨碍我们理解逻辑。
从源码我们可以看到,对于@Lazy
的依赖,我们其实是返回了一个代理类(以下称为LazyProxy
)而不是正真通过getBean
拿到目标bean
注入。而真正的获取bean
的逻辑,被封装到了一个TargetSource
类的getTarget
方法中,而这个TargetSource
类最终被用来生成LazyProxy
了,那么我们是不是可以推测,LazyProxy
应该持有这个TargetSource
对象。
而从我们懒加载的语意来讲,是说真正使用到这个bean
(调用这个bean
的某个方法时)的时候,才对这个属性进行注入/初始化。
那么对于当前这个例子来讲,就是说其实B
创建的时候,并没有去调用getBean("a")
去获取构造器的参数,而是直接生成了一个LazyProxy
来做B
构造器的参数,而B
之后正真调用到A
的方法时,才会去调用TargetSource
中的getTarget
获取A
实例,即调用getBean("a")
,这个时候A
早就实例化好了,所以也就不会有循环依赖问题了。
4. 伪代码描述
还是同样,我们可以用伪代码来描述一下这个流程,伪代码我们就直接用静态代理来描述了:
1 | java复制代码public class A { |
那么整个初始化的流程简单来描述就是:
1 | java复制代码Map<String, Object> ioc = new HashMap<>(); |
我们也模拟一下运行:
1 | java复制代码void test() { |
当然是能成功打印的:
1 | shell复制代码in a prt |
六、总结
关于循环依赖的问题,Spring提供了通过设计缓存的方式来解决的,而设计为三级缓存,主要是为了解决bean
初始化过程中,实例被放入缓存之后,实例的引用还可能在调用initializeBean
方法时被替换的问题。
对于构造器的循环依赖,三级缓存设计是无法解决的,这属于java
语言的约束;但是spring
提供了一种使用@Lazy
的方式,绕过这个限制,使得构造器的循环依赖在特定情况下(循环链中的某个注入打上@Lazy
注解)也能解决。
(小声BB,接下来更新应该会变慢了…
创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~
如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列
٩(* ఠO ఠ)=3⁼³₌₃⁼³₌₃⁼³₌₃嘟啦啦啦啦。。。
这里是新人博主小希子,大佬们都看到这了,左上角点个赞再走吧~~
本文转载自: 掘金