作者:小傅哥 博客:https:bugstack。cn包含:Java基础,面经手册,Netty4。x,手写Spring,用Java实现JVM,重学Java设计模式,SpringBoot中间件开发,IDEA插件开发,DDD系统架构项目开发,字节码编程。。。 沉淀、分享、成长,让自己和他人都能有所收获!一、前言 延迟满足能给你带来什么? 大学有四年时间,但几乎所有人都是临近毕业才发现找一份好工作费劲,尤其是我能非常熟悉的软件开发行业,即使是毕业了还需要额外花钱到培训机构,在学一遍编程技术才能出去找工作。好像在校这几年压根就没学到什么! 就我个人而言可能是因为上学期间喜欢编程,也从师哥、师姐那里听到一些关于毕业后找工作的不容易,也了解了一些社会上对程序员开发技能的要求级别。也就是得到了这些消息,又加上自己乐于折腾,我给自己定了一个每天都能完成的小目标:红尘世界几个王,我自不服迎头上。日敲代码两百行,冲进世界五百强。 哈哈哈,就这么每天两百行代码,一个月就是6千行,一年就是6万行,三年后开始实习就有18万行,一个应届实习生有将近20万行代码的敲击量,几乎已经可以非常熟练的完成各类简单的工作,在加上实习中对整个项目流程真正的锻炼后,找一个正经的开发工作,还是很容易的。 而这时候找工作的容易,就来自于你一直以来的学习和沉淀,但如果你没经过这些努力,可能等毕业后就会变得非常慌乱,最后没办法只能去一些机构再学习一遍。二、面试题 谢飞机,小记!,以前感觉Spring没啥,看过一篇getBean,我的天! 谢飞机:面试官,最近我看了Spring的getBean发现这里好多东西,还有一个是要解决循环依赖的,这玩意面试有啥要问的吗? 面试官:有哇,Spring是如何解决循环依赖的? 谢飞机:嗯,通过三级缓存提前暴露对象解决的。 面试官:可以哈,那这三个缓存里都存放了什么样的对象信息呢? 谢飞机:一级缓存存放的是完整对象,也叫成品对象。二级缓存存放的是半成品对象,就是那些属性还没赋值的对象。三级缓存存放的是ObjectFactorylt;?类型的lambda表达式,就是这用于处理AOP循环依赖的。 面试官:可以呀,谢飞机有所准备嘛!那如果没有三级缓存,只有二级或者一级,能解决循环依赖吗? 谢飞机:其实我看过资料了,可以解决,只不过Spring要保证几个事情,只有一级缓存处理流程没法拆分,复杂度也会增加,同时半成品对象可能会有空指针异常。而将半成品与成品对象分开,处理起来也更加优雅、简单、易扩展。另外Spring的两大特性中不仅有IOC还有AOP,也就是基于字节码增强后的方法,该存放到哪,而三级缓存最主要,要解决的循环依赖就是对AOP的处理,但如果把AOP代理对象的创建提前,那么二级缓存也一样可以解决。但是,这就违背了Spring创建对象的原则,Spring更喜欢把所有的普通Bean都初始化完成,在处理代理对象的初始化。 面试官:飞机,不错嘛,这次了解了不少。那问个简单的,你撸过循环依赖的解决方案? 谢飞机:哦哦,这没有,没实践过!!!确实应该搞一下,试试。三、什么是循环依赖?1。问题描述 了解问题的本质再分析问题,往往更利于对问题有更深入的了解和研究。所以我们在分析Spring关于循环依赖的源码之前,先要了解下什么是循环依赖。 循环依赖分为三种,自身依赖于自身、互相循环依赖、多组循环依赖。但无论循环依赖的数量有多少,循环依赖的本质是一样的。就是你的完整创建依赖于我,而我的完整创建也依赖于你,但我们互相没法解耦,最终导致依赖创建失败。所以Spring提供了除了构造函数注入和原型注入外的,setter循环依赖注入解决方案。那么我们也可以先来尝试下这样的依赖,如果是我们自己处理的话该怎么解决。2。问题体现publicclassABTest{publicstaticvoidmain(String〔〕args){newClazzA();}}classClazzA{privateClazzBbnewClazzB();}classClazzB{privateClazzAanewClazzA();}这段代码就是循环依赖最初的模样,你中有我,我中有你,运行就报错java。lang。StackOverflowError这样的循环依赖代码是没法解决的,当你看到Spring中提供了getset或者注解,这样之所以能解决,首先是进行了一定的解耦。让类的创建和属性的填充分离,先创建出半成品Bean,再处理属性的填充,完成成品Bean的提供。3。问题处理 在这部分的代码中就一个核心目的,我们来自己解决一下循环依赖,方案如下:publicclassCircleTest{privatefinalstaticMapString,ObjectsingletonObjectsnewConcurrentHashMap(256);publicstaticvoidmain(String〔〕args)throwsException{System。out。println(getBean(B。class)。getA());System。out。println(getBean(A。class)。getB());}privatestaticTTgetBean(ClassTbeanClass)throwsException{StringbeanNamebeanClass。getSimpleName()。toLowerCase();if(singletonObjects。containsKey(beanName)){return(T)singletonObjects。get(beanName);}实例化对象入缓存ObjectobjbeanClass。newInstance();singletonObjects。put(beanName,obj);属性填充补全对象Field〔〕fieldsobj。getClass()。getDeclaredFields();for(Fieldfield:fields){field。setAccessible(true);Classlt;?fieldClassfield。getType();StringfieldBeanNamefieldClass。getSimpleName()。toLowerCase();field。set(obj,singletonObjects。containsKey(fieldBeanName)?singletonObjects。get(fieldBeanName):getBean(fieldClass));field。setAccessible(false);}return(T)obj;}}classA{privateBb;。。。getset}classB{privateAa;。。。getset}这段代码提供了A、B两个类,互相有依赖。但在两个类中的依赖关系使用的是setter的方式进行填充。也就是只有这样才能避免两个类在创建之初不非得强依赖于另外一个对象。getBean,是整个解决循环依赖的核心内容,A创建后填充属性时依赖B,那么就去创建B,在创建B开始填充时发现依赖于A,但此时A这个半成品对象已经存放在缓存到singletonObjects中了,所以B可以正常创建,在通过递归把A也创建完整了。四、源码分析1。说说细节 通过上面的例子我们大概了解到,A和B互相依赖时,A创建完后填充属性B,继续创建B,再填充属性A时就可以从缓存中获取了,如下: 那这个解决事循环依赖的事放到Spring中是什么样呢?展开细节! 虽然,解决循环依赖的核心原理一样,但要放到支撑起整个Spring中IOC、AOP特性时,就会变得复杂一些,整个处理Spring循环依赖的过程如下; 以上就是关于Spring中对于一个有循环依赖的对象获取过程,也就是你想要的说说细节乍一看是挺多流程,但是这些也基本是你在调试代码时候必须经过的代码片段,拿到这份执行流程,再调试就非常方便了。2。处理过程 关于本章节涉及到的案例源码分析,已更新到github:https:github。comfuzhengweiinterviewinterview31 以下是单元测试中对AB依赖的获取Bean操作,重点在于进入getBean的源码跟进;Testpublicvoidtestalias(){BeanFactorybeanFactorynewClassPathXmlApplicationContext(springconfig。xml);BeanAbeanabeanFactory。getBean(beana,BeanA。class);logger。info(获取Bean通过别名:{},beana。getBeanb());} org。springframework。beans。factory。support。AbstractBeanFactory。javaOverridepublicTTgetBean(Stringname,ClassTrequiredType)throwsBeansException{returndoGetBean(name,requiredType,null,false);}从getBean进入后,获取bean的操作会进入到doGetBean。之所以这样包装一层,是因为doGetBean有很多不同入参的重载方法,方便外部操作。 doGetBean方法protectedTTdoGetBean(finalStringname,finalClassTrequiredType,finalObject〔〕args,booleantypeCheckOnly)throwsBeansException{从缓存中获取bean实例ObjectsharedInstancegetSingleton(beanName);mbd。isSingleton()用于判断bean是否是单例模式if(mbd。isSingleton()){获取bean实例sharedInstancegetSingleton(beanName,newObjectFactoryObject(){OverridepublicObjectgetObject()throwsBeansException{try{创建bean实例,createBean返回的bean实例化好的returncreateBean(beanName,mbd,args);}catch(BeansExceptionex){destroySingleton(beanName);throwex;}}});后续的处理操作beangetObjectForBeanInstance(sharedInstance,name,beanName,mbd);}。。。返回bean实例return(T)bean;}按照在源码分析的流程图中可以看到,这一部分是从getSingleton先判断是否有实例对象,对于第一次进入是肯定没有对象的,要继续往下走。在判断mbd。isSingleton()单例以后,开始使用基于ObjectFactory包装的方式创建createBean,进入后核心逻辑是开始执行doCreateBean操作。 doCreateBean方法protectedObjectdoCreateBean(finalStringbeanName,finalRootBeanDefinitionmbd,finalObject〔〕args)throwsBeanCreationException{创建bean实例,并将bean实例包装到BeanWrapper对象中返回instanceWrappercreateBeanInstance(beanName,mbd,args);添加bean工厂对象到singletonFactories缓存中addSingletonFactory(beanName,newObjectFactoryObject(){OverridepublicObjectgetObject()throwsBeansException{获取原始对象的早期引用,在getEarlyBeanReference方法中,会执行AOP相关逻辑。若bean未被AOP拦截,getEarlyBeanReference原样返回bean。returngetEarlyBeanReference(beanName,mbd,bean);}});try{填充属性,解析依赖关系populateBean(beanName,mbd,instanceWrapper);if(exposedObject!null){exposedObjectinitializeBean(beanName,exposedObject,mbd);}}返回bean实例returnexposedObject;}在doCreateBean方法中包括的内容较多,但核心主要是创建实例、加入缓存以及最终进行属性填充,属性填充就是把一个bean的各个属性字段涉及到的类填充进去。createBeanInstance,创建bean实例,并将bean实例包装到BeanWrapper对象中返回addSingletonFactory,添加bean工厂对象到singletonFactories缓存中getEarlyBeanReference,获取原始对象的早期引用,在getEarlyBeanReference方法中,会执行AOP相关逻辑。若bean未被AOP拦截,getEarlyBeanReference原样返回bean。populateBean,填充属性,解析依赖关系。也就是从这开始去找寻A实例中属性B,紧接着去创建B实例,最后在返回回来。 getSingleton三级缓存protectedObjectgetSingleton(StringbeanName,booleanallowEarlyReference){从singletonObjects获取实例,singletonObjects是成品beanObjectsingletonObjectthis。singletonObjects。get(beanName);判断beanName,isSingletonCurrentlyInCreation对应的bean是否正在创建中if(singletonObjectnullisSingletonCurrentlyInCreation(beanName)){synchronized(this。singletonObjects){从earlySingletonObjects中获取提前曝光未成品的beansingletonObjectthis。earlySingletonObjects。get(beanName);if(singletonObjectnullallowEarlyReference){获取相应的bean工厂ObjectFactorylt;?singletonFactorythis。singletonFactories。get(beanName);if(singletonFactory!null){提前曝光bean实例,主要用于解决AOP循环依赖singletonObjectsingletonFactory。getObject();将singletonObject放入缓存中,并将singletonFactory从缓存中移除this。earlySingletonObjects。put(beanName,singletonObject);this。singletonFactories。remove(beanName);}}}}return(singletonObject!NULLOBJECT?singletonObject:null);}singletonObjects。get(beanName),从singletonObjects获取实例,singletonObjects是成品beanisSingletonCurrentlyInCreation,判断beanName,isSingletonCurrentlyInCreation对应的bean是否正在创建中allowEarlyReference,从earlySingletonObjects中获取提前曝光未成品的beansingletonFactory。getObject(),提前曝光bean实例,主要用于解决AOP循环依赖 综上,是一个处理循环依赖的代码流程,这部分提取出来的内容主要为核心内容,并没与长篇大论的全部拆取出来,大家在调试的时候会涉及的比较多,尽可能要自己根据流程图操作调试几遍。3。依赖解析 综上从我们自己去尝试解决循环依赖,学习了循环依赖的核心解决原理。又分析了Spring解决的循环依赖的处理过程以及核心源码的分析。那么接下来我们在总结下三级缓存分别不同的处理过程,算是一个总结,也方便大家理解。1。一级缓存能解决吗? 其实只有一级缓存并不是不能解决循环依赖,就像我们自己做的例子一样。但是在Spring中如果像我们例子里那么处理,就会变得非常麻烦,而且也可能会出现NPE问题。所以如图按照Spring中代码处理的流程,我们去分析一级缓存这样存放成品Bean的流程中,是不能解决循环依赖的问题的。因为A的成品创建依赖于B,B的成品创建又依赖于A,当需要补全B的属性时A还是没有创建完,所以会出现死循环。2。二级缓存能解决吗? 有了二级缓存其实这个事处理起来就容易了,一个缓存用于存放成品对象,另外一个缓存用于存放半成品对象。A在创建半成品对象后存放到缓存中,接下来补充A对象中依赖B的属性。B继续创建,创建的半成品同样放到缓存中,在补充对象的A属性时,可以从半成品缓存中获取,现在B就是一个完整对象了,而接下来像是递归操作一样A也是一个完整对象了。3。三级缓存解决什么? 有了二级缓存都能解决Spring依赖了,怎么要有三级缓存呢。其实我们在前面分析源码时也提到过,三级缓存主要是解决SpringAOP的特性。AOP本身就是对方法的增强,是ObjectFactorylt;?类型的lambda表达式,而Spring的原则又不希望将此类类型的Bean前置创建,所以要存放到三级缓存中处理。其实整体处理过程类似,唯独是B在填充属性A时,先查询成品缓存、再查半成品缓存,最后在看看有没有单例工程类在三级缓存中。最终获取到以后调用getObject方法返回代理引用或者原始引用。至此也就解决了SpringAOP所带来的三级缓存问题。本章节涉及到的AOP依赖有源码例子,可以进行调试五、总结回顾本文基本以实际操作的例子开始,引导大家对循环依赖有一个整体的认识,也对它的解决方案可以上手的例子,这样对后续的关于Spring对循环依赖的解决也就不会那么陌生了。通篇全文下来大家也可以看到,三级缓存并不是非必须不可,只不过在满足Spring自身创建的原则下,是必须的。如果你可以下载Spring源码对这部分代码进行改动下,提前创建AOP对象保存到缓存中,那么二级缓存一样可以解决循环依赖问题。关于循环依赖可能并不是一个好的编码方式,如果在自己的程序中还是要尽可能使用更合理的设计模式规避循环依赖,可能这些方式会增加代码量,但在维护上会更加方便。当然这不是强制,可以根据你的需要而来。