从源码层面深度剖析Spring循环依赖 | 京东云技术团队

以下举例皆针对单例模式讨论

图解参考 https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce

1、Spring 如何创建Bean?

对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。

Spring 在创建 Bean 过程中,使用到了三级缓存,即 DefaultSingletonBeanRegistry.java 中定义的:

    /** Cache of singleton objects: bean name to bean instance. */private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
​/** Cache of singleton factories: bean name to ObjectFactory. */private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
​/** Cache of early singleton objects: bean name to bean instance. */private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

以 com.gyh.general 包下的 OneBean 为例,debug springboot 启动过程,分析spring是如何创建bean的。

参考图中 spring创建bean 的过程。其中最关键的几步有:

1.getSingleton(beanName, true) 依次从一二三级缓存中查找bean对象,如果缓存中存在对象,则直接返回(early);

2.createBeanInstance(beanName, mbd, args) 选一个合适的构造函数,new实例对象(instance),此时的instance中依赖的属性还都是null,属于半成品;

3.singletonFactories.put(beanName, oneSingletonFactory) 利用上一步的instance,构建一个 singletonFactory,并将其放到三级缓存中;

4.populateBean(beanName, mbd, instanceWrapper) 填充bean:为该bean定义的属性创建对象或赋值;

5.initializeBean("one",oneInstance, mbd) 初始化bean:对bean进行初始化或其他加工,如生成代理对象(proxy);

6.getSingleton(beanName, false) 依次在一二级缓存中查找,检查是否有因循环依赖导致提前生成的对象,有的话与初始化后的对象是否一致;

2、Spring 如何解决循环依赖?

以 com.gyh.circular.threeCache 包下的 OneBean 和 TwoBean 为例 ,两个 Bean 相互依赖(即形成闭环)。

参考图中 spring解决循环依赖 的过程可知,spring利用三级缓中的 objectFactory 生成并返回一个 early 对象,提前暴露这个 early 地址,供其他对象依赖注入使用,以此解决循环依赖问题。

3、Spring 不能解决哪些循环依赖?

3.1 循环中使用了 @Async 注解

3.1.1 为什么循环中使用了 @Async 会报错?

以 com.gyh.circular.async 包下的 OneBean 和 TwoBean 为例,两个bean相互依赖,且oneBean中的方法使用了 @Async 注解,此时启动spring失败,报错信息为:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a.one': Bean with name 'a.one' has been injected into other beans [a.two] 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.

并通过debug代码,发现报错位置在 AbstractAutowireCapableBeanFactory#doCreateBean 方法内,由于 earlySingletonReference != null 且 exposedObject != bean,导致报错。

结合流程图中 spring解决循环依赖 及上述图片中可知:

1.行1中 bean 为 createBeanInstance 创建的实例(address1)

2.行2中 exposedObject 为 initializeBean 后生成的代理对象(address2)

3.行3中 earlySingletonReference 为 getEarlyBeanReference 时创建的对象【此处地址同bean(address1)】

深层原因为:先前 TwoBean 在 populateBean 时已经依赖了地址为 address1 的 earlySingletonReference 对象,而此时 OneBean 经过 initializeBean 之后,返回了地址为 address2 的新对象,导致spring不知道哪个才是最终版的bean,所以报错。

earlySingletonReference 是如何生成的,参考getSingleton(“one”, true)过程。

3.1.2 循环中使用了 @Async 一定会报错吗?

依然以 com.gyh.circular.async 包下的 OneBean 和 TwoBean 为例,两个bean相互依赖,使 TwoBean(非OneBean)中的方法使用了 @Async 注解,此时启动spring成功,并未报错。

debug代码可知:虽然TwoBean 使用了 @Async 注解,但其 earlySingletonReference = null; 故不会引起报错。

