四探循环依赖 → 当循环依赖遇上 BeanPostProcessor,爱情可能就产生了!
开心一刻
那天知道她结婚了,我整整一个晚上没睡觉,开了三百公里的车来到她家楼下,缓缓的抽了一支烟......
天渐渐凉了,响起了鞭炮声,迎亲车队到了,那天披着婚纱的她很美,真的很美!
我跟着迎亲车队开了几公里的时候,收到了她的信息:别送了,别送了,你的手扶拖拉机太响了 ......
前情回顾
楼主一而再,再而三的折腾循环依赖,你们不烦,楼主自己都烦了,如果你们实在是受不了,那就...
言归正传,虽然确实有点像懒婆娘的裹脚布,又臭又长,但确实还是有点东西的,只要大家坚持看完,肯定会有收获的!
我们先回顾下前三探
一探
Spring 的循环依赖,源码详细分析 → 真的非要三级缓存吗 中讲到了循环依赖问题
Spring 通过三级缓存解决 setter 循环依赖
一级缓存 singletonObjects 存的是对外暴露的对象,也就是我们应用真正用到的对象
二级缓存 earlySingletonObjects 存的是半成品对象或半成品对象的代理对象,用于处理循环依赖的对象创建问题
三级缓存 singletonFactories 存的是创建对象的工厂方法,用于处理存在 AOP + 循环依赖的对象创建问题
着重分析了是否一定需要三级缓存来解决循环依赖问题
二探
Spring 不能处理构造方法的循环依赖,也不能处理原型循环依赖
再探循环依赖 → Spring 是如何判定原型循环依赖和构造方法循环依赖的,从源码的角度分析了 Spring 是如何鉴别构造方法循环依赖、原型循环依赖的
Set<String> singletonsCurrentlyInCreation 会记录当前正在创建中的实例名称, Spring 创建实例对象之前,会判断 singletonsCurrentlyInCreation 中是否存在该实例的名称,如果存在则表示产生构造方法循环依赖了
ThreadLocal<Object> prototypesCurrentlyInCreation 会记录当前线程正在创建中的原型实例名称, Spring 创建原型实例对象之前,会判断 prototypesCurrentlyInCreation 中是否存在该实例的名称,如果存在则表示产生原型循环依赖了
三探
三探循环依赖 → 记一次线上偶现的循环依赖问题,从源码的角度分析了这次偶现问题可能出现的原因
BeanDefinition 的扫描顺序:以启动类为起点,扫描启动类同级目录下的所有文件夹,按文件夹名升序顺序进行扫描,会递归扫描每个文件夹,文件扫描也是按文件名升序顺序进行
BeanDefinition 覆盖, @Configuration + @Bean 修饰的 BeanDefinition 会覆盖 @Component 修饰的 BeanDefinition , BeanDefinition 的覆盖并不影响 BeanDefinition 的扫描
Bean 的实例化顺序,理论上来讲,先被扫描到的就先被实例化,但实例化过程中的属性填充会打乱这个顺序,会将被依赖的对象提前实例化
一通分析下来,虽说没能找到问题的真正原因,但至少知道了如何去规避这个问题,如何正确的书写规范的代码
问题复现
经过前面三探,楼主以为对 Spring 的循环依赖已经拿捏的死死的了,然而当他出现后,楼主才发现,不是她离不开我,而是我离不开她了
我们来看看循环依赖和 BeanPostProcessor 是如何产生爱情的火花的
SpringBoot 版本 2.0.3.RELEASE ,示例代码地址:spring-circular-beanpostprocessor
我们只需要关注三个类
依赖很简单, ServiceAImpl 依赖 ServiceBImpl , ServiceBImpl 也依赖 ServiceAImpl ,这种循环依赖,楼主自认为拿捏的死死的
直到 BeanPostProcessor 的出现,循环依赖决定不再迁就,她俩的爱情就产生了
她俩的爱情信息:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'serviceAImpl': Bean with name 'serviceAImpl' has been injected into other beans [serviceBImpl] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
此刻,楼主才明白,小丑竟是我自己!
问题分析
其实她俩的爱情信息已经提示的很明显了,楼主再忍痛翻译一下: serviceAImpl 作为循环依赖的一部分注入到了 serviceBImpl 后,又被包装了,这就意味着 serviceBImpl 引用的不是最终版本的 serviceAImpl
关于 BeanPostProcessor ,楼主不想过多介绍,大家可以查看:Spring拓展接口之BeanPostProcessor,我们来看看它的底层实现
从错误堆栈信息,我们可以追踪到 Spring 报错的代码
因为 ServiceAImpl 比 ServiceBImpl 先被扫描,所以 serviceAImpl 先被实例化,实例化过程如下
此时一切都正常,问题就出在 serviceAImpl 填充属性serviceBImpl 完成之后,我们来 debug 下
从 debug 结果可以看到, ServiceBImpl 的实例对象 ServiceBImpl@5171 中注入的 ServiceAImpl 对象是 ServiceAImpl@5017
而经过 initializeBean(beanName, exposedObject, mbd); 后, Spring 暴露出来的 ServiceAImpl 的最终对象是 $Proxy53@5212
这就导致 ServiceBImpl@5171 中注入的 ServiceAImpl@5017 并不是最终版本的 ServiceAImpl ,她们的爱情就这么产生了
问题处理
面对这样的问题,我们可以怎么处理了
@Lazy
通过 @Lazy 延迟注入,在真正使用到的时候才进行注入
在任意一个属性上加 @Lazy 即可,例如
或者
或者两个都加上 @Lazy
SmartInstantiationAwareBeanPostProcessor
弃用 BeanPostProcessor ,改用 SmartInstantiationAwareBeanPostProcessor
重写的方法是: getEarlyBeanReference ,而非 postProcessAfterInitialization 方法,提前暴露代理对象
也就是说在 ServiceAImpl 对象填充属性(populateBean(beanName, mbd, instanceWrapper))之前,就将代理对象提前暴露到第三级缓存中
后续给 ServiceBImpl 对象填充 serviceAImpl 属性时,就用第三级缓存中的 ServiceAImpl 代理对象
剔除循环依赖
循环依赖本就不合理,项目中应尽量避免
至于如何剔除,无法一概而论,需要大家自己去琢磨了
总结
循环依赖
虽说 Spring 通过三级缓存解决了 setter 方式的循环依赖,但这不能成为我们有恃无恐的理由
循环依赖本就不合理,尽量去规避
真实项目问题
相信很多小伙伴会有这样的疑问:楼主,你是怎么就让 循环依赖 遇上 BeanPostProcessor ?
因为已有代码的不规范,导致很多地方都产生了循环依赖,而最近又引入 Shareding-JDBC 做分库,而 Shareding-JDBC 又通过 BeanPostProcessor 来生成代理对象
就这样,她俩就相遇了
四探循环依赖 → 当循环依赖遇上 BeanPostProcessor,爱情可能就产生了!的更多相关文章
- BFS (1)算法模板 看是否需要分层 (2)拓扑排序——检测编译时的循环依赖 制定有依赖关系的任务的执行顺序 djkstra无非是将bfs模板中的deque修改为heapq
BFS模板,记住这5个: (1)针对树的BFS 1.1 无需分层遍历 from collections import deque def levelOrderTree(root): if not ro ...
- 四、Oracle loop循环、while循环、for循环、if选择和case选择、更改读取数据、游标、触发器、存储过程
数据库的设计(DataBase Design): 针对于用户特定的需求,然后我们创建出来一个最使用而且性能高的数据库! 数据库设计的步骤: 01.需求分析 02.概念结构设计 03.逻辑结构设计 04 ...
- day4 四、流程控制之if判断、while循环、for循环
一.if判断 1.语法一: if 条件: 条件成立时执行的子代码块 代码1 代码2 代码3 示例: sex='female' age= is_beautiful=True and age < a ...
- 浅谈集合框架四——集合扩展:集合循环输出方式及list输出方式的效率对比
最近刚学完集合框架,想把自己的一些学习笔记与想法整理一下,所以本篇博客或许会有一些内容写的不严谨或者不正确,还请大神指出.初学者对于本篇博客只建议作为参考,欢迎留言共同学习. 之前有介绍集合框架的体系 ...
- Python基础学习参考(四):条件与循环
在实际的开发中,想要实现某些功能或者需求,里面必然涉及到一些逻辑,复杂的也好简单也好,那么,通过python语法如何实现呢?这就涉及到了条件与循环.很显然绝大多数的语言都有条件和循环的语法,pytho ...
- Spring框架系列(四)--IOC控制反转和DI依赖注入
背景: 如果对象的引用或者依赖关系的管理由具体对象完成,代码的耦合性就会很高,代码测试也变得困难.而IOC可以很好的解决这个问题,把这 些依赖关系交给框架或者IOC容器进行管理,简化了开发. IOC是 ...
- C# 循环语句 for循环
循环:反复执行某段代码. 循环四要素:初始条件,循环条件,循环体,状态改变.for(初始条件;循环条件;状态改变){ 循环体} 给出初始条件,先判断是否满足循环条件,如果不满足条件则跳过for语句,如 ...
- for循环、for循环嵌套
循环:反复执行某段代码. 循环四要素:初始条件,循环条件,循环体,状态改变. 循环的最后一句:循环条件不再满足. 1.找出100以内与7有关的数并打印:(1).从1找到100(2).找出与7有关的数 ...
- WPF 依赖属性与依赖对象
在介绍依赖属性之前,我先介绍下属性的历史 属性的历史: 早期C++的类中,只有字段及方法,暴露数据靠的是方法, 但是字段直接暴露会不安全,所以才用方法来暴露,在设置的时候加些约束,在MFC中 ...
随机推荐
- MySQL索引失效的常见场景
当然请记住,explain是一个好习惯! MySQL索引失效的常见场景 在验证下面的场景时,请准备足够多的数据量,因为数据量少时,MySQL的优化器有时会判定全表扫描无伤大雅,就不会命中索引了. 1. ...
- 【Java】final
final final可以用来修饰的结构:类.方法.变量 final 用来修饰一个类:此类不能被其他类所继承. 比如:String类.System类.StringBuffer类 final 用来修饰方 ...
- 学习javaScript必知必会(3)~数组(数组创建,for...in遍历,辅助函数,高级函数filter、map、reduce)
一.数组: 1.js是弱语言,js中的数组定义时:不用指定数据类型.不用功指定数组长度:数组可以存储任何数据类型的数据 2.数组定义的[ ] 的实质: [] = new Array(); {} = n ...
- RHCSA 第五天
1. a.创建普通变量local_data=1并访问 [root@sss ~]#local_data=1 [root@sss ~]#echo local_data local_data [root@s ...
- Centos下安装Spark
(注:由于第一次安装操作失误,所以重新安装了,因此截图为第一次的截图,命令为第二次安装的命令) (注:图是本人安装所截图,本人安装参考网址:https://www.cnblogs.com/shaosk ...
- 【转载】select case break引发的血案
原文请看:select case break引发的血案 我也遇到了,浪费了一个多小时. 牢记: for { switch var1{ case "not match": go En ...
- Ubuntu SVN 搭建
SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是取代CVS.互联网上很多版本控制服务已从CVS迁移到Subversion ...
- linux磁盘管理(全面解析)
目录 一:磁盘管理 1.磁盘管理作用 2.磁盘挂载顺序 3.磁盘分区 4.Linux 磁盘管理常用命令 5.磁盘分区内容 二:linux中分区的意义 三:分区的步骤与顺序 1.添加磁盘 2.查看创建新 ...
- AT2657 [ARC078D] Mole and Abandoned Mine
简要题解如下: 记 \(1\) 到 \(n\) 的路径为关键路径. 注意到关键路径只有一条是解题的关键,可以思考这张图长什么样子. 不难发现关键路径上所有边均为桥,因此大致上是关键路径上每个点下面挂了 ...
- Spring中NESTED和REQUIRED_NEW传播行为的区别
简介 PROPAGATION_REQUIRED_NEW: 表示当前方法必须运行在它自己的事务中.一个新的事务将被启动.如果存在当前事务,在该方法执行期间,当前事务会被挂起.如果使用JTATransac ...