当前位置: 首页 > 后端技术 > Java

Spring是如何解决循环依赖的?写的很好

时间:2023-04-01 17:13:44 Java

作者:青石路\www,cnblogs.com/youzhibing/p/14337244.html写作背景是Java开发,一般都少不了Spring,所以面试的时候肯定会问到Spring相关内容,并循环dependency是Spring中高频面试题。前不久有个朋友去面试,被问到循环依赖,还是敲了一点。他们聊天的过程是这样的。面试官:说说什么是循环依赖朋友:两个或两个以上的对象相互依赖,最终形成闭环。比如对象A依赖对象B,对象B又依赖对象A。面试官:那有什么问题呢?朋友:对象创建过程会产生死循环,类似下面面试官:Spring是怎么解决的?提前曝光对象的面试官:三级缓存里存的是什么朋友:一级缓存里存的是成品对象,已经完成实例化和初始化。我们应用中使用的对象都在一级缓存中,二级缓存中存放的是半成品,用于解决对象创建过程中的循环依赖问题。三级缓存存放objectFactory类型的lambda表达式,用于处理AOP存在时的循环依赖问题。面试官:为什么要用三级缓存来解决循环依赖问题(只有一级缓存行不行,只有二级缓存行不行)朋友:欺负,只有一级缓存也可以可以解决,但是会使整个逻辑复杂化半成品对象不能直接使用(存在NPE问题),所以Spring需要保证在启动过程中,中间产生的所有半成品对象最终都会变成成品对象。如果一级缓存中混杂着半成品对象和成品对象,那么为了区分它们,必然会增加一些额外的标签和逻辑处理,这会使对象的创建过程变得复杂。半成品和成品分开存放,两级缓存各司其职,可以简化对象。创建过程更简单、更直观。如果Spring不引入AOP,那么二级缓存就够了,但是作为Spring的核心之一,AOP怎么能少呢?所以为了处理AOP中的循环依赖,Spring引入了一个三级Cache来处理循环依赖时代理对象的创建面试官:如果代理对象的创建过程超前,在实例化之后,初始化之前,只能两个级缓存被使用?朋友心想:这已经到了我的知识盲点了,我来做吧!但点点头说:你说的有道理,我没有仔细考虑,我会改源码试试前面的问题,感觉朋友的回答还不错,但是三级的作用cache在上一题,答案还是有点短,那是什么,正式讲之前慢慢看下写吧,先复习一下内容,不然后面的内容可能看起来有点模糊(其实,主要是怕你惹我生气)1.对象的创建一般来说,对象的创建分为两个步骤:实例化和初始化。实例化是指在JVM层面从堆中申请内存空间来完成对象的创建。当然,属性值赋值也可以直接通过构造方法一步到位实例化初始化。当然还有其他创建对象的方式,比如工厂等2.Spring的注入方式方法分为构造方法注入、setter方法注入、接口注入三种。接口注入的方式过于灵活易用,所以没有被广泛使用。大家都知道这么说很好,施工方法就不细说了。注入方式将实例化和初始化结合在一起,可以快速创建一个可以直接使用的对象,但是无法处理循环依赖的问题。很好理解,setter方法的注入是在对象实例化完成之后,然后调用对象的setter方法完成属性赋值,可以处理循环依赖的问题。与类中的定义顺序无关,所以一级缓存:singletonObjects,二级缓存:earlySingletonObjects,三级缓存:singletonFactories,虽然不能直接在应用中使用,但是还是可以的在创建对象的过程中使用。就这样,有出栈有出栈,而不是一直被压入,解决了循环依赖的死循环问题。春天是不一样的。基于5.2.12.RELEASE,来看看Spring是如何解决循环依赖的Spring源码分析下面将从几种不同的情况跟踪源码。中间有疑惑,先用笔记。下来吧,看完全部有什么问题欢迎在评论区留言推荐一个SpringBoot基础教程和实例:https://github.com/javastacks...1.无依赖,AOP代码很简单:spring-no-dependencehttps://gitee.com/youzhibing/...此时,Spring中的SimpleBean对象是如何创建的呢?跟着源码走吧。接下来我们从DefaultListableBeanFactory的preInstantiateSingletons方法开始调试没有跟进的方法,或者快速跳过的方法。我们可以先跳过它。重点关注跟进的方法和留下的代码。这时候有几个属性值值得我们关注。我们会从createBean到doCreateBean的关键代码进行跟踪,其中有几个关键的方法调用值得大家参考。跟进到这里:代理对象的创建是在对象实例化和初始化之后进行的。就是为一个完成的对象创建一个代理对象。所以这种情况下:只用一级缓存就可以了,另外两个2.循环依赖,不用AOP代码还是很简单的:spring-circle-simplehttps://gitee.com/youzhibing/...在这个时间,循环依赖的两个类分别是:Circle和Loop对象的创建过程和前面的基本一样,只是多了循环依赖,少了AOP,所以重点说:先创建populateBean和initializeBean方法一个Circle对象,那么我们从创建它的populateBean开始,在开始之前,我们先看一下三个Let'sstartwithpopulateBeanforthedatainthelevelcache.它完成了属性的填充,与循环依赖有关。你必须仔细看。在仔细填充circle对象的propertyloop的时候,去Spring容器中找到loop对象,发现没有,然后创建,来到熟悉的createBean。此时三级缓存中的数据没有变化,只是在SetsingletonsCurrentlyInCreation中多了一个循环。相信大家到这里都没有问题。我们继续往下看。循环实例化完成后,填充它的属性circle,去Spring中获取circle对象,来熟悉的此时doGetBean在一级缓存和二级缓存中没有circle和loop,但是在三级缓存中有这两个。往下看,重点来了,仔细看。通过getSingleton获取圆时,三级缓存调用getEarlyBeanReference,但是因为没有AOP,getEarlyBeanReference直接返回普通的半成品圆,然后将半成品圆放入二级缓存,返回,然后填充它进入循环对象。这时候循环对象就是一个finished对象;然后将loop对象返回并填充到circle对象中。如下图,我们发现finished循环直接放在了一级缓存中,二级缓存一直没有循环过。三级缓存虽然有循环,但是用不到就直接去掉。这时候读完缓存中的数据,相信大家可以想到,虽然loop对象已经填充到circle对象中了,但是还有一些流程没有完成。我们往下看,将完成的圆放入一级缓存。关卡缓存中的圆不用用直接移除。最后,各级缓存中的数据相信大家都很清楚,就不展示了。我们再回顾一下这种情况下各级缓存的存在。一级缓存的存在感是满的,二级缓存可以说是没有存在感,三级缓存是有存在感的(在循环中填圈的时候有用)。所以这种情况下:可以减少一定的缓存,只需要二级缓存就可以了3.循环依赖+AOP代码还是很简单的:spring-circle-aop,在循环依赖的基础上加入AOP比AOP多和之前的案例一样,我们看看对象创建过程有什么不同;同样是先创建Circle,创建Loop的创建过程和前面的大致相同,只是有一小部分不同。当我关注源代码时,我会暂停这些差异,其他的将被跳过。仔细看Circle的实例化,然后填入半成品circle的属性loop,到Spring容器中去获取loop对象,如果没有就实例化Loop,然后填写半成品循环的属性circle,到Spring容器中去获取circle对象。这个过程和前面的情况是一致的,所以直接跳过之后,我们从上图红色的步骤开始往源码走。此时三级缓存中的数据如下。重要的部分在这里。我们发现是从三级缓存中获取的创建圆时调用getEarlyBeanReference创建半成品圆的代理对象,将半成品圆的代理对象放入二级缓存,并将代理对象赋值回半成品的circle属性finishedloop注意:此时循环正在进行初始化,但是半成品循环的代理对象是提前创建的。循环的初始化还没有完成。我们往下看,又是一个重点。仔细看initializeBean方法中半成品循环的初始化,终于创建了。成品的循环代理对象创建完成后,会放入一级缓存(三级缓存中的循环去掉,二级缓存从头到尾没有循环),以及然后返回loop代理对象,赋值给半成品圈的属性loop,然后初始化半成品圈的Bean,因为圈的代理对象已经生成了(在二级缓存中),所以不需要生成代理对象;将二级缓存中的圈子代理对象移动到一级缓存中,并返回代理对象。此时各级缓存中的数据如下(正常的circle和loop对象在各自代理对象的target中)。我们再回顾一下这种情况下各级缓存的存在。一级缓存还是有存在感的,二级缓存有存在感的,三级缓存有存在感的。三级缓存预先创建一个循环代理对象。如果不提前创建,它只能作为半成品圆给loop对象的circle对象赋值。那么loop对象中的circle对象是没有AOP增强功能的。二级缓存用于存放循环代理,解决循环依赖;可能在这个例子中还不够明显,因为依赖比较简单,依赖稍微复杂一点。感觉一级缓存存放的是暴露给外界的对象,可能是代理对象,也可能是普通对象。所以在这种情况下:三级缓存一个都不能少。4.循环依赖+AOP+删除三级缓存NoDependency,在AOP的情况下,我们知道AOP代理对象的生成是在finished对象创建之后创建的。这也是Spring的设计原则。代理对象应该尽量推迟循环依赖+AOP的创建。的生成是提前的,因为它的AOP功能要保证,但是循环代理对象的生成还是跟着Spring的原理如果打破这个原理,把代理对象的创建逻辑往前推进,是不是就可以不用三级缓存,只用二级缓存了?代码还是很简单的:spring-circle-customhttps://gitee.com/youzhibing/...只是对Spring的源码做了非常小的改动,改动如下:去掉了三级缓存,并且代理对象的创建逻辑提前,放在实例化之后,初始化之前;我们看看执行结果,没有问题。有兴趣的可以关注源码。跟踪流程相信大家都已经掌握了,这里就不做演示了。5.循环依赖+AOP+注解目前基于xml的配置越来越少,基于注解的配置越来越多,所以提供一个注解版的也很简单,大家可以关注源码:spring-circle-annotationhttps://gitee.com/youzhibing/...跟踪过程和循环依赖+AOP的情况基本一致,只是在属性的填写上有些区别。具体可以参考:Spring的自动组装→骚花@Autowired的底层工作原理总结1.三级缓存各自的作用一级缓存存放对外暴露的对象,即二级缓存我们的应用程序需要用到的就是处理循环依赖对象创建的问题,其中存放半成品对象或者半成品对象的代理对象的三级缓存的作用。存在AOP+循环依赖的对象创建问题,可以提前创建代理对象。2、Spring为什么要引入三级缓存。严格来说,三级缓存并不是不可或缺的。因为可以提前创建代理对象。提前创建代理对象只会节省一点点内存空间,不会带来性能提升,反而会打破Spring的设计原则。Spring的设计原则是保证尽可能创建普通对象。之后生成它的AOP代理(尽量延迟代理对象的生成),所以Spring使用了三级缓存,既保持了设计原则,又处理了循环依赖;牺牲这样的内存空间是愿意接受的。近期热点文章推荐:1.1000+Java面试题及答案(2021最新版)2.别在满屏的if/else里,试试策略模式,真香!!3.操!Java中xx≠null的新语法是什么?4、SpringBoot2.6正式发布,一大波新特性。5.《Java开发手册(嵩山版)》最新发布,赶快下载吧!感觉不错,别忘了点赞+转发!