[后端]Spirng循环依赖全方位理解,读懂后吊打面试官(附面试回答模板)

一.提示:

  • 全文共1万5千+字 , 阅读全文耗时较久 , 如果只需要面试回答模板 , 可以点击侧边目录最底层,即可立即到达
  • 文章内知识点众多,强烈建议搭配侧边目录逐级向下阅读
  • 目录4和5 是该文章的核心 , 必看

二.前言

本文章参考的其他文章/视频/图片:

spring中为什么要三级缓存?二级不行吗?https://cloud.tencent.com/developer/article/2007243

Spring源码最难问题《当Spring AOP遇上循环依赖》https://blog.csdn.net/chaitoudaren/article/details/105060882Spring 为什么要用三级缓存来解决循环依赖(AOP),二级缓存不行吗https://blog.csdn.net/qq_48051666/article/details/135000021【Spring源码三千问】哪些循环依赖问题Spring解决不了?https://blog.csdn.net/wang489687009/article/details/120546430

【Spring源码三千问】@Lazy原理分析——它为什么可以解决特殊的循环依赖问题?https://blog.csdn.net/wang489687009/article/details/120577472讨论:Service层需要接口吗?https://zhuanlan.zhihu.com/p/337211773别再说 Spring AOP 默认用的是 JDK 动态代理https://blog.csdn.net/HongYu012/article/details/123819787

本文章内容概况:

1.知识汇总

对目前网上SpringBean循环依赖的相关文章进行了知识汇总, 使知识点前后串联, 有利于加深Spring循环依赖相关知识的印象  例如:

  • AOP动态代理和循环依赖
  • 循环依赖相关注解和配置
  • 三级缓存执行流程和本质 

2. 最佳实践

对不同类型循环依赖的测试 和 对其底层原理的解析

本文章出现原因:

  1. 起因是因为要背一个面试题 "spring的三级缓存执行流程" 由于本作者非常善于钻牛角尖,所以针对这个面试题相关的其他知识点越挖越多,于是产生了整理一个文章的想法
  2. 目前网上针对spring循环依赖这么全面描述和详尽测试的文章比较少
  3. 为了在面试官问我spring的循环依赖是什么的时候 吊打面试官

本文章的优势:

  1. 文章内不含Spring源码 , 因此看起来没那么枯燥 
  2. 详尽的图例 和 代码测试图例   
  3. 数天的打磨  通俗易懂的话术 
  4. 完善的Spring循环依赖相关知识点汇总

本文章的缺点: 

  1. 涉及知识点众多  可能显得繁琐 
  2. Spring底层源码对文章的部分观点内容支持不强

三.测试项目准备

在文章后续,无论是bean循环依赖的测试,亦或者一些其他知识点的测试,都需要代码支持,因此我们需要先创建一个springboot项目,用做测试项目 , 测试项目的创建比较简单,主要分为4步

1.创建项目

2.引入依赖

3.创建启动类

4.创建bean类和项目运行时测试接口

当测试项目准备就绪后,我需要说明一些注意点

  1. 一些Bean类会随着测试的不同  , 进而会不断修改内部代码逻辑  ,  亦或者一些Bean类会被删除 一些Bean类会被新建.
  2. 测试项目运行时接口由Apifox触发,目的是为了观察项目运行中,这些bean对象的特征 , 它可以让你知道某个bean对象注入的其他成员属性bean对象 , 是否被注入成功,以及某个bean对象是否为代理bean对象 等等.. 这非常重要!
  3. 测试项目架构预览

四, 必懂知识点

文章主体部分的内容包括,不同类型的循环依赖类型和其代码测试,以及一些理论知识描述,这当中会涉及到一些其他知识点,那么为了保证文章主体部分的连贯性,因此在阅读文章主体部分之前,你必须要了解并掌握以下知识点:

获取IoC容器中bean对象的方式

1.ConfigurableApplicationContext类

这种方式比较简单 只需要在启动类声明一个 ConfigurableApplicationContext类的 静态成员

并且让其=run方法的返回值即可

使用时,直接通过启动类.静态成员的方式 去调用getBean之类的方法

2.ApplicationContextAware 接口

这种方式封装性较好,但实现较为复杂,首先我们创建一个任意类名的类,例如ApplicationContextHelper, 并且让其实现ApplicationContextAware接口

然后对ApplicationContextHelper类增加一个静态成员ApplicationContext ac;

紧接着 去实现ApplicationContextAware接口的setApplicationContext方法,并且让自己的ac静态成员=setApplicationContext方法的入参, 这是固定写法

后续我们只需要调用ApplicationContextAware类的静态成员ac即可

查看bean是否是动态代理

1.启动项目,并且下断点观察bean的内存名称

 2.

如果是下面这种 说明是基于CGLIB动态代理技术实现的

如果是下面这种 说明是基于JDK动态代理技术实现的

如果是下面这种  说明不是代理bean 就是一个普通的bean对象

注意了哈, 这里只需要知道怎么看是否动态代理就行了,至于什么是动态代理 我马上解释

什么是AOP动态代理?

如果想要完全理解spring的三级缓存本质,那么AOP动态代理技术的底层原理和本质一定是你绕不开的一个知识点,那么什么是动态代理? 我从以下方面进行解答:

  • 动态代理的诞生思想和原理?
  • 在spring环境中怎么样触发动态代理?
  • 最常见的动态代理理解误区
  • JDK动态代理和CGLIB动态代理
