当前位置: 首页 > 科技观察

图解Spring循环依赖,写的好!_0

时间:2023-03-21 19:40:20 科技观察

前言如何解决Spring中的循环依赖是近两年火的一道Java面试题。其实对于这种框架源码问题,作者本人还是持怀疑态度的。如果作者是面试官,我可能会问一些场景问题,比如“如果注入的属性为null,你会从哪些方向检查?”那么既然写完了这篇文章,我们就不废话了,开始看Spring是如何解决循环依赖的,让大家看看循环依赖的本质是什么。一般来说,如果你问Spring如何在内部解决循环依赖,那肯定是单个默认单例bean中属性相互引用的场景。比如几个bean之间的相互引用:甚至自身的“循环”依赖:首先说明前提:原型(Prototype)场景不支持循环依赖,通常会在AbstractBeanFactory类中进行如下判断并抛出异常。if(isPrototypeCurrentlyInCreation(beanName)){thrownewBeanCurrentlyInCreationException(beanName);}这个道理很好理解。新建A时,发现需要注入原型字段B,新建B时,发现需要注入原型字段A……这是套娃了,猜猜哪个首先是StackOverflow还是OutOfMemory?Spring怕你猜不出来,所以它先根据构造函数的循环依赖抛出BeanCurrentlyInCreationExceptionImage。还别说,官方文档都是摊牌。如果你想让构造函数注入对循环依赖的支持,它是不存在的。最好把代码改一下。那么在默认的单例属性注入场景下,Spring是如何支持循环依赖的呢?Spring解决循环依赖首先,Spring内部维护了三个Map,也就是我们通常所说的三级缓存。笔者翻阅了Spring文档,并没有找到三级缓存的概念,这也可能是为了方便理解的地方性词汇。在Spring的DefaultSingletonBeanRegistry类中,你会发现类上方挂着这三个Map:singletonObjects是我们最熟悉的朋友,俗称“单例池”、“容器”,缓存创建单例bean的地方。singletonFactories映射创建Bean的原始工厂earlySingletonObjects映射Bean的早期引用,也就是说这个Map中的Bean并不完整,甚至不能称为“Bean”,它只是一个Instance。最后两个Map其实是“敲门砖”关卡,只在创建bean的时候使用,创建完成后清空。所以笔者在上一篇文章中对“三级缓存”这个名词有点迷糊,可能是因为评论都是以Cache的开头。为什么要成为最后两个Map作为敲门砖,假设最后放到singletonObjects中的Bean就是你要的那杯“酷白”。然后Spring准备了两个杯子,分别是singletonFactories和earlySingletonObjects来回“折腾”了几次,将热水“凉白”地倒入singletonObjects中。闲话少说,全都凝聚在了画面之中。上面的是一个GIF,如果你没有看到它可能还没有加载。三秒一帧,不是你的电脑卡。作者画了17张图来简化Spring的主要步骤。GIF上方就是刚才说的三级缓存,主要方法如下图。当然,此时你必须结合Spring源码来看,否则你可能看不懂。如果只是想大概了解一下,或者面试,可以先记住笔者上面提到的“三级缓存”,下面会说到的精髓。循环依赖的本质在了解了上面Spring是如何处理循环依赖的之后,让我们跳出“读源码”的思路。假设你被要求实现一个具有以下特征的功能,你会怎么做?指定的一些类实例是单例的,类中的所有字段都是单例的,以支持循环依赖。比如,假设有classA:publicclassA{privateBb;}//classB:publicclassB{privateAa;}说白了,让你模仿Spring:假设A和B被@Component修饰,而@Component中的字段class假装被@Autowired修改,处理后放到Map中。其实很简单。我写了一个粗略的代码供参考:/***放置创建的beanMap*/privatestaticMapcacheMap=newHashMap<>(2);publicstaticvoidmain(String[]args){//假装扫描出对象Class[]classes={A.class,B.class};//假装项目初始化并实例化所有beanfor(ClassaClass:classes){getBean(aClass);}//checkSystem.out.println(getBean(B.class).getA()==getBean(A.class));System.out.println(getBean(A.class).getB()==getBean(B.class));}@SneakyThrowsprivatestaticTgetBean(ClassbeanClass){//本文只是将bean的命名规则替换为小写的类名StringbeanName=beanClass.getSimpleName().toLowerCase();//如果已经是bean,则直接返回if(cacheMap.containsKey(beanName)){return(T)cacheMap.get(beanName);}//实例化对象本身Objectobject=beanClass.getDeclaredConstructor().newInstance();//放入缓存cacheMap.put(beanName,object);//把所有的字段都当作需要注入的bean,创建并注入到当前bean中Field[]fields=object.getClass().getDeclaredFields();for(Fieldfield:fields){field.setAccessible(true);//获取待注入字段的classClassfieldfieldClass=field.getType();StringfieldBeanName=fieldClass.getSimpleName().toLowerCase();//如果要注入的bean已经在缓存Map中,则将缓存Map中的值注入到字段中。//如果缓存不继续创建field.set(object,cacheMap.containsKey(fieldBeanName)?cacheMap.get(fieldBeanName):getBean(fieldClass));}//属性填充返回return(T)object;}这段代码的作用其实就是处理循环依赖,处理完成后,将完整的“Bean”放到cacheMap中。这才是“循环依赖”的本质,而不是“Spring如何解决循环依赖”。泥潭”,忘记了问题的本质,为了看源码而看源码,一直看不懂,却忘记了本质是什么,如果你真的不会看不懂,还是先写基础版吧,反过来Spring为什么要这样实现,效果可能会更好。什么?问题的本质其实就是两个sum!看了刚才作者的代码,有没有有没有似曾相识的感觉?没错,就是类似twosum的解题,不知道twosum是什么,作者给大家介绍下:twosum是leetcode上序号为1的一道题,就是大部分人入门算法的第一道题,经常被嘲笑,一个算法方面的公司,面试官指定的,很合得来:给定一个数组,给定一个数。返回数组中可以是a的两个索引dded获取指定的数字。例如:给定nums=[2,7,11,15],target=9,那么应该返回[0,1],因为2+7=9问题的最优解是遍历+HashMap一次:classSolution{publicint[]twoSum(int[]nums,inttarget){Mapmap=newHashMap<>();for(inti=0;i