什么是循环依赖?

先定义两个类 Apple、Orange,如下所示:

@Component
public class Apple{@Autowiredprivate Orange orange;
}@Component
public class Orange {@Autowiredprivate Apple apple;
}

像这种在 Apple 里面有一个属性 Orange、Orange 中有一个属性 Apple,你中有我,我中有你,这样可以称之为循环依赖。循环依赖问题不止在 Spring 中有,在 Mybatis 中也有,解决思想基本一样,都需要借助额外的缓存进行实现。

Spring 对于这种属性注入的循环依赖是支持的,不会有任何问题,今天这里探讨一下 Spring 中构造方法的循环依赖问题,Spring 默认是不支持的,但是也提供了方法解决。

构造方法循环依赖

同样把上面 Apple、Orange 两个类改造下,如下所示:

@Component
public class Apple{public Apple(Orange orange) {System.out.println("=====> 调用 Apple 构造方法");}
}@Component
public class Orange {public Orange(Apple apple) {System.out.println("======>调用 Orange 构造方法");}
}

测试类如下:


public class TestCircleMain {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(CircleConfig.class);Orange orange = context.getBean( Orange.class);Apple bean = context.getBean(Apple.class);}}

发现直接就抛出了循环依赖异常如下:

很显然构造方法的循环依赖,Spring 是不太支持的,但是我非要这样使用,怎么解决呢?

加 @Lazy 注解解决构造方法循环依赖

具体怎么解决构造方法循环依赖问题呢?可以通过加 @Lazy 注解,但是也需要注意一些细节(后面我们会分析到),这里先看加 @Lazy 注解之后能够解决构造方法循环依赖的案例,如下所示:

@Component
public class Apple{@Lazypublic Apple(Orange orange) {System.out.println("=====> 调用 Apple 构造方法");}
}@Component
public class Orange {public Orange(Apple apple) {System.out.println("======>调用 Orange 构造方法");}
}

或者加载参数上也行,都表示一个意思,就是后面会临时创建出 Orange 的代理对象

@Component
public class Apple{public Apple(@Lazy Orange orange) {System.out.println("=====> 调用 Apple 构造方法");}
}@Component
public class Orange {public Orange(Apple apple) {System.out.println("======>调用 Orange 构造方法");}
}

加上之后测试结果正常输出,如下:

=====> 调用 Apple 构造方法
======>调用 Orange 构造方法

那么为什么加上 @Lazy 注解就能够解决这样一个问题呢?继续往下分析,这里我们主要看加上这个 @Lazy 注解的执行流程是什么样的?

@Lazy 执行流程源码分析

首先第一次过来的是 Apple 类,先从缓存中查询是否有实例化的对象,源码如下:

第一次过来很显然是没有的,然后就要开始去创建实例,但是创建实例 Spring 会做一个标识,避免重复创建实例,这个标识标识这个 Apple 类正在创建中,当创建成功之后就会删除,此时创建好的实例就会存在缓存中。

记录标识源码如下:注意如果标识 singletonsCurrentlyInCreation 容器中已经存在,那么会直接添加失败,抛出 BeanCurrentlyInCreationException 异常


异常信息也是大家非常熟悉的循环依赖问题,源码如下:

接着要开始创建实例,如下所示:

会选择出一个合适的构造方法进行实例化,由于我们只有且仅有一个构造方法,所以肯定就用这个唯一的构造方法了,然后就开始进入 autowireConstructor() 属性注入环节

拿到构造方法中所有的参数,对每个参数一一遍历进行赋值操作,那么就要格外关注这个方法是怎么做的了

进入 createArgumentArray() 方法内部逻辑,源码如下(核心部分,无关代码省略):

很显然这里是采用 for 循环对构造方法中参数一一赋值,所以构造方法中如果参数过多,性能也会降低许多,这个得注意了

看到 resolveDependency() 方法一定要有一种意识,就是极大可能要出发 getBean() 操作了,除了代理不会触发,但是也不一定(后面会讲到)

然后下面这一段代码是加了 @Lazy 注解的关键处理逻辑了(这段逻辑非常非常重要):

1、就是先判断构造方法中(构造方法上,或者参数上)是否标注了 @Lazy 注解,如果标注了就会创建代理对象,不会立即触发 getBean() 操作
2、反之,就是走正常的逻辑,直接调用 getBean() ,但是这样就会直接报异常了,因为 Spring 是不支持构造方法的循环依赖的(还没有来得及把半 Apple 类的半成品放到三级缓存),只有加了 @Lazy 注解临时通过代理方法可以解决构造方法循环依赖