第一 : 动态代理的诞生思想和原理
  • 假如现在产品经理提出一个需求,让你对目前项目中所有的controller层接口进行平均执行耗时统计,以此测试出哪个接口性能较慢,进行针对性改造,
  • 假如现在产品经理又提出一个需求,让你对所有service层的接口,进行方法入参和出参的日志记录,以此保证系统的数据来去可追查
  • 假如现在某些mapper层的SQL方法执行较慢,你需要开启异步子线程来解决这个现象
  • 假如....

可以发现的是  , 这些需求都具有同一个特征 ,  那就是和我们当前接口的业务完全没关系,并且不管我们对多少个接口进行这样的逻辑增加,其增加的代码都是一样的,此时AOP概念横空出世 , 也就是"面向方法编程" ,  通过简单注解标记, 即可对一个方法实现前后增强, 使原来需要重复写多份的代码只定义一份即可:

也就是从这样:

变成了这样:

现在解决了重复代码的问题 , 那么在我们程序运行中, 如何调用这些被增强的方法呢?

继续通过原始对象调用 显然是不可能的,因为现在重复代码已经不在原有的接口内部定义了,通过原始对象调用,则不会触发重复代码的执行 , 也就是这样:

而我们预期是希望这样:

但是显然原始bean对象是无法触发重复代码的执行的,因为那些代码已经不属于它了,此时代理对象就来了

代理对象: "老弟,我来帮你执行那些重复代码,你自己好好做自己的事情就可以咯"

以上就是动态代理的诞生思想和原理 希望这些图可以帮助你理解动态代理

第二:在spring环境中怎么样触发动态代理?

对一个类的方法陈列@Transactional注解  此时BeanA会被生成一个代理beanA

对一个类的方法陈列@Async注解  此时BeanA会被生成一个代理beanA

对注入的成员陈列@Lazy注解,此时注入的成员b会是一个代理b对象,而非原始b对象

第三  最常见的动态代理理解误区

注意了哈, 这些误区是我对动态代理一直的错误理解,为此踩了很多坑,现在有必要发出来让大家避雷

我举个例子: 现在beanA类定义如下, 然后我从IOC容器获取beanA 并且执行func()方法

那么从IOC容器中得到的beanA是谁?

而func()方法中的 this 的beanA又是谁?  如图:

答案: 前者是代理beanA对象 , 后者是原始beanA对象

如果你不理解 那我再掏出这个图 你就明白了

也就是说 代理对象只负责执行因@Async @Transactional 这些注解生成的重复代码 ,  而核心代码还是由原始bean对象自己去调用执行的 , 因此func中的this就是原始beanA对象

了解完上面这个误区之后 我再进阶升级一下挑战难度 , 现在ab2个bean定义如下

首先我从IOC容器直接获取2个bean 那么结果不容置疑 a是代理beanA ,  b是原始beanB

那么我再次访问这个代理beanA的成员属性beanB呢 ? 你会发现 诶,怎么是空的? spring你坑我啊 说好的注入呢!!!

其实这不是spring在坑你 spring确实把代理beanB给你往beanA注入了 但是它是往原始beanA中注入的 , 而不是往代理的beanA中注入  如图:

此时你可能有疑问 , 那我就文字+图 给你解释一编 为什么是这样

如果我们不给原始beanA注入代理beanB对象 就会导致beanA类中的func中的beanB为null , 因为 b 等同于 this.b  , this其实是被省略的 ,     那么this是谁? 其实就是原始的beanA , 因为是它调用的核心方法 , 也就是说 ,  如果spring不这样做 你以后代码可能要这样写了

好了 以上就是spring动态代理的误区了 如果你看完并理解的话 恭喜你今天又进步了 如果你还不理解的话 或者是我写的不清楚的话 欢迎留言告诉我 我会继续打磨修改

第四:JDK动态代理和CGLIB动态代理

Spring在2.X版本之前  用的是JDK的动态代理技术

Spring在2.X版本之后  默认是用CGLIB的动态代理技术

  1. JDK动态代理要求 : 被代理的类必须实现自一个接口,否则无法生成代理对象,因为代理对象是通过和原始类实现相同的接口,从而实现方法增强的
  2. 而CGLIB则没有这些规定,因为CGLIB是通过生成一个子类,然后继承自原始类,进而子类包装父类的方法,从而实现方法增强的

如图:

如何配置:

通过yml文件配置: 当有了这个配置之后 某些代理对象将使用JDK的技术来实现

注意点:

尽管上面所说的配置打开后 , Spring也不是百分百使用JDK动态代理 换而言之:

被代理的类必须满足能够使用JDK动态代理的条件之后 , 那么才能使用JDK的动态代理

  1. JDK动态代理 和 CGLIB动态代理  是可以同时存在的
  2. 以下这些点是对 JDK动态代理失效的研究 和 JDK动态代理报错的研究

a. JDK动态代理失效 : 仅仅是定义了一个接口 而被代理的类并没有实现该接口   如图:

其中@Async注解是为了让该类产生动态代理对象 此时不用太过于纠结 后续会讲到这个注解, 但是可以发现的是 BeanA依然使用的是基于CGLIB的动态代理

b.JDK动态代理失效 : 被代理的类仅仅是实现了接口 而没有实现接口的任何抽象方法

c.JDK动态代理成功 : 如果想要满足JDK动态代理触发必须要这样写:

d.JDK动态代理报错 :

