Spring面试精选java学习之路

java大厂面试题整理(十二)Spring循环依赖

2021-06-08  本文已影响0人  唯有努力不欺人丶

Spring是java的重要框架。这块的知识点也很多,这里只是简单的挑了比较高频的两个问题来讲:一个是spring的aop的顺序算是开胃菜,重点就是spring 的循环依赖。下面开始正文:

Spring 的aop顺序。

aop简单来讲就是面向切面编程。spring利用aop可以对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间耦合度降低。提高程序的可用性和开发效率。
这么说是很官方的语言,举一个实际工作中的例子:一般运营后台的一些增删改操作,我们常常会把操作人,操作参数,ip等记录成日志。这样的话某一天某个管理员失心疯故意损坏数据我们也可以通过这个日志定位到某个人,某个ip,从而来判断是盗号了还是怎么样的。
而这样的记录日志操作是每一个方法都要调用的。一般这个时候虽然我们也可以每个方法中调用同样的代码。但是更好一点的方式是直接将记录操作用aop的前置或者后置通知来做记录日志的操作。下面简单介绍下aop中常用注解:

以上的注解其实很容易理解,甚至没注解的话只看单词都能猜的差不多。但是其实这些注解的执行顺序是很有意思的。比如@After和@AfterRentruning/@AfterThrowing哪个先执行?这些执行顺序其实不是一成不变的。
我们现在常用的是Spring boot框架。但是18,19年比较流行的是Spring boot1。近两年用的都是Spring boot2.而sb1的底层是spring4.sb2的底层是spring5.
spring4和spring5中,这些注解的执行顺序是不同的。下面让我们简单的用代码看一下顺序。先用spring5,也就是spring boot2版本测试下,如下代码:


spring boot版本2以上

随便写个方法:


划重点,返回的是包装类
然后写环绕方法:
@Aspect
@Component
public class MyAspect {
    