然后进入 getLazyResolutionProxyIfNecessary() 方法看是怎么判断,和要怎么创建代理对象的,源码如下:

可以清楚的看到 @Lazy 为什么可以标注在构造方法上和构造方法的入参上面两种方式,可以从下面这段源码中知道答案,如下所示:

找到了 @Lazy 注解就会通过 buildLazyResolutionProxy() 方法去创建这个入参的代理对象,如下所示:

代理对象就是对原始目标类的一种增强,注意当使用代理对象调用它的方法时会回调到 getTarget() 方法,这个 getTarget() 方法中调用了 doResolveDependency() 方法,这个方法会触发调用 getBean() 流程实例化 bean,要格外注意,后面会演示如何调用到这个方法的。

代理对象已经创建好了,现在准备通过构造方法反射调用实例化 Apple 类即可,源码如下:

其中 argsWithDefaultValues 值是 Orange 的一个代理对象,实例化好之后相当于 Apple 已经创建好了,然后放入到三级缓存中,如下所示:

然后就是删除 singletonsCurrentlyInCreation 标识容器中的标识位(因为已经实例化完成了,所以标识位可以抹除),如下所示:

最后在把 Apple 实例化好的 bean 从三级缓存中删除,然后移动到一级缓存中,也就是我们经常所说的单例缓冲池中,如下所示:


至此,Apple 类实例化 bean 就已经在 Spring 的单例缓冲池中存在了,其他地方如果想要使用直接从这个单例缓冲池中取值即可。

那么当 Orange 类过来实例化的时候,也是先从容器中查找是否有实例化 bean 存在,源码如下:

然后打标记,源码如下:


异常信息也是大家非常熟悉的循环依赖问题,源码如下:

接着要开始创建实例,如下所示:

会选择出一个合适的构造方法进行实例化,由于我们只有且仅有一个构造方法,所以肯定就用这个唯一的构造方法了,然后就开始进入 autowireConstructor() 属性注入环节

拿到构造方法中所有的参数,对每个参数一一遍历进行赋值操作,那么就要格外关注这个方法是怎么做的了

然后进入代码核心逻辑,此时因为在 Orange 的构造方法中是没有标注 @Lazy 注解的,所以这里不会进入创建代理的逻辑,而知直接进入 doResolveDependency() 逻辑,前面已经提到很多遍历,这个方法很重要,会触发到 getBean() 流程。

那么 Orange 构造方法中的入参为 Apple,Apple 在第一遍的时候就已经在单例缓冲池中存在了,所以 Apple 在执行 getBean() 流程的时候,直接就会从一级缓存中获取到 Apple 实例化好的对象,赋值给 Orange 构造方法中的 Apple
变量。

然后表示就是 Orange 通过反射调用构造方法实例化 Orange 实例

然后后面的流程 Orange 也是要放入到三级缓存中,然后删除标识位,最后将 Orange 实例从三级缓存中删除,移动到一级缓存(单例缓存池)中。

至此 Apple、Orange 两个构造方法的循环依赖就分析完成了,下面是稍微改动一点,继续分析。

使用 @Lazy 注解注意事项(特别小心)

将 Apple、Orange 类稍微变动一下,如下所示:

@Component
public class Apple{public Apple(@Lazy Orange orange) {System.out.println("=====> 调用 Apple 构造方法");System.out.println("======>orange="+orange);}
}@Component
public class Orange {public Orange(Apple apple) {System.out.println("======>调用 Orange 构造方法");}
}

经过上的分析,Apple 构造方法中 @Lazy 注解修饰的 Orange,会创建一个代理对象来规避入参 orange 调用 getBean() 流程,从而解决循环依赖问题,现在我们在 Apple 构造方法中,直接把 orange 打印出来。

经过测试直接报错,错误如下:

发现还是发生了循环依赖问题,下面具体分析下是为什么呢?前面分析过的下面都会直接通过简短描述直接带过

1、Apple 类首先会去缓存中查找是否已经实例化 bean,第一次很显然没有

2、开始记录标记位

3、调用 createBeanInstance() 方法实例化对象

4、给 Apple 类构造方法的入参进行属性赋值,会创建代理类,如下所示:

注意这里面的 getTarget() 方法,下面会回调到这里,现在代码继续往后走,代理类创建好之后就要开始通过反射调用构造方法创建实例了,源码如下:

注意此时的 argsWithDefaultValues 是 Orange 代理对象,当我们通过反射调用 Apple 的构造方法时,立即回调到 Apple 的构造方法中的逻辑,如下所示:

@Component
public class Apple{public Apple(@Lazy Orange orange) {System.out.println("=====> 调用 Apple 构造方法");System.out.println("======>orange="+orange);}
}