其他bean尝试注入被JDK代理的原始类的bean对象  例如直接注入BeanA 而非注入BeanAInterface , 这样会导致你的项目启动时就报错, 若是CGLIB动态代理 则不会触发此错误

e.JDK动态代理报错 :

期望从IOC容器中获取被代理的原始类的bean对象 例如期望直接获取BeanA 而非BeanAInterface 会导致报错: 没有这样的beanA

f.JDK动态代理报错底层原理:

CGLIB是新建一个子类作为代理对象 , 因此你既可以获取父类也可以获取子类 , 这并不影响 , 想用哪个用哪个, 但JDK则是使用一个接口完全代替你的原始bean对象 ,  一切访问交由该接口决定 你无需关心底层操作 , 因此它禁止你再去访问原始的bean对象


e.但是BeanA的原始对象真的无法访问了吗?

不 SpringTest依赖 提供了一些工具方法 可以获取到一个代理对象的底层原始对象 该方法适用于JDK代理对象 也适用于CGLIB代理对象 , 不过这种操作实际生产不会用到,这也是为什么它在test包中

结论: 

CGLIB实现简单 限制更少  而JDK实现复杂 限制较多

为什么Service层总是1个接口+1个类?

如图:

原因:

1.解耦 当后续业务有不同情况的迭代时 新增实现类即可 而非修改原始的类

2.为了使用JDK动态代理

结论:

就目前来看一般的项目完全用不到service层1个接口多个实现类

一般来说直接只写1个类即可  根本用不到接口  没必要那么复杂

@Lazy注解是什么?

在默认情况下,Spring项目启动时,会把所有扫描到的bean对象 , 立即加载到IoC容器 , 但是在某些情况下, 我们希望某些bean对象只有在用到的时候 , 才去加载到ioc容器中, 也就是懒加载 , 通过懒加载我们做到提高spring项目启动速度 , 不提前占用系统内存空间等 操作 , 而@Lazy注解就可以对这些bean对象使用 , 从而达到此效果

但是上面这段话  对我们理解@Lazy注解本质没什么实质帮助 , 因为懒加载注入的这个行为 我们是无法感知到的 若要想全面理解@Lazy注解的一些特性 我们还需要创建2个bean做一些测试 , 如下:

a注入了b  并且是使用的@Lazy注入的b , 这意味着a中的成员属性b将会被延迟加载,只有用到时才会注入,也就是说, 我们预期是这样的,如图:

但是当我们项目运行时, 真的会和我们预期一样吗? 来测测看 :

首先从IOC获取beanA  然后通过调试工具点击beanA的构成 , 诶 , 你会发现,这特喵怎么会有一个beanB对象啊,我还没开始访问成员beanB呢 !

而且 , 有就有吧 , 怎么还是个代理的beanB对象!!

那为什么会是这样? 当我们第一次了解@Lazy注解的作用  根本不会想到还和aop代理有关系

我们预期的想法一定是这样的:

"懒加载就懒加载呗, 只是慢一点吧原始的beanB对象注入到beanA中罢了  并且被注入的beanB肯定和原始的beanB对象是一样的啊"

但很显然 , 这样的理解是错误的, Spring也并不是这样做的

我们来看看spring对懒加载的真正操作: 

懒加载并不是指的beanA中的beanB会被懒加载 , 而是beanA中的代理beanB中的原始beanB会被懒加载, (听起来很绕口,看图即可) 也就是这样:

也就是说代理beanB更像是一个约定, 它始终会在第一时间将自己注入到原始beanA中,而当我们真正的去访问beanA中的beanB时, 也是通过代理beanB去访问的,并且代理beanB此时才会去真正的加载原始beanB到IOC容器中,并且赋值给自己 , 那么是不是这样呢? 我们再做一个测试 : 在beanB中 , 显式的去声明构造函数,并且打印一句话:

此时  项目启动...

诶, 你会发现这句话居然随着项目启动一起被打印了, 此时你心里一定会想: "md 作者你坑我 , 刚刚可是你自己说的 , 懒加载是针对原始beanB啊 怎么现在直接就被加载了?"

其实我想说  , 上面的bean定义其实就是一种懒加载失效的一个场景 , 注意了哈 , 我们只是在beanA中注入的beanB上面添加了@Lazy注解, 而这和原始beanB并没有什么关系, 因此原始beanB该怎么注入到IOC还是怎么注入到IOC,如图:

因此 , 要想真正让beanB做到懒加载 , 那么必须在任何用到beanB的地方 都声明@Lazy, 一旦有1个地方注入了beanB,而又没有声明@Lazy,就会导致懒加载失效,从而让beanB在项目启动阶段直接注入 , 所以我在beanB上面也陈列一个@Lazy注解

此时,再次启动项目看看 : 首先确定的是没有构造函数的日志打印,其次IOC容器视图中也没有beanB,这说明了现在 beanB确实是被懒加载的

然后让我们访问一下beanA中的beanB(直接从IOC访问beanB也可以), 发现beanB构造函数执行,beanB此时真正被实例化

并且@Lazy注解并不会让beanB自身产生代理,也就是说我们从IOC容器获取beanB依然是原始beanB

ok , 了解完@Lazy注解的真正本质后, 我们还要填一个坑: "为什么需要beanA中需要代理beanB的存在? "

我觉得有2个原因:

1.如果代理beanB不存在,那么beanA无法感知自己的成员beanB是否已经被注入,这可能造成空指针异常, 或者增加spring对这种懒加载注入监控的性能耗费 , 但总的就是一句话 "不好管理"  ,  如图:

2.第二 , 也是最重要的一点:  "如果beanB在全局被懒加载注入,例如BeanACD都懒加载注入beanB , 然后在spring初始化阶段 , 原始beanB始终为null ,  那么spring就无法给这个beanB创建代理对象 , 从而进行方法包装增强 "

如何理解这句话呢? 众所周知,代理对象也是可以被代理的,也就是代理是可以无限套娃的, 那么如果注入的beanB对象真正需要被代理时 , 而我们又因为懒加载而导致它是null, 就会让那些准备套娃代理的对象,无法找到代理目标, 最终导致spring无法完全完成bean的初始化操作 , 如图:

以上就是@Lazy注解的全部了解啦 , 希望看完对你有所帮助

@Async和@EnablAsync注解是什么?

@Async注解和@EnablAsync注解是需要一起搭配使用的

一般来说  , 我们将@Async注解陈列在bean对象的方法上 , 将@EnablAsync注解陈列在调用该方法的类上 , 以此就能做到让该bean的方法变为一个异步方法,也就是多开一个子线程去执行该方法 , 就像这样 :

@Async的底层原理:

陈列了@Async方法所属的bean类对象 会被aop代理 生成一个代理bean对象 以此才能在方法前后开启和关闭子线程的执行

@Scope注解和单例Bean和多例Bean的关系?

@Scope注解用于声明一个bean的作用范围 和 生命周期长短

我们可以通过给bean类陈列@Scope注解   来指定该bean类生成的bean对象为单例还是多例

  • 单例模式的bean:

IoC容器中只会存储一个 此后无论是其他bean来注入 或者 再次主动获取该bean那么只会拿到刚开始生成的那1个,如图:

  • 多例模式的bean:

spring在刚开始加载时 若针对该bean有注入需要 那么会生成一个bean对象 此后任何一次的从IoC容器获取该bean的操作 都将新创建一个这样的bean对象,如图:

  • 当bean类没有声明@Scope注解 那么默认为单例模式
  • 当bean类声明了@Scope注解为@Scope("singleton") 那么该bean类为单例模式
  • 当bean类声明了@Scope注解为@Scope("prototype") 那么该bean类为多例模式

此外 @Scope注解还有以下其他配置

  • "request"(在一次请求内,会使用同一个bean对象,第二次请求就再生成1个bean对象) 
  • "session"(在一次会话内,会使用同一个bean对象,第二次会话就再生成1个bean对象) 
  • "globalSession"(在一次全局会话内,会使用同一个bean对象,第二次全局会话就再生成1个bean对象)

然后 全局会话 和 会话 和 请求 我的理解如下:

会话指的就是用户打开我们服务的一个页面  那么会产生一次会话  , 基于这个页面产生的每次前后端交互就对应多次请求  , 而全局会话则是用户打开的多个页面(当然这是我问GPT得到的答案)

还有一种理解是这样的 , 1个用户在我们服务的全局页面被称之为会话, 多个用户在我们的服务页面就对应多次会话, 而全局会话指的就是多个用户针对我们服务的页面)

我大概率猜第二种理解是对的

但是这里就不详细了解和展开了 因为这个文章只需要知道单例bean和多例bean即可

五.文章主体

ok 当你了解完上面那么多知识点之后 终于来到正文了,下面我们将要对不同类型的循环依赖进行代码测试 ,目的是为了了解其本质和解决方案 ,在这之中会用到刚刚所了解的那些知识点,文章主体部分也分为3个部分

  1. 先说明循环依赖是什么?
  2. 测试计划
  3. 具体的测试和原理探究

循环依赖是什么?

对于循环依赖 , 一般我们说的就是  2个bean互相引用对方作为自己的成员属性
那么此时要想初始化a对象必须要有b对象 而要想初始化b对象 必须要有a对象 此时就会导致报错的一个场景,就像这样子 a依赖b  b依赖a:

当然了, 也有这样子的: a依赖b  b依赖c   c依赖a

甚至还有这样子的 a依赖a , 话说谁会这么干呢? (小声bb)

但总的来说 循环依赖就是这么个意思  ,请注意 上面演示的只是循环依赖的大概架构 是用于描述循环依赖的思想 ,并非是具体的循环依赖的类型  和我后面所说的各种循环依赖类型是不一样的

测试计划

  • 由于循环依赖有很多种类型(大概有七,八种),因此我们需要定义一个标准的测试流程,并且每一种类型我会用一个"块"来包裹起来, 所以总共会有七八个测试块 , 让大家看的不那么辛苦
  • 我也会贴出造成这种循环依赖的bean定义 ,  一般是图片
  • 我将会针对每一种循环依赖的细分类型  按照以下流程去做测试 , 其中针对于 "项目运行时" 的测试,我会定义一些代码,去访问一些bean或者是它的成员属性bean , 最后结合代码和调试工具来得出结论

  • 基本上每一种测试的内部目录划分都遵循这个:

不同类型的循环依赖测试和原理研究