深层原因为:OneBean 先被创建,TwoBean 后创建,再整条链路中,并未在三级缓存中查找过 TwoBean 的 objectFactory 。(OneBean在创建过程中,被找过两次,即 one-> two ->one;TwoBean 的创建过程中,只找过它一次,即 two ->one。)

由此可得:@Async 造成循环依赖报错的先约条件为:

1.循环依赖中的 Bean 使用了 @Async 注解

2.且这个 Bean,比循环内其他 Bean 先创建。

3.注:一个Bean可能会同时存在于多个循环内;只要存在它是某个循环内第一个被创建的Bean,那么就会报错。

3.1.3 为什么循环中使用了 @Transactional 不会报错?

已知使用了 @Transactional 注解的 Bean,Spring 也会为其生成代理对象,但为什么这种 Bean 在循环里时不会产生报错呢?

以 com.gyh.circular.transactional 包下的 OneBean 和 TwoBean 为例,两个 Bean 相互依赖,且 OneBean 中的方法使用了 @Transactional 注解,启动Spring成功,并不会报错。

debug 代码可知,生成 OneBean 过程中,虽然 earlySingletonReference != null,但 initializeBean 之后的 exposedObject 和 原始实例的地址相同(即 initializeBean 步骤中,并未对实例生成代理),所以不会产生报错。

3.1.4 为什么同样是代理会产生两种不同的现象?

同样是生成代理对象,同样是参与到循环依赖中,会产生不同现象的原因是:当他们处在循环依赖中时,生成代理的节点不同:

1.@Transactional 在 getEarlyBeanReference 时生成代理,提前暴露出代理之后的地址(即最终地址);

2.@Async 在 initializeBean 时生成代理,导致提前暴露出去的地址不是最终地址,造成报错。

为什么 @Async 不能在 getEarlyBeanReference 时生成代理呢?对比下两者执行的代码过程发现:

两者都是在 AbstractAutoProxyCreator#getEarlyBeanReference 的方法对原始实例对象进行包装,如下图

使用 @Transactional 的Bean 在 create proxy 时,获取到一个advice ,随即生成了代理对象 proxy.

而使用 @Async 的Bean 在 create proxy 时,没有获取到 advice,不能被代理.

3.1.5 为什么@Async 在 getEarlyBeanReference 时不能返回一个 advice?

在 AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法内,其主要做的事情有:

1.找到当前 spring 容器中所有的 Advisor

2.返回适配当前 bean 的所有 Advisor

第一步返回的 Advisor 有 BeanFactoryCacheOperationSourceAdvisor 和 BeanFactoryTransactionAttributeSourceAdvisor,并无处理 Async 相关的 Advisor.

刨根问底,追查为什么第一步不会返回处理 Async 相关的 Advisor?

已知使用 @Async @Transactional @Cacheable 需要提前进行开启,即提前标注 @EnableAsync、@EnableTransactionManagement、@EnableCaching 。

以 @EnableTransactionManagement、@EnableCaching 为例,在其注解定义中,引入了Selector类,Selector中又引入了Configuration 类,在 Configuration 类中,创建了对应 Advisor 并放到了 spring容器中,所以第一步才能得到这两个 Advisor.

而 @EnableAsync的定义中引入的 Configuration 类,创建的是 AsyncAnnotationBeanPostProcessor 并非一个 Advisor,所以第一步不会得到它,所以 @Async 的 bean 不会在这一步被代理。

3.2 构造函数引起的循环依赖

以 com.gyh.circular.constructor 包下的 OneBean 和 TwoBean 为例,两个类的构造函数中各自依赖对方,启动spring,报错:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'c.one': Requested bean is currently in creation: Is there an unresolvable circular reference?

debug 代码可知,两个bean在根据构造函数 new instance 时,就已经陷入的死循环,无法提前暴露可用的地址,所以只能报错。

4、如何解决以上循环依赖报错?

1.不用 @Async,将需要异步操作的方法,放到线程池中执行。(推荐)

2.提出 @Async 标注的方法。(推荐)