先执行输出语句,打印出"=====> 调用 Apple 构造方法",然后再打印下一句语句时,请注意,orange 是一个代理对象,在 JVM 执行这条输出语句的时候,其实默认调用了 toString() 方法,你要知道在创建代理对象的时候,并没有限定哪个方法增强,而是对整个 Orange 中的方法增强了,所以在你输出 Orange 的时候就会触发代理对象的对 toString() 方法的增强,所以会回调到代理对象中的 intercept() 方法,然后再 intercept() 方法中有调用了 getTarget() 方法,注意哦在创建代理对象的时候,我特意说明要注意 getTarget() 方法,因为现在就要被回调到了,恰好 getTarget() 方法中又会触发 getBean() 流程,所以最终又导致循环依赖问题的产生。

对于 Apple 类构造方法中的入参 Orange, Spring 是通过 cglib 进行代理对象创建的,具体看 CglibAopProxy 类就知道为什么在执行 toString() 方法最终会回调到 getTarget() 方法,这里就截取一段核心代码,如下:
 

那么怎么解决这个问题呢?

1、在 Apple 类构造方法中不要调用任何代理对象的方法,比如这样使用,如下所示:

@Component
public class Apple{private Orange orange;public Apple(@Lazy Orange orange) {System.out.println("=====> 调用 Apple 构造方法");this.orange = orange;}public void sop() {System.out.println("this.orange = " + this.orange);}
}@Component
public class Orange {private Apple apple;public Orange(Apple apple) {System.out.println("======>调用 Orange 构造方法");}
}

我只是在 Apple 构造方法中使用了一下 Orange 代理对象,并没有调用任何 API,所以不会触发代理对象执行增强逻辑。

2、继续在 Apple 构造方法中触发代理对象回调(调用 toString() 等方法),此时会出现循环依赖问题,就是因为方法 toString() 的增强逻辑触发了 Orange 的 getBean() 操作,然后 Orange 实例化时,又触发了 Orange 构造方法中的入参 Apple 类的实例化,此时你要知道 Apple 类还没有实例化完成呢,缓存中压根也还没有,Apple 类现还停留在System.out.println("======>orange="+orange); 输出语句呢

所以说到这里了,我们也可以在 Orange 中加上 @Lazy 注解,如下所示:

@Component
public class Apple{public Apple(@Lazy Orange orange) {System.out.println("=====> 调用 Apple 构造方法");System.out.println("this.orange = " + this.orange);}public void sop() {System.out.println("this.orange = " + this.orange);}
}@Component
public class Orange {private Apple apple;public Orange(@Lazy Apple apple) {System.out.println("======>调用 Orange 构造方法");}
}

当给 Orange 类构造方法中入参 Apple 赋值先给定一个代理对象,避免 Apple 类触发 getBean() 操作,这样 Orange 构造方法的入参就相当于赋上值,那么 Orange 类就完成了实例化,代码回调上层调用处,就是 Apple 类构造方法中的输出语句 System.out.println("======>orange="+orange); 这条输出语句执行完,相当于 Apple 类构造方法也实例化完成,从而没有发生循环依赖问题。

但是如果在将 Apple、Orange 类变动一下,如下所示:

@Component
public class Apple{public Apple(@Lazy Orange orange) {System.out.println("=====> 调用 Apple 构造方法");System.out.println("this.orange = " + this.orange);}
}@Component
public class Orange {private Apple apple;public Orange(@Lazy Apple apple) {System.out.println("======>调用 Orange 构造方法");System.out.println("this.orange = " + this.orange);}
}

这样是绝对没办法解决了,因为相当于 @Lazy 注解没有加上一样,每个构造方法中都会立即触发 getBean() 操作,此时以为缓存中根本还没来得及放入实例化 bean。

以上只是个人对 @Lazy 的理解,仅供参考。

总结

在构造方法循环依赖问题中,通过 @Lazy 注解,只是临时创建一个代理对象来为属性赋值,避免触发二次 getBean() 调用。

并且注意代理对象和被 @Lazy 修饰的类的实例并不是同一个,完全是两个对象,可以输出 hashCode() 编码即可查看,不能使用 toString() 来做验证,因为代理对象会回调到切面逻辑,然后触发 getBean() 实例化 @Lazy 修饰的类,然后最终通过 toString() 方法输出的结果都是一样的 com.gwm.circle.Banana@5149d738,然后你就会误认为代理对象和被 @Lazy 修饰类的真正对象是相同的,其实并不相同,代理对象是代理对象,和真正实例完全是两个对象!

