美食家大橙子 发表于 2024-9-16 12:10:53

深入相识Spring循环依赖本质

阐明:
  1. 本文基于Spring-Framework 5.1.x版本讲解
  2. 发起读者对创建对象部门源码有一定相识
概述

这篇讲讲Spring循环依赖的题目,网上讲循环依赖的帖子太多太多了,信赖很多人也多多少少相识一点,那我照旧把这个题目自己梳理一遍,紧张是基于以下出发点:
1. Spring到底如何办理的循环依赖题目,有没有’黑科技‘;
2. 有时项目会因为循环依赖题目导致启动失败,由于不相识其机制,排查耗费时间
3. 网上众说风云,没有形成自己的思考
另有其他文章说Spring使用三级缓存是为相识决循环依赖题目,为相识决代理下的循环依赖题目? 那废话不多说,直接开始吧
循环依赖简介

循环依赖的寄义: BeanA依赖BeanB,BeanB又依赖BeanA , 如下图
https://i-blog.csdnimg.cn/blog_migrate/03758ee1ebe7b01e285c7f057757c45b.png
这就是循环依赖, 我们来分析下会有什么题目?

1. 实例化BeanA
2. BeanA在属性注入阶段从容器里面找BeanB
3. 如果BeanB已经在容器里面创建好,那万事大吉,没循环依赖的题目
4. 如果BeanB还没有在容器里面创建好,这时间Spring会创建BeanB
5. 创建BeanB的时间又发现依赖BeanA,但此时BeanA也没有创建完,在没有开启循环依赖开关的情况下就会报错:Requested bean is currently in creation: Is there an unresolvable circular reference?
从软件代码分层结构来讲,如果分层公道,这种情况一般可以避免掉,但是避免不了同一个层次中的Bean相互引用,好那既然循环依赖肯定会出现,我们自己先来思考下,如果是我们自己写一个IOC容器,这个题目如何办理?
如何办理循环依赖?

从上面的4步可以看出来,题目所在就是BeanB在创建过程中,无法找到正在创建中的BeanA,那我们是不是可以找一个地方把正在创建的BeanA(为了行文方便,把Bean正在创建中的状态称为半状态)先给生存起来,等BeanB用到的时间赋值给它不就行了,这时间步调如下:
1. 实例化BeanA
2. BeanA在属性注入之前先把自己放到一个Map里面(此时BeanA为半状态)
3. BeanA在属性注入阶段从容器里面找BeanB
4. 如果BeanB还没有在容器里面创建好,这时间Spring会创建BeanB
5. 创建BeanB的时间又发现依赖BeanA,由于BeanA已经在Map里面了(虽然是半状态,但不影响最终使用,反正现在又不袒露给用户使用) ,所以注入成功,BeanB创建完成
6. 由于BeanB已创建完成,意味着BeanA注入BeanB成功,此时从Map中移除BeanA的半状态
7. 容器初始化完成
到这里有什么大的题目没有? 其实是没有的,Bean确实会创建成功 , 容器可以正常启动完成。 那我们在来看下启用动态代理情况下,使用一个Map(一级缓存)会不会有题目? 为了阐明简朴,我们只生成BeanA的代理对象BeanA_Proxy,BeanB无需创建:
1. 实例化BeanA,并生成BeanA的代理对象BeanA_Proxy ,此时上下文中有BeanA、BeanA_Proxy两个对象
2. BeanA在属性注入之前先把代理对象BeanA_Proxy放到Map里面
3. BeanA在属性注入阶段从容器里面找BeanB
4. 如果BeanB还没有在容器里面创建好,这时间Spring会创建BeanB
5. 创建BeanB的时间又发现依赖BeanA,由于BeanA的代理对象BeanA_Proxy已经在Map里面了,所以把BeanA_Proxy注入BeanB,此时BeanB创建完成
6. 由于BeanB已创建完成,意味着BeanA注入BeanB成功,此时从Map中移除BeanA_Proxy
7. 容器把BeanA_Proxy袒露给用户使用,并初始化完成
从上面可以看出,纵然使用代理的情况下,使用一个Map(一级缓存)来办理循环依赖题目也是可以的。
为什么是三级缓存?