3.将使用 @Async 的方法提出到单独的类中,该类只做异步处理,不做其他业务依赖,即避免形成循环依赖,从而解决报错问题。参考 com.gyh.circular.async.extract 包。

4.尽量不使用构造函数依赖对象。(推荐)

5.破坏循环(不推荐)即不形成闭环,在开发之前,规划好对象依赖,方法调用链,尽量做到不使用循环依赖。(较难,随着迭代开发不断变化,很可能产生循环)

6.破坏创建顺序(不推荐)

7.由于使用 @Async 注解的所在类,比循环依赖内其他类先创建时才会报错,那么想办法使该类不先于其他类先创建,也可解决该问题,如:@DependsOn、 @Lazy

作者:京东科技 郭艳红

来源:京东云开发者社区

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/32884.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue在页面输出JSON对象,测试接口可复制使用

效果图&#xff1a; 数据处理前&#xff1a; 数据处理后&#xff1a; 代码实现&#xff1a; HTML: <el-table height"600" :data"tableData" border style"width: 100%" tooltip-effect"dark" size"mini"><el-…

前后端分离------后端创建笔记(上)

本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论&#xff0c;如有侵权请联系 源码&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…

如何使用 ESP-01S 模块

如何使用 ESP-01S 模块 原始PDF文档 参考&#xff1a; 将 ESP-01 用作 WiFi shield的更好方法 (e-tinkers.com) How do I use ESP8266 ESP-01S WiFi Module with ESP-01S Adapter - Using Arduino / Programming Questions - Arduino Forum ESP-01S WiFi 模块 – 配置布线 -…

boost写日志

单个写日志 #include <boost/log/core.hpp> #include <boost/log/trivial.hpp> #include <boost/log/expressions.hpp> #include <boost/log/utility/setup/file.hpp> #include <boost/log/utility/setup/common_attributes.hpp> #include <…

1037:计算2的幂

【题目描述】 给定非负整数n&#xff0c;求2^n的值&#xff0c;即2的n次方。 【输入】 一个整数n。0<n<31。 【输出】 一个整数&#xff0c;即2的n次方。 【输入样例】 3 【输出样例】 8 【参考答案】&#xff1a; #include<bits/stdc.h> using namespa…

服务管理和计划任务

文章目录 服务管理计划任务 服务管理 systemctl 命令字 服务名 //配置服务与systemctl有关的命令字&#xff1a; 计划任务 一次性计划 at 时间 at now 5 min //当前时间五分钟后执行 at -l //列出计划任务 atrm 任务号 //删除计划任务执行完命令后Ctrld生效 周期性计…

Oracle数据迁移

问题描述&#xff1a; oracle数据库的所有表结构、数据、索引等需要需从测试库迁移到正式库。 解决步骤&#xff1a; oracle数据库迁移&#xff0c;主要通过expdp从测试库所在的源服务器将指定的数据表或数据源导出为一个或多个数据文件&#xff08;.dmp文件&#xff09;&…

黑马项目一完结后阶段面试45题 JavaSE基础部分20题(一)

一、Java数据类型 基本数据类型——四类八种 整数型 byte short int long 浮点型 float double 字符型 char 布尔型 boolean 引用数据类型 String字符串 类&#xff08;对象&#xff09; 接口类型 数组类型 枚举类型 二、面向对象的三大特性 1.封装 把同一类事物…

添加SQLCipher 到项目中

文章目录 一、克隆下载SQLCipher二、手动导入1. 生成sqlite3.c2. 在项目中添加命令3. 添加 Security.framework 三、CocoaPods导入 SQLCipher官方地址 一、克隆下载SQLCipher $ cd ~/Documents/code $ git clone https://github.com/sqlcipher/sqlcipher.git二、手动导入 1.…

二叉树小结