在该文章中 , 我大概对循环依赖的类型 , 细分分为6类 ,也就是酱紫的:

  • 普通bean的@Autowired注入的循环依赖                  (重点,简单了解一下三级缓存)
  • 普通AOP代理bean的@Autowired注入的循环依赖   (重点,三级缓存方案的诞生由来和本质) 
  • 特殊AOP代理bean的@Autowired注入的循环依赖   (扩展,AOP代理的时机前后) 
  • 普通bean的@Autowired+构造函数注入的循环依赖 (扩展,三级缓存所依赖的构造函数) 
  • 普通多例bean的@Autowired注入的循环依赖           (扩展,多例bean和三级缓存的碰撞) 
  • 普通bean的@Autowired+seter方法注入的循环依赖 (没啥用,简单了解) 

但实际上每个人对于这个的分类都略有不同,这个不用太在意

第1类: 普通bean的@Autowired注入的循环依赖
说明:

这种bean的循环依赖是我们最常见的, 基本上你参与开发的项目都存在这种问题 , 除非你的项目完全遵守面向对象思想编程,才能避免这种情况,但大多数时候我们为了写代码简洁和方便 都会不经意间让某些bean形成循环依赖的关系

bean定义:

a依赖注入b    b依赖注入c    c依赖注入a

项目启动:

报错原因:

bean之间形成了循环依赖 bean1需要等待bean2创建完毕 然后供自己注入 而bean2需要等待bean1创建完毕 然后供自己注入 最终导致报错

解决方案:

解决这个报错比较简单,在yml配置加上这个配置即可

 解决原理:

当我们声明allow-circular-references: true时,代表允许spring的bean循环依赖,此时spring会采取三级缓存的策略来解决bean的循环依赖

那么三级缓存是什么?

三级缓存是spring为了解决带有aop代理的bean的循环依赖采取的一种代码实现方案 :

  • 一级缓存 : 存放bean对象的成熟态 可以直接拿来用
  • 二级缓存 : 缓存早期的引用对象 , 未被填充属性 , 可能是原始bean对象 , 可能是aop代理引用bean对象
  • 三级缓存 : 放能能获取bean对象的工厂对象,可以通过工厂对象得到一个二级缓存的对象

三级缓存的执行流程?  (如果文字描述不清晰 建议看图)

我这里为了方便描述,就假设造成循环依赖的bean  只有A和B , 而不是上面的ABC ,并且在描述前,先分清楚2个概念: 

1)实例化: 对象只被调用构造函数,创建出了一个引用,尚未被填充属性

2)初始化: 对象的所有属性均被填充完毕 可以正常使用

  1. 首先spring容器开始初始化a对象 , 但会先尝试在三个缓存中都获取一遍,但第一次肯定获取不到 , 那么就反射调用a的构造函数 , 来实例化一个a对象 (此时a对象属性均为空),  并且将该a对象包装为一个工厂对象放入三级缓存中
  2. 然后继续对刚刚实例化的a对象填充属性,此时发现a对象注入了b对象 , 那么就暂停a对象的属性填充 ,转而去开始初始化b对象,那么此时初始化b对象的过程就和第1步操作一样的了(b对象被调用构造函数,实例化1个b对象 并且该b对象被包装为b工厂对象放入三级缓存中)
  3. 紧接着,spring开始对b对象填充属性 ,此时又发现b对象又注入了a对象,那么就从三个缓存中尝试获取a对象, 而此时就能从第三级缓存中拿到能得到a对象的工厂对象 , 通过工厂对象的getObejc()方法得到一个之前已经实例化好了的那个a对象 , 然后删除该a工厂对象的三级缓存 , 并将刚刚得到的a对象放入第2级缓存中
  4. 此时b对象获取到了a对象之后 , 就能够完成自己的属性填充 ,然后确认初始化成功 , b对象直接被放入一级缓存 , 然后b对象会删除自己的三级缓存,
  5. 当b对象初始化成功 , 就会恢复a对象的属性填充 ,此时a对象要想获取b对象就可以直接通过一级缓存拿到b对象,进而完成的自己属性填充,,然后确认初始化成功,a对象再将自己放入一级缓存 , 然后a对象会删除自己的二级缓存,
  6. 至此 ab 2个对象的循环依赖解决完毕 , spring开始处理其他的bean对象...

如果你看懂了上面的流程图 , 你一定会产生这个疑问:

"为什么要用3级缓存这种方案?  我看这种方式的话只用2级貌似都可以了啊"

图例如下:

可以看到 , 用2级缓存也可以完美解决循环依赖, 但是我等下测试的第二种类型的循环依赖 , 也就是 "普通AOP代理bean的@Autowired注入的循环依赖"  就无法用上面这种二级缓存解决了,这也就是为什么spring采取的是三级缓存

那么所谓的 "普通AOP代理bean的@Autowired注入的循环依赖"  到底是个什么东东呢 , 先别急  , 我们这一种"普通bean的@Autowired注入的循环依赖"  还没有测试"项目运行时"  , 为了遵循测试计划  , 这是必须要进行的

项目运行:

这里的代码测试大概分为2块:

1.首先 我们直接访问原始bean对象 拿到内存名称  看看有没有问题

2.其次 我们去用调用链的方式 查看这些bean内部成员的内部成员 也就是套娃的形式去访问内存名称 看看有没有问题

bean对象访问无异常 项目运行完全正常 没有任何隐患 所有bean都可以被信任

报错原因: 无报错
解决方案: 无需解决
补充:  第一类循环依赖 测试完毕
第2类: 普通AOP代理bean的@Autowired注入的循环依赖
说明:

这种循环依赖, 工作中的项目也经常遇到 , 只要你自己写过AOP的方法 , 自定义过AOP注解 或者 你用过@Transactional注解  , 然后你的bean又循环依赖了,  那么就属于这一种类型的循环依赖

bean定义:

a依赖注入b b依赖注入a  且ab2个对象都有@Transactional事务方法 也就是需要被动态代理

项目启动:

报错原因:

bean之间形成了循环依赖 , bean1需要等待bean2创建完毕 然后供自己注入  ,而bean2需要等待bean1创建完毕 然后供自己注入  , 最终导致报错

解决方案:

解决这个报错比较简单,在yml配置加上这个配置即可

解决原理:

还是依靠spring的三级缓存解决的

注意了哈 ,这里是本文章最最最重点的部分 , 让我想想该怎么和你解释呢  emmmm.......

啊 ! 我知道了 , 我会按照这样的思路和你解释 :

  • 我们预期想让bean如何注入                 (bean们如何注入到IoC 以及 如何注入成员bean)
  • 基于2级缓存是否能达到我们的预期?   (能和不能,这是个好问题)
  • 3级缓存是如何达到我们的预期的         (新3级缓存的执行流程)
  • 总结                                                      (不来点总结 总觉得缺点儿什么)
首先来看 我们预期想让bean如何注入 ?

从2点出发,

第1从IoC的角度出发,预期想让ab2个bean都以代理对象的形式注入进来

第2从bean们的角度出发,预期想让自己注入的其它bean都以代理的形式注入进来

也就是这样(图例):

因为如果不按照我们的预期 , 那么@Transactional事务方法就会失效,

我们肯定不想通过某个bean去调用其事务方法时 , 却调用的是原来的原始方法吧

或者 调用某个bean内部注入的其他bean的方法,却也是调用的原始方法

(通过代理对象.func()就是事务方法,通过原始对象.func()就是未被增强的原始方法)

再来看第二点 基于2级缓存是否能达到我们的预期?

直接上图:

观察这个流程图 你会发现一个问题 我来描述一下:

首先IOC 容器中注入的2个bean最终确实是代理对象,但是代理beanB中注入的成员beanA却是原始的beanA对象,而非最后经过增强的beanA代理对象,也就是说,注入结果变成了这样:

如果最终注入的结果是这样,就会导致我们在beanB中调用自己的成员beanA的方法,调用的是原始的func() 并不是经过事务增强的方法,就导致事务失效

那么造成这个结果的原因是什么? 我觉得最重要的一点就是:

"AOP代理操作是属于bean的后置操作,也就是在bean初始化完成之后才会执行,这也就导致了循环依赖中的bean总有一方会错误注入"

但是问题来了,二级缓存真的没办法解决这个问题吗? 我可以肯定告诉你 可以解决 ,如图:

但采取这样的架构解决 会带来一些问题 目前我理解的有2个问题

问题1:

如果像这样在bean初始化时 就去检查是否需要循环代理,并且进行代理操作 , 那么这违背了bean的创建规范 , 如图

但是这是个小问题 , 带来的后果大概就是以后框架维护成本高了

问题2:

如此一来, 等于放弃了原始的bean对象的初始化了 , 全部操作的是代理bean对象 ,  而原始对象则未经过任何成员属性填充 , 那么这就很严重了 , 我根据上面的bean定义来举个代码例子,

我这样抛出2个图, 第一个图中的beanA通过IOC容器获取 是完完整整的代理beanA对象,这个没有异议,而第二个图中的beanA是通过这个代理beanA去调用func()方法,然后内部通过this拿到的 , 那么问题就是 , 你们猜猜图2的beanA是谁?

OK 我不买关子了  直接上链接 哦不 上内存名称

可以发现从IOC直接获取的beanA是代理beanA 而通过代理beanA.func()内部的this,却是原始beanA对象

如果有同学对这个结果产生了疑问,说明你对AOP动态代理的理解还不够深啊 , 不管是基于JDK的动态代理, 还是基于CGLIB的动态代理 其本质上都是对方法的增强 , 而非重写 , 因此无论什么时候 触发原始方法的 都是通过原始bean去点点点的 那么this必定只会是原始bean对象

然后知道了this是谁之后 如果我们这样调用呢

答案就是空指针 , 因为this这个原始bean在IOC初始化阶段就没被操作过任何的属性填充, 所有属性填充都是针对带来bean对象的 因此要想不空指针 我们必须这样调用

哈, 其实这也没多大关系嘛 ,  只是让coder们编码时麻烦一点呗  要不我说spring就这样干得了

好了 本文章结束 

但是上面的图例 , 我们是在后续操作的是代理对象 , 那么如果不操作代理对象 , 继续操作原始对象呢? 如图

这种也有问题 , 就是代理对象和原始对象之间的一个互动和感知太差了 , 有时要从代理转为原始对象 , 有时又要从原始对象替换为代理对象 ,  如果这样写架构, 会让框架变得难以维护

ok 进入第三部分 3级缓存是如何达到我们的预期的

我这里就不对spring的改造过程的想法进行意淫了  毕竟我不是写框架的人 , 我这里画一个三级缓存的详细流程图 然后做一个总结

三级缓存执行流程图(解决AOP代理对象循环依赖):