从上面可以看出,把半状态的Bean或者代理对象无脑的放入一级缓存之后,确实可以办理循环依赖的题目,那为什么Spring要使用三级缓存来办理这个题目呢? 让我们来追念下Bean的整个生命周期: 1. 实例化Bean 2. 属性注入 3. 初始化Bean 。 这是创建一个Bean必经的几个步调,而我们上面是为相识决循环依赖题目而强行加了一个往一级缓存里面放对象的步调,而对于不存在循环依赖的对象,这一步无疑是多余的;另有另外一个题目: 把半状态的Bean和创建完成的Bean对象放入同一个缓存里面也不太好管理,违反单一职能原则
所以这里我们要办理2个题目:
1. 办理循环依赖的代码(也就说生成半状态对象的过程)不能放到创建Bean的主流程中;
2. 半状态的对象需要与创建完成的对象所在的容器隔离开。
为相识决题目一我们可以思量在检测到有循环依赖的时间才把半状态对象袒露给BeanB使用,就是说说BeanB发现BeanA正在创建的时间,在把半状态BeanA赋值给BeanB。 但是题目又来了:这个半对象BeanA哪来的? 谁创建出来的? 这无疑酿成了蛋和鸡的题目, 所以我们照旧要在刚开始创建BeanA的时间以最小的代价(这里指实行时间最短) 把半对象给生存起来,BeanB检测到循环依赖的时间,在把BeanA给取出来。那Spring其实就是使用另一个Map生存匿名函数的方式来办理这个题目标, 因为你要思量直接袒露BeanA的代价(比如说袒露给BeanB是一个代理对象,那要思量创建BeanA代理对象的实行成本)。 这里我们简朴贴下Spring的源码:
/** * 实际生成Bean的核心方法 */protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
// 省略上下文中其他不重要的代码
// 这里注册一个匿名函数,把匿名函数放到一个临时缓存里面(其实就是所谓的三级缓存) addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}
/** * 作用: 获取提前要暴露给其他Bean的引用 * 这个方法只有在Spring检测到有循环引用的情况下才会调进来 */protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 获取要提前暴露的对象,这里有可能产生代理对象以及其他非入参Bean的对象 exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject;} 好,看到这里其实我们已经顺便把上述第二个题目也一起办理掉了,因为生存匿名函数的Map和最终生存对象实例的Map不是同一个,而且生存匿名函数的Map在办理完循环依赖题目之后会清空掉,这样我们就可以以延迟加载的方式办理掉循环依赖的题目。 那在BeanA会产生代理的情况下会不会有题目??? 请你自己思考下
但这里另有一个题目,思量下面一种场景:
https://i-blog.csdnimg.cn/blog_migrate/c1bec9bda42e1030f477967f267a1fe9.png
​BeanA与BeanB、BeanC的关系是相互引用, 套用我们上面的理论, 在创建BeanA的主流程中我们只是插入了一个获取半状态对象的匿名函数,而不是要袒露给外部的最终对象,当BeanC也需要注入BeanA的时间,照旧要实行一次匿名函数来获取最终袒露的对象,这里有两个题目:

1. 匿名函数重复实行,其实是没有必要的。
2. 更为严重的是,有可能每次调用函数返回的是差异的对象,这样会导致注入给BeanB和BeanC的对象不一致,这就是大题目了。 (那有人会问如果保证获取最终袒露对象的接口SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference中返回同一个对象不就行了? 站在办理题目标角度确实是这样的,但是站在容器的角度来讲就不一样了,接口只是一个拓展点,他的实行逻辑是不可控的,所以照旧要在容器级别来办理这个题目)
为相识决以上两个题目,Spring采用了第三个缓存,把已经暴袒露去的对象给缓存起来,这样题目就完美办理以上两个题目。
getEarlyBeanReference

在上面我们已经解释了为什么要用三级缓存来办理循环依赖的题目,我们在简朴说下SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference接口,其实这个接口没有什么高深的地方,只是为了获取循环依赖下需要提前袒露给其他Bean的对象,这里要注意一下:仅仅在检测到有循环依赖的情况下才会调进来,我们看下接口界说
/** * Extension of the {@link InstantiationAwareBeanPostProcessor} interface, * adding a callback for predicting the eventual type of a processed bean. * * <p><b>NOTE:</b> This interface is a special purpose interface, mainly for * internal use within the framework. In general, application-provided * post-processors should simply implement the plain {@link BeanPostProcessor} * interface or derive from the {@link InstantiationAwareBeanPostProcessorAdapter} * class. New methods might be added to this interface even in point releases. * * @author Juergen Hoeller * @since 2.0.3 * @see InstantiationAwareBeanPostProcessorAdapter */public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor { default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { return bean; }} 从类注释中”This interface is a special purpose interface, mainly for internal use within the framework“可以看到,这个接口仅仅是Spring内部为相识决某些题目提供的接口,并不渴望袒露给上层用户使用,所以我们在实际工作中一般用不到的
但是我们要特别注意一个坑: 我们知道在Bean后置处理接口BeanPostProcessor中也有可能返回代理对象,如果这里返回的对象和循环依赖暴袒露去的对象不一致的话就会报以下错误:
Error creating bean with name 'serviceA': Bean with name 'serviceA' has been injected into other beans 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 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serviceA': Bean with name 'serviceA' has been injected into other beans 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 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:622) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:514) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:321) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:319) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) at test.circle_ref.CircleRefTests.run(CircleRefTests.java:13) 从逻辑上来讲报错也是公道的,因为既然提前暴袒露去的Bean,像是一种承诺,后续就不能修改了,修改意味着Spring容器中Bean的’状态‘是不一致的。 所以在有循环依赖的情况下,一定要保证getEarlyBeanReference和BeanPostProcessor 返回的Bean是同一个。
小结

本篇文章先容了Spring为什么用三级缓存才办理循环依赖题目,并且先容了SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference接口的寄义以及在实际情况中可能遇到的坑,那我们总结下Spring的三级缓存的作用:
一级缓存: 生存已经创建完的Bean对象,我们常用的BeanFactory#getBean方法就是从这里获取;
二级缓存: 缓存在循环依赖中袒露给其他Bean的半状态对象,防止注入对象不一致的题目;
三级缓存: 缓存获取提前袒露的Bean的匿名函数 ,为的是以最小的代价减少对Spring创建对象主干流程的影响
关于循环依赖的题目就先容到这,不得不说Spring思量题目真是的非常全面,设计相称公道。如有疑问,欢迎交流

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 深入相识Spring循环依赖本质