二叉树 树的遍历(如何遍历&#xff0c;如何利用特性问题) 前序遍历&#xff08;中前后&#xff09; 递归 class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> res new ArrayList<>();inorder(root, res);return res…

电商系统架构设计系列(八):订单数据越来越多,数据库越来越慢该怎么办?

上篇文章中&#xff0c;我给你留了一个思考题&#xff1a;订单数据越来越多&#xff0c;数据库越来越慢该怎么办&#xff1f; 今天这篇文章&#xff0c;我们来聊一下如何应对数据的持续增长&#xff0c;特别是像订单数据这种会随着时间一直累积的数据。 引言 为什么数据量越大…

shell从入门到精通(19)特殊变量

​ ​参考:Special Parameters (Bash Reference Manual) (gnu.org) 文章目录 $0$*$@$?变量名称作用$0当前脚本的文件名,不一定是全路径,取决于执行时传入的脚本路径$n叫做位置参数,n≥1 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第…

成考本科和国开本科哪个认可度高些 应该选择哪个

成考本科和国开本科都属于成人教育&#xff0c;两种学历都能为您提供知识、技能和学术背景&#xff0c;为您的职业道路打下坚实的基础。选择哪种学历取决于您的学习兴趣、时间安排和职业目标。 成考本科和国开本科谁认可度高 成人高等教育考试(成考)本科和国家开放大学(国开)本…

jenkins容器内CI/CD 项目失败问题

问题&#xff1a; 在jenkins 的docker容器内CI/CD制作vue项目镜像失败 1、docker权限问题 permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/build?buildargs%…

【idea】点击idea启动没反应

RT 点击idea启动的时候没反应&#xff0c;接着百度报错&#xff0c;基本跟他们的也不一样。 首先我是做版本升级。其次&#xff0c;我之前是破解的。如果你也是跟我一样的话&#xff0c;那问题可能就处在破解上了 解决方式 首先&#xff0c;是跟大部分解决思路一样。先找到项…

分清性能测试,负载测试,压力测试这三个的区别

做测试一年多来&#xff0c;虽然平时的工作都能很好的完成&#xff0c;但最近突然发现自己在关于测试的整体知识体系上面的了解很是欠缺&#xff0c;所以&#xff0c;在工作之余也做了一些测试方面的知识的补充。不足之处&#xff0c;还请大家多多交流&#xff0c;互相学习。 …

C++笔记之单例模式

C笔记之单例模式 参考笔记&#xff1a;C笔记之call_once和once_flag code review 文章目录 C笔记之单例模式1.返回实例引用2.返回实例指针3.单例和智能指针share_ptr结合4.单例和std::call_once结合5.单例和std::call_once、unique_ptr结合 1.返回实例引用 代码 #include <…

ubuntu 如何命令行打开系统设置(Wifi,网络,应用程序...)

关于GNOME GNOME 是一个自由、开放源代码的桌面环境&#xff0c;它运行在 Linux 和其他类 UNIX 操作系统上。它是 GNU 项目的一部分&#xff0c;旨在为 Linux 操作系统提供一个现代化、易于使用的用户界面。 GNOME 桌面环境包括许多应用程序&#xff0c;例如文件管理器、文本编…

Java“牵手拼多多商品详情数据采集方法,拼多多API接口申请指南

拼多多详情接口 API 是开放平台提供的一种 API 接口&#xff0c;它可以帮助开发者获取商品的详细信息&#xff0c;包括商品的标题、描述、图片等信息。在电商平台的开发中&#xff0c;详情接口API是非常常用的 API&#xff0c;因此本文将详细介绍详情接口 API 的使用。 一、拼…

前端-NVM,Node.js版本管理

NVM&#xff08;Node Version Manager&#xff09;是一个用于管理Node.js版本的工具&#xff0c;主要用于前端开发中。它允许开发者同时安装和切换不同版本的Node.js&#xff0c;以满足不同项目对Node.js版本的需求。 使用NVM可以带来以下几个好处&#xff1a; 多版本管理&…