    @Before("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
    public void before() {
        System.out.println("<<<<<<<<<<<<<<<<<before-前置通知");
    }
    
    @After("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
    public void after() {
        System.out.println("<<<<<<<<<<<<<<<<after-后置通知");
    }
    
    @AfterReturning("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
    public void aft() {
        System.out.println("<<<<<<<<<<<<<<AfterReturning-返回通知");
    }
    
    @AfterThrowing("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
    public void at() {
        System.out.println("<<<<<<<<<<<<<<AfterThrowing-异常通知");
    }
    
    @Around("execution(public Integer lsj.service.impl.AspectImpl.*(..))")
    public void around(ProceedingJoinPoint point) throws Throwable{
        System.out.println("<<<<<<<<<<<<<<<Around-环绕通知前");
        point.proceed();
        System.out.println("<<<<<<<<<<<<<<<Around-环绕通知后");
    }
}

这个也没什么业务逻辑,就是单纯的打印语句以便于看清执行顺序。重点是我们的方法不要返回数值类型,要返回包装类,不然会报个错。

访问这个方法
注意这里先用正常的数字访问,然后y=0访问(y=0会除以0报错)
正常访问执行顺序
异常执行顺序
同样的代码,我只改一下spring boot的版本:
![修改spring boot为1.x版本](https://img.haomeiwen.com/i16553345/70252c1784f4bc74.png?im 一张对比下:
spring4和spring5通知顺序变化
明显能看出来,这里主要的@After和@Around还有另外两个异常或者正常返回的顺序。明显在4的时候其实逻辑是不合适的、因为@After是无论如何都是必须执行的。既然这样应该是finally里的东西,也就是最后执行。所以在spring5改成了正常或者异常返回之后执行。而环绕的话有点类似于方法的第一行和最后一行的一个输出语句。代码走到了会执行。但是代码走不到就执行不了。下面一张简单的五种注解的位置:
五种注解执行位置如图
这个我们可以理解为输出语句就是切面的方法。至于这个方法能不能执行还得看代码能不能走到这个方法中。不要死记硬背执行顺序,而是要知道是在什么情况下才能执行到。
这个比较简单,就不多说了,下面说说spring的循环依赖。

Spring中的循环依赖

跟这个问题相关的问题有一下:

下面我们一点一点的介绍和学习。
什么是循环依赖呢?
简单来说就似乎多个bean之间相互依赖。形成了一个闭环。比如A依赖于B,B依赖于C,C依赖于A。下面用最简单的代码表示下这种情况:

循环依赖
而通常来讲,面试问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中属性互相引用的场景。如下代码示例:
spring容器中的循环依赖
注意只要形成闭环就算是循环依赖,不管是2个,还是多个bean。反正到这里我们起码简单的明白了什么是循环依赖。
两种注入方式对循环依赖的影响
循环依赖的提出和解决其实官网上也都有说,下面的官网截图:
spring官网对循环依赖的建议
spring官网说明中文版
其实这里也涉及到了两种spring的注入方式:构造器注入和setter注入。
而官网很明确的说了,构造器注入不推荐,容易产生循环依赖的问题。并且如果检测到代码中存在这种现象会报异常。
而我们AB循环依赖的问题只要A的注入方式是setter并且是singleton,就不会有循环依赖的问题(spring容器中默认都是单例的。而且B这样也可以解决)。
下面代码演示下两种注入方式的循环依赖:
构造器注入
如上A,B两个类。别说Spring了,我们自己都创建不出来A或者B对象了。因为创建A要先有B,创建B先有A。有点类似于先有鸡先有蛋的哲学问题了。所以说这个构造器的循环依赖是解决不了的。而setter方式注入的话,是可以解决的。
setter方式的循环依赖
如果代码是这样无论A还是B都是很容易创建的了。而且其实我们在实际代码中也经常这样做。比如说订单商品表和订单表。有时为了特定的需求会互相注入的。然后因为是用setter方式注入,所以两个对象都有默认的无参构造,所以很容易就可以创建对象。
其实上面说的都是纯java代码的循环依赖。下面开始说spring容器的循环依赖(其实原理类似。毕竟spring也是对java代码的封装)。
Spring中的循环依赖
Spring容器中默认的单例(singleton)的场景是支持循环依赖的。而原型(Prototype)的场景是不持支循环依赖的。
之所以这样的原因就似乎因为:Spring内部通过三级缓存来解决循环依赖的。
Spring中的三级缓存
说到spring的三级缓存,一个很重要的类要被提到了:DefaultSingletonBeanRegistry
我们可以直接看下这个类的源码:
DefaultSingletonBeanRegistry的三个map
这三个map中:

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题。而非单例的bean每次从容器中获取的都是一个新的对象。都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

debug调试查看spring三级缓存工作运作

想要明白运作过程有一些前备知识简单的说一下:
实例化/初始化:

三级缓存:

三级缓存的使用流程:

四大方法:

说了这么多前置知识,下面让我们跟着debug走一遍代码。亲眼看看执行过程:
代码还是之前的注入A,B两个组件的代码。简单贴出来:

准备代码
如上代码。然后我们跟着代码一步一步走:
注意最开始是几个run方法一层一层走我就不说了,直接到正经的方法中:
最后一个run不是套娃,是代码实现
注意我红色框起来的方法,这个方法执行完会在公平打印出A,B创建完成。所以真正的创建过程在这个方法里。另外说一个细节:在prepareContext方法执行完后会打印spring boot的logo。
其实我们完全可以第一个断点就下在这个SpringApplication类的315行(就是refreshContext方法这行。前面的都是跳来跳去的没啥意义)
然后走进这个方法:
顺着方法往下走到了这行
还是这个类,走到了758行。同样走完这行会A,B创建完。所以创建过程在这个方法里,进去看:
注意换类了
然后这个方法一步一步走,我是先无脑过。然后注意看控制台。确定这个创建过程发现的方法,一点点细化。这样做就是要不断debug。耐心点就好了。继续往下走:
框起来的方法创建了A,B
然后发现finishBeanFactoryInitialization(beanFactory);这行代码执行的过程中创建了A,B两个对象。所以咱们可以把断点设置在这行代码中并且重新debug:
更加细致的确认了创建的方法
我框起来的方法是创建A,B的方法。继续往里走:
getBean方法
注意这里定位到了getBean方法。而且这个方法是创建所有的bean,容器中有好多bean,所以这里要多跑几遍。注意看当前遍历到是是不是我们测试用的这两个对象。继续往下走:
getBean方法继续往下
doGetBean方法是四大方法之一。进入到这个方法后发现有个从容器中获取bean的方法:
getSingleton
同样这个方法也是四大方法之一。虽然我们知道这个时候肯定是容器中没有这个bean了,但是我们依然可以进入看看方法:
三个map挨个查找
然后方法一直往下走,
createBean方法
注意这里会走进这个getSingleton方法中,返回中调用了createBean这个方法。并且在这个方法中输出了A创建。我们进入这个方法打个断点。
准备创建a
这个代码的调试就是顺着一步一步往下走,其实一来可以向我那样无脑过,看控制台输出打印语句记住这行代码下次调试进入到方法里,也可以每一个方法都点进去瞅一眼,反正这个方法中很明显应该进入的方法就是我框起来的这个doCreateBean。
doCreateBean
进入到这个方法中前几行的逻辑也挺简单的。如果这个bean构造器是null。那么执行createBeanInstance方法。注意这个时候我们确定还没有创建a这个bean,所以可以考虑格外注意create的方法。
createBeanInstance方法
继续走进createBeanInstance方法:
createBeanInstance方法
首先这个方法是通过反射做了很多的事。
代码执行完这行发现A已经创建
emmmm...再一次跟丢了,不过这个时候a中的b是null:
a中的b是null
因为上面的A只是单纯的创建了,B也没填充,所以我去翻了一下createBeanInstance的方法,从注释可以看出来最后调用无参构造器创建了对象:
image.png
所以这一步就不重新走了,我们继续往下:
doCreateBean方法
继续走这个方法,注意看这个判断比较有意思,还记得当时三级缓存中三个map中的二级缓存的名字就是earlySingletonObjects吧。然后这个判断中有个变量,spring中默认值就是true:
开启三级缓存
所以这里的判断其实本质上就是判断当前bean是不是单例的。是的话就支持三级缓存,也就是进入到这个if方法中,如下方法:
addSingletonFactory
这里是一个lambda,我是先进入到方法中打个断点然后往下debug的:
addSingletonFactory方法
这个方法的逻辑很明了:如果一级缓存中没有这个bean,那么把这个bean放入到三级缓存中,并且从二级缓存中移除这个bean(其实事实上这个时候二级缓存中是没有a的,这个删除就是一个空删)。最后一个不是三级缓存中的map,所以先不管。继续往下走。
注意现在这个时候:a这个bean已经在三级缓存中了。
继续往下走代码又走到了四大方法之一:populateBean。也就是属性填充,注意这个时候我们要把b填充进来了:
populateBean方法
判断b属性容器中有没有
这块的方法名字比较明显,正常的情况下是没有的,所以这个走了下面这个分支:
applyPropertyValues方法
往这个方法里面走:
applyPropertyValues中有这么一行代码
这个方法是需要b这个属性但是b又没有,所以解决这个value。我们继续往里走:
这个方法的走向是进了这个分支
然后继续往resolveReference方法里走:
注意走到了getbean方法
这个方法代码走到了我红框框起来的方法,getBean。这个名字很眼熟吧,我们走进去:
兜兜转转,回到原点
到了这我们必须眼熟啊,之前创建a的时候从这里开始的。所以说现在需要b,所以b也要和a一样走一遍。这我就不一步步调试走了,因为A,B是我们自己写的,配置啥的都一样,我们盲猜就能猜到走和a一样的流程。
直接用文字来讲:
  1. getBean方法进入流程
    2.doGetBean方法开始去获取bean
  2. getSingleton方法试图直接从容器中拿bean(这个步骤没啥好说的,但是第一次进来肯定获取不到,所以走下面的步骤)
  3. 如果没有则再走getSingleton方法(注意这里和第三步的getSingleton是两个方法。上一步单纯的三级缓存中拿。而这个方法中会有业务逻辑)
  4. getSingleton方法中有个getObject方法是用匿名内部类的方式书写的(第四步中写的)。是createBean方法。
  5. createBean方法中经过一系列判断,进入doCreateBean方法
  6. doCreateBean方法中调用createBeanInstance利用反射真正的创建了B这个bean(但是这个时候是无属性填充的)
  7. 创建完后调用addSingletonFactory试图把这个bean添加到工厂(如果一级缓存中没有这个bean,则把这个bean从二级缓存中删除,放到三级缓存,但是其实正常来讲二级缓存中也没这个bean,所以多一步删除为了保险吧)
  8. 当前b这个bean已经完成初始化,接下来进入populateBean方法,进行属性填充
  9. 我们发现b中有个属性a,因为a我们之前已经实例化了,所以容器中有a,所以调用applyPropertyValues方法填充这个属性。
  10. applyPropertyValues方法中对所有属性进行resolveValueIfNecessary方法判断,a进入此方法
  11. resolveValueIfNecessary方法返回值是resolveReference方法的结果
  12. resolveReference方法会走进getBean方法试图从容器中拿a
  13. 循环走到getBean方法的doGetBean方法,进入到getSingleton方法获取bean
  14. 这个方法是第一个getSingleton方法,从三级缓存中依次获取bean的。而且注意这个时候a已经在三级缓存中了,所以进入最后一个逻辑中,从三级缓存中获取bean删除,并且放入到二级缓存中。而且这个doGetBean方法中会直接将a返回。
  15. 将a填充到b的这个属性中。
  16. 注意这里是重点。上面的逻辑中从第5步开始这些代码都发生在一个匿名内部类中。而这个代码的外层是 getSingleton方法。所以现在回到这个方法中。
  17. 这个getSingleton方法的最后一步是addSingleton方法。(getSingleton方法有多个重载,这里不要记混了)
  18. 这个方法将此bean也就是b放入一级缓存中,并且删除二三级缓存的此bean。
  19. 至此b实例化完成并且初始化完成。
  20. 我们创建b是在a需要b的时候走进来的。当b创建完成继续走a的填充属性的代码。把b填充给a。a也初始化完成。


    addSingleton方法将bean放入一级缓存并且删除二三级缓存中的bean
    getSingleton方法1
    doGetBean方法中用getSingleton方法传匿名内部类方式用createBean创建bean

而三级缓存能解决循环依赖的主要原因:bean的创建是分为创建原始bean对象和填充对象属性和初始化两个步骤的。也就是说Spring解决循环依赖依靠的是bean的“中间态”这个概念。中间态是指已经实例化但是还没初始化的状态。
三级缓存的学名:

所以循环依赖中我们可以先解决有没有的问题。然后再去一点点填充属性。而之所以必须单例才能实现循环依赖也是这个原因。我感觉说到这里循环依赖就说的挺明白了。

一点不夸张的说这段debug我用了三四个小时才算是理明白这个代码的走向,流程什么的。而且就像一开始说的一不小心就debug丢了,一切从头再来。但是这个是真的很有意思的一个东西,有时候调着调着就会觉得写出这种代码的人简直神仙。首先阅读spring源码可能除了吹NB不会有什么立刻马上的明显的提高。毕竟工作中写个循环依赖啥的太扯了。但是阅读本身是一个开拓思路的好途径。会有一种恍然大悟的感觉,啊,原来代码还能这么写。另外也是一个学习方式,毕竟一般跳槽大多数都会接手现有项目,所以学会阅读源码,快速理解源码都挺有用的。甚至换个角度:看别人写的好的小说是放松,看别人写的好的代码不也是享受和放松么?这是一种很有成就感也很有意义的事。学会享受阅读源码,尤其是比较复杂的源码真的不错。
另外我一直强调的一个观点:别难为自己。如果在代码中找不到乐趣,看代码如上坟,就换个工作吧。这个社会搬砖也能养活自己,还不用动脑。干嘛非要去做让自己抑郁的事呢?如果只是为了混日子也没必要学这些,还是那句话,放过自己。
本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝大家工作顺顺利利,生活健健康康~!另外因为我也不知道我图文表述的够不够清楚,如果有同样在阅读spring源码的小伙伴可以留言或者私聊我加个好友一起讨论呀。

上一篇下一篇

猜你喜欢

热点阅读