总结:

        三级缓存就是spring为了解决带有aop的bean的循环依赖的一个解决方案, 如果bean只是单纯的循环依赖,并不涉及aop,那么是用不到三级缓存的,其实用二级缓存就能解决

        如果想要完全理解三级缓存的设计 必须结合AOP动态代理来理解 ,  其实研究明白后 我发现还挺好理解的 , 然后就是我们要不要之所以然,而要知所以然, 要有打破砂锅问到底的精神 , 如果大家对文章部分内容看不懂 欢迎留言告诉我 我会继续打磨修改 , 如果文章哪些观点是错误的,也欢迎您的指正

        ok 本文章主要重点说完了,后续是一下其他类型的循环依赖的测试 了解一下即可 你也可以直接跳到面试题回答模板 bye bye

项目运行:

测试一下IOC容器中的bean注入

以及代理bean的成员属性和原始bean的成员属性

报错原因: 无报错
解决方案: 无需解决
补充: 第二种循环依赖类型测试完毕
第3类.特殊AOP代理bean的@Autowired注入的循环依赖
说明:

这种类型的循环依赖,在工作中可能会触发,beanAB互相注入  并且beanA因为有陈列了@Async的方法,因此预期beanB中注入的beanA是代理beanA,IOC容器中的beanA也是代理beanA,但实际情况,这种写法会报错

bean定义:

项目启动:

报错

Error creating bean with name 'beanA': Bean with name 'beanA' has been injected into other beans [beanB] 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.

创建名称为“beanA”的bean时出错:名称为“beanA”的bean已作为循环引用的一部分注入其原始版本中的其他bean [beanB],但最终已被包装。这意味着其他bean不使用该bean的最终版本。这通常是过度渴望类型匹配的结果-例如,考虑使用'getBeanNamesForType'并关闭'allowEagerInit'标志。

报错原因:

这是因为@Aysnc注解代理的时机是在IOC容器实例化完成后,而并非可以像@Transactional注解那样提前代理,最终导致beanB中注入的beanA是原始beanA,而IOC容器中的beanA却是代理beanA,因此报错

解决方案:

在使用到beanA的地方加@Lazy注解,提前对beanA代理

解决原理:

这样陈列@Lazy注解 , 会让使用到beanA的地方对beanA提前进行代理 , 可以发现IOC容器中的beanA是内存名称为6227的代理对象,而beanB中的beanA则是名称为6240的代理对象

项目运行:

成功启动

报错原因: 
解决方案:
补充:
第4类.普通bean的@Autowired+构造函数注入的循环依赖
说明:

开发中,几乎不会这样做,也不会遇到这种情况

bean定义:

项目启动:

报错

报错原因:

ab形成了一个循环依赖,因为spring解决循环依赖主要依靠执行bean的构造函数,获取早期引用对象,但是这样的写法会导致在调用构造函数阶段就形成无限的套娃,因此报错

解决方案一(思考):

既然这个构造函数用不了,那我手动的声明一个构造函数是否可行? 如图:

答案是:不可行

从最终结果来看: 

即使spring调用的是我们声明的无参构造函数,那么最终结果就会导致beanAB中的对方是为null,这显然不合法

从spring实际处理来看:

spring会放弃我们声明的无参构造函数,因为它必须要调用那个有参的构造函数,或者说它必须要调用这个陈列了@Autowired注解的构造函数,才能够保证所有bean的成员属性注入的正常

因此项目启动依然会报错

解决方案二:

在使用到beanA的地方加@Lazy注解,用代理对象去打破循环依赖

解决原理二:

这样的话,beanB中的beanA是代理beanA对象了,自然就不构成循环依赖了

项目运行:

成功

报错原因:
解决方案:
补充:  

当我们继续对bean对象的结构进行复杂化  例如都给其声明一个@tran注解,让其都被代理

那么此时的bean注入图例是如何的?

如图:

基于上面bean结构的bean注入图:

断点验证:

首先我们直接访问IOC容器的beanAB 以及beanB中的beanA

会发现IOC的2个bean都是代理对象,这是正常的,但是beanB中的beanA却是null,这个情况其实和之前文章提到的一个情况类似,spring并不会直接为beanB这个代理对象注入有效的成员属性,而是只会对原始beanB注入有效的成员属性,因此我们继续进入到beanB1的方法内部看看

会发现此时beanB的成员属性benaA并不为空了,而是有效的,并且它是一个代理对象,这是正常的因为它被@Lazy修饰了,这导致了beanB中的beanA是代理对象

其实到这里整个bean的结构图看的差不多了,那么扩展一个问题,就是如何访问到真正的原始beanA? 

直接调用beanB中的代理beanA的func方法 , 该func方法内部的this就是原始beanA

第5类.普通多例bean的@Autowired注入的循环依赖
说明:

这个不演示

bean定义:
项目启动:
报错原因:
解决方案:
解决原理:
项目运行:
报错原因:
解决方案:
补充:
第6类.普通bean的@Autowired+seter方法注入的循环依赖
说明: 

该类型不演示,没有必要

bean定义:
项目启动:
报错原因:
解决方案:
解决原理:
项目运行:
报错原因:
解决方案:
补充:

六.面试题回答模版

面试官: "说一下spring的三级缓存?"

好的 , 关于spring的三级缓存我是研究过一些这方面源码 , 并且我也看了网上很多文章 , 也做过demo项目去研究过 , 然后关于这个问题我从2个方面回答 , 分别是 :

  • 循环依赖是什么?
  • 我对三级缓存的理解

那我首先来说 循环依赖是什么?