Spring 通过 @Lazy 注解解决构造方法循环依赖问题相关推荐

  1. Spring IOC 容器源码分析 - 循环依赖的解决办法

    1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的.在本篇文章中,我会首先向大家介绍一下什么是循环依赖.然后,进入源码分析阶段.为了更好的说明 Spring 解决循环依赖的办法,我 ...

  2. 不懂就问,Spring 是如何判定原型循环依赖和构造方法循环依赖的?

    作者:青石路 cnblogs.com/youzhibing/p/14514823.html 写在前面 Spring 中常见的循环依赖有 3 种:单例 setter 循环依赖.单例构造方法循环依赖.原型 ...

  3. 从一个Spring动态代理Bug聊到循环依赖

    文章目录 Bug复现 结论 @PostConstruct的在Bean的生命周期的哪一步 一般代理类的生成时机在生命周期的哪一步 解决办法 两个思路 1.不生成代理类 2.在生成代理类之后再进行数据的初 ...

  4. 解决springboot 循环依赖

    错误提示 Relying upon circular references is discouraged and they are prohibited by default. Update your ...

  5. Spring Boot 2.6 正式发布:循环依赖默认禁止、增加SameSite属性...

    昨天,Spring官方正式发布了Spring Boot今年最后一个特性版本:2.6.0 同时,也宣布了2.4.x版本的终结. 那么这个新版本又带来了哪些新特性呢?下面就一起跟着DD来看看吧! 重要特性 ...

  6. 【Spring】@Lazy注解

    今天主要从以下几方面来介绍一下@Lazy注解 @Lazy注解是什么 @Lazy注解怎么使用 1.@Lazy注解是什么 @Lazy注解用于标识bean是否需要延迟加载,源码如下: @Target({El ...

  7. 详解Spring的循环依赖

    本篇博客为学习哔哩哔哩中的黑马程序员的spring复习视频的学习笔记,仅供参考. 目录 代理的创建时机 aspectj与advisor的关系 自动后置代理处理器 循环依赖 set注入导致的循环依赖以及 ...

  8. 什么是循环依赖?Spring如何解决循环依赖?

    1. Spring创建代理原理 1.1 ProxyFactory类 第一步:创建一个基础SpringBoot项目 <!--web--> <dependency><grou ...

  9. Spring系列五:Spring怎么解决循环依赖

    15.说说循环依赖? 什么是循环依赖? Spring循环依赖 Spring 循环依赖:简单说就是自己依赖自己,或者和别的Bean相互依赖. 鸡和蛋 只有单例的Bean才存在循环依赖的情况,原型(Pro ...

最新文章

  1. 别再SOTA了,那叫“微调”!Science发文炮轰论文灌水
  2. 计算机考试前的心情作文,期中考试前的心情作文
  3. SAP UI5 应用开发教程之五十五 - 如何将本地 SAP UI5 应用通过 Node.js Express 部署到公网上试读版
  4. opencv 分割边界_电影观众:场景边界分割
  5. 解决Android studio 加载不出网络图片的步骤
  6. 7PYX 网站代码下载
  7. java 浅堆 深堆_JVM中的一个小知识点:深堆和浅堆的概念
  8. Ubuntu9.04更新源
  9. 2684 亿背后的虚拟化技术:双 11 All on 神龙 | 问底中国 IT 技术演进
  10. seo该如何防止网站被挂***?!
  11. 黑鲨官网装机大师工具如何制作u盘启动盘,u盘启动盘制作方法
  12. FFmpeg 图片转TS
  13. 近红外光谱建模之区间偏最小二乘法python实现(ipls算法)
  14. 大数据驱动教育变革,产教融合呈现新高度——数据科学与大数据技术教育分论坛顺利召开...
  15. 工程测量内业中提取横断面线折点坐标数据并写入文件
  16. SOLARIS SYSTEM COMMAND(个人整理笔记)
  17. 数据结构-指针和结构体
  18. 人民搜索2013年招聘的三道算法题 西安站
  19. Serverless Job—— 传统任务新变革
  20. 第一次在OJ上写个a+b简直弱爆了。。。。

热门文章

  1. A03-arcgis无法统计地块面积常见问题及解决方案
  2. 靶机渗透练习84-The Planets:Earth
  3. 计算机专业大专考试题,计算机大专考试试题1.doc
  4. .csd文件怎么读?--CMU_MOSI_Opinion_Labels.csd
  5. 常用数据集预处理(dota)
  6. js与html和css的关系
  7. 转 http://wenku.baidu.com/view/8719b5dad15abe23482f4d9e.html
  8. java poi excel导出2003版改成2007版本的时候报错
  9. 在sql 2000中实现Oracle 中 rownum的功能
  10. 零基础学习C语言必读书籍