循环依赖的意思就是单个或者多个bean互相依赖注入的一个场景 , 在这样的情况下 , 我举个例子 ,假如现在AB 2个bean互相依赖注入 , 那么想要完成beanA的初始化必须对其填充初始化后的beanB,而想要完成beanB的初始化又必须填充初始化后的beanA, 以此造成无限getBean()方法的调用,进而引起报错.

因为循环依赖的具体类型类型是比较多种的 , 以下这些操作都会影响循环依赖的一个类型

  • bean的生命周期                          我们可以通过@Scope来声明
  • bean底层的动态AOP代理方式    我们可以通过接口+配置的方式来改变
  • bean依赖注入其他bean的方式    我们可以通过 @Lazy注解,构造函数 的方式来改变

但我们最常遇到的还是 "普通AOP代理的单例bean的循环依赖" 和  "普通单例bean的循环依赖" 

而Spring的三级缓存本质上就是为了解决前者的一个实现方案 , 而针对后者 ,其实普通的二级缓存就能解决了

而其他类型的循环依赖 ,  其实spring的三级缓存也解决不了,必须通过@Lazy注解来生成新的代理对象  或者 重构bean之间依赖架构 的做法来打破循环依赖的构成条件 , 从而解决循环依赖

上面是对循环依赖的类型的大概描述 , 接下来我回答第二方面 , 我对三级缓存的理解

但是在描述之前, 我们需要先了解一下二级缓存执行流程:

假设现在AB 2个bean互相依赖注入,a需要等待b初始化 , b需要等待a初始化 ,那么spring通过二级缓存的概念 , 解决这个循环依赖的过程如下:

1.开始对a对象的初始化,先从2个缓存都找一遍是否已经有a对象了,但第一次肯定找不到,于是反射a对象构造函数,实例化一个a对象,此时a对象未经过任何属性填充,然后将a对象放入第二级缓存

2.开始对a对象进行属性填充,发现需要注入b对象 , 那么暂停a对象的属性填充,转而去初始化b对象

3.开始进行b对象的初始化,此时这个步骤和刚刚的第一步一样,最终b对象被实例化,存入二级缓存

4.开始对b对象进行属性填充,发现需要注入a对象,那么暂停b对象创建,转而获取a对象,此时就能够在第二级缓存得到a对象的早期引用,进而完成b对象的属性填充,然后b对象删除自己的二级缓存,将自己放入一级缓存,代表b对象完成初始化

5.当b对象完成初始化之后,线程回到a对象属性填充这边,此时a对象就能从一级缓存得到b对象,并且完成a对象的属性填充,然后a对象删除自己的二级缓存,将自己放入一级缓存,代表a对象完成初始化

以上是二级缓存解决循环依赖的过程, 主要就是利用java对象引用的特性 , 通过提前暴露早期对象引用来解决循环依赖的

但是后来,  随着spring的框架的迭代 ,AOP概念被引入 , 一些bean在初始化阶段末期还要进行动态代理 , 也就是去替换IOC容器对自身的一个原始注入 , 也就是说AOP操作是在bean解决循环依赖之后才执行的操作 , 那么如果继续保持上面这种二级缓存的架构 , 并且ab2个对象都需要被动态代理 , 那么此时就会导致一个bug , 也就是最终b对象注入的a , 这个a它是一个原始a对象 , 而后续的a对象实际上会被AOP代理 , 这就导致最后beanB中的a和IOC中的a是不一样的 ,这就是一个严重bug

然后这个场景就是一个"普通AOP代理的单例bean的循环依赖"

那么解决这个问题 , 其实也比较简单 , 就是通过提前代理这些bean对象 , 让其他的bean在注入时 , 直接注入代理 , 但是如果这种操作直接强加到现有的二级缓存架构中  , 会带来一些不规范 , 首先打破了bean的创建流程, 也就是先实例化 , 再属性填充 , 再初始化, 再AOP代理 , 其次违背了AOP代理这个行为的面向对象思想 , 因为AOP代理这个操作最好是在对象初始化完成之后再做的

那么针对上面的种种情况 ,spring最终选择稍微打破一点AOP代理的规范 , 也就是采用的三级缓存这种方案 ,

首先是三级缓存的定义:

一级缓存存储bean的成熟态,可以直接使用

二级缓存存储bean的早期引用 , 可能是代理bean对象 也可能是原始bean对象

三级缓存存储能够生成二级缓存bean的bean工厂对象

然后是三级缓存的大概核心执行流程

首先刚开始初始化a对象时 依然是调用构造函数 但是spring会将生成出来的a对象包装为一个a工厂对象放入三级缓存 , 然后继续走自己的原始a对象的初始化过程

那么后续 b对象拿到a工厂对象 调用getObjet()方法 , 该方法内部会判断a对象是否需要代理 , 需要需要,则返回的是a代理对象 , 至此b对象完成初始化  , 并且b对象可以继续按照标准的bean初始化流程 完成自己的AOP代理 并且将自己AOP代理对象放入一级缓存

那么再回到原始a对象属性填充 , 他就能从一级缓存拿到b代理对象 , 进而完成自己的属性填充和初始化 , 再后面走A对象AOP代理 , spring就会做校验知道a对象已经被代理过了 , 那么就不会再执行代理

所以说三级缓存的本质就是在不大规模修改现有的bean初始化流程的前提下 又解决了这种普通aop代理的单例bean的循环依赖 , 它是一个优雅的解决方案 , 但并不是唯一的解决方案

end.


 




 


 


 

  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值