人人都能看懂的spring源码解析,spring如何解决循环依赖

黄俊懿 黄俊懿     2023-04-08     699

关键词:

人人都能看懂的Spring源码解析,Spring如何解决循环依赖

往期内容:

  1. 人人都能看懂的Spring底层原理,看完绝对不会懵逼
  2. 简单易懂的Spring扩展点详细解析,看不懂你来打我
  3. 人人都能看懂的Spring源码解析,配置解析与BeanDefinition加载注册
  4. 简单易懂又非常牛逼的Spring源码解析,ConfigurationClassPostProcessor的具体逻辑
  5. 简单易懂值得收藏的Spring源码解析,依赖注入和bean的初始化

原理解析

什么是循环依赖

两个Bean,BeanA和BeanB,它们都有一个引用指向对方,这就是最简单的循环依赖。还有更复杂的循环依赖,涉及到多个bean,只要其中形成环,就是循环依赖。

循环依赖会有什么问题?

如果Spring不对这种情况做处理,那么在进行依赖注入的时候就会出现死循环

如何解决循环依赖

问题的根本原因

要解决循环依赖的问题,首先就要分析导致这个问题的根本原因。

因为上面是假设Spring只有一个缓存,那就是单例缓存池,而且该缓存池是用于存放已初始化完成的bean,而没有完成依赖注入的bean不算是初始化完成的bean,所以不会放入单例缓存池中。

所以beanA依赖beanB时,只能实例化一个beanB,然后对其进行依赖注入,而beanB又依赖了beanA,所以又只能实例化一个beanA,对其进行依赖注入,这样就没完没了了。

如何解决

解决办法就是多增加一个缓存(二级缓存),用于存放已经实例化但是未完成初始化的bean。因为增加了一个二级缓存,在bean被实例化的之后就预先放入到该二级缓存中,那么在后续其他bean进行依赖注入的时候,发现依赖了该bean,然后检查到二级缓存中有,就不需要往下进行实例化。

比如上图,在第二次getBean(beanA)的时候,二级缓存中已经存放了beanA,因此不会往下再次进行beanA的实例化,而是直接从缓存中取,死循环因此就解决了。

为什么需要三级缓存?

那看起来搞两级缓存就能解决问题,但是Spring实际上是有三级缓存的,那为什么Spring需要搞三级缓存呢

那是因为Spring要处理AOP,我们可以想一想,如果只有两级缓存,要怎么同时处理循环依赖和AOP呢?

此时就要在每个bean实例化后,放入二级缓存之前,都要判断一下这个bean是否需要进行AOP处理:如果需要,就要先通过动态代理生成代理对象,放入二级缓存的就是代理对象;如果不需要进行AOP处理,那么就把原始对象放入二级缓存。

这个显然不符合Spring的设计。Spring的设计是把AOP放到依赖注入完成以后,在初始化阶段触发的,而现在却要把AOP的处理提前到实例化之后。

那如果还是要把AOP放到初始化阶段触发,只有涉及到循环依赖的bean才提前进行AOP呢?那就要在每次从二级缓存获取bean的时候,都要判断一下是否需要进行AOP处理,这样就太繁琐了。

因此最好的解决办法就是再加一级缓存,三级缓存。先从二级缓存中取,如果有,直接返回,如果二级缓存没有,再从三级缓存中拿,放入二级缓存,并从三级缓存删除,并且只有从三级缓存中取的时候,才判断是否需要进行AOP处理

Spring的三级缓存

  • singletonObjects:一级缓存,Map<String, Object>类型,key是beanName,value是已初始化完成的bean。
  • earlySingletonObjects:二级缓存,Map<String, Object>类型,key是beanName,value是已实例化完成但是未进行初始化的bean。
  • singletonFactories:三级缓存,Map<String, ObjectFactory<?>>,key是beanName,value是ObjectFactory<?>类型。ObjectFactory是一个函数式接口,可以视为一个回调函数,里面的逻辑就是调用bean后置处理判断是否需要进行AOP处理,如果需要则进行AOP处理,返回一个代理对象,如果不需要进行AOP处理,则返回原始对象。

源码走读

Spring的三级缓存

上面描述的三级缓存,就是DefaultSingletonBeanRegistry的成员变量

DefaultSingletonBeanRegistry又是DefaultListableBeanFactory的祖先类,所以三级缓存就在DefaultListableBeanFactory里面。

提前暴露


AbstractAutowireCapableBeanFactory#doCreateBean方法,首先调用createBeanInstance方法进行bean的实例化


实例化以后,就是判断是否需要提前暴露(earlySingletonExposure为true),如果需要,则调用addSingletonFactory方法进行提前暴露

然后下面的populateBean方法里面就是依赖注入的处理,initializeBean方法里面就是bean初始化的处理,这两个方法在前面的文章已经讲过。

需要提前暴露的条件:

  • mbd.isSingleton():是否单例。
  • this.allowCircularReferences:是否允许循环依赖,默认为true(但是高版本改成默认为false了,Spring希望我们把代码写的更好,而不是指望Spring来给我们处理循环依赖的问题)。
  • isSingletonCurrentlyInCreation(beanName):该bean是否正在创建中。

三个条件都满足,就调用addSingletonFactory方法,提前暴露。


addSingletonFactory方法里面,就是把beanName和ObjectFactory类型的对象singletonFactory,放入三级缓存singletonFactories。这里的singletonFactory的就是外面的lambda表达式 () -> getEarlyBeanReference(beanName, mbd, bean)。ObjectFactory是一个函数式接口,里面有一个getObject方法,而singletonFactory的getObject方法就是调用getEarlyBeanReference(beanName, mbd, bean)。

getSingleton方法

一个bean在依赖注入阶段,要处理对别的bean的依赖,会调用beanFactory的getBean方法,尝试从容器中取,getBean方法会先调用getSingleton方法,尝试从三级缓存中取

  1. 首先从第一级缓存取this.singletonObjects.get(beanName),不是null就返回
  2. 如果第一级缓存取到的是null,则从第二级缓存取this.earlySingletonObjects.get(beanName),不是null就返回
  3. 如果第二级缓存取到的还是null,则从第三级缓存取this.singletonFactories.get(beanName)
  4. 如果第三级缓存取到的不是null,则调用getObject方法,获取getObject方法返回的对象,放入二级缓存,从三级缓存中删除
  5. 如果第三级缓存取到的还是null,就要走创建bean的逻辑

这里的singletonFactory的getObject实现方法,就是addSingletonFactory方法的第二个参数的lambda表达式 () -> getEarlyBeanReference(beanName, mbd, bean),也就是会调用 getEarlyBeanReference(beanName, mbd, bean) 方法获取返回的对象。

可以看到遍历所有的bean后置处理器,判断如果是SmartInstantiationAwareBeanPostProcessor类型,就回调getEarlyBeanReference方法,这里会进入到AbstractAutoProxyCreator#getEarlyBeanReference方法。


wrapIfNecessary方法就是判断如果当前bean需要进行AOP处理,则进行动态代理,返回代理对象;如果不需要进行AOP处理,会返回原始对象。

总结

以上就是关于“Spring如何处理循环依赖”的讲解的全部内容。下面做一个简单总结:

  • Spring是通过三级缓存解决循环依赖的。一级缓存是singletonObjects;二级缓存是earlySingletonObjects;三级缓存是singletonFactories。
  • 一个bean在实例化完成后,在进行依赖注入之前,如果判断满足三个条件(当前bean是一个单例bean,并且允许循环依赖,并且当前bean正在创建中),就会进行提前暴露,放入到第三级缓存中。
  • 在依赖注入阶段,会调用beanFactory的getBean方法获取依赖的bean,getBean方法会先调用getSingleton方法尝试从三级缓存中取。
  • getSingleton方法先尝试从第一级缓存取,如果没有再从第二级缓存取,如果再没有就从第三级缓存取,如果第三级缓存中有,会调用getObject方法获取返回对象,再放入第二级缓存,从第三级缓存中删除。
  • singletonFactory的getObject方法,会调用getEarlyBeanReference方法,里面会调用到AbstractAutoProxyCreator的getEarlyBeanReference方法,判断是否要进行AOP处理,如果需要会进行动态代理返回代理对象,如果不需要,就返回原始对象。

大白话聊聊微服务——人人都能看懂的演进过程

这篇博客的本意是希望看到这篇文章的读者能够很轻松的理解我想表达的意思。但程序向的分享经常会不经意间就贴上了代码,很可能就会让人看的很懵。而且我认为分享一个东西,只有对方真正明白了其中的逻辑,才是有意义... 查看详情

人人都能看懂的6种限流实现方案(代码片段)

  为了上班方便,去年我把自己在北郊的房子租出去了,搬到了南郊,这样离我上班的地方就近了,它为我节约了很多的时间成本,我可以用它来做很多有意义的事,最起码不会因为堵车而闹心了,幸福感直线上升。  但即... 查看详情

小学生都能看懂的fft!!!

小学生都能看懂的FFT!!!前言在创新实践重心偷偷看了一天FFT资料后,我终于看懂了一点。为了给大家提供一份简单易懂的学习资料,同时也方便自己以后复习,我决定动手写这份学习笔记。食用指南:本篇受众:如标题所示... 查看详情

一文彻底解密spring源码之springmvc

小白都能看懂的Spring源码揭秘之SpringMVC前言SpringMVC请求流程SpringMVC两大阶段初始化HttpServletBean#init()FrameworkServlet#initServletBeanFrameworkServlet#initWebApplicationContextDispatchServlet#onRefreshSpringMVC九大组件Multipar 查看详情

人人都能看懂的lstm

点击上方“迈微AI研习社”,选择“星标★”公众号重磅干货,第一时间送达大家好,我是Charmve。熟悉深度学习的朋友知道,LSTM是一种RNN模型,可以方便地处理时间序列数据,在NLP等领域有广泛应用。在... 查看详情

7万字总结spring,这回能看懂spring源码了!(代码片段)

...xff1a;https://bugstack.cn沉淀、分享、成长,让自己和他人都能有所收获!😄一、为什么手写Spring这个与我们码农朝夕相处的Spring,就像睡在你身边的媳妇,你知道找她要吃、要喝、要零花钱、要买皮肤。但你不知... 查看详情

7万字总结spring,这回能看懂spring源码了!(代码片段)

...xff1a;https://bugstack.cn沉淀、分享、成长,让自己和他人都能有所收获!😄一、为什么手写Spring这个与我们码农朝夕相处的Spring,就像睡在你身边的媳妇,你知道找她要吃、要喝、要零花钱、要买皮肤。但你不知... 查看详情

小白都能看懂的关于mixins机制的理解(代码片段)

前言​在学习Flutter源码的时候,看到各种复杂的mixin和on,为了便于后续Flutter的学习,这里有必要一起来份详细Dart的Mixin机制。什么是mixins首先看看官方文档的定义:Mixinsareawayofreusingaclass’scodeinmultipleclasshierarchie... 查看详情

小白都能看懂的关于mixins机制的理解(代码片段)

前言​在学习Flutter源码的时候,看到各种复杂的mixin和on,为了便于后续Flutter的学习,这里有必要一起来份详细Dart的Mixin机制。什么是mixins首先看看官方文档的定义:Mixinsareawayofreusingaclass’scodeinmultipleclasshierarchie... 查看详情

一文彻底解密spring源码之springmvc(代码片段)

小白都能看懂的Spring源码揭秘之SpringMVC前言SpringMVC请求流程SpringMVC两大阶段初始化HttpServletBean#init()FrameworkServlet#initServletBeanFrameworkServlet#initWebApplicationContextDispatchServlet#onRefreshSpringMVC九大组件Multipar 查看详情

review:人人都能看懂pod与容器设计模式

Review:人人都能看懂Pod与容器设计模式[]:https://mp.weixin.qq.com/s?__biz=MzA4ODg0NDkzOA==&mid=2247488010&idx=1&sn=37e4a3bff67d5ce466685b646cbab279&chksm=9022adefa75524f9f63ddc98d05210958725b79ba9823 查看详情

优秀文章及工具汇总

...:如何在Python中处理不平衡数据2.2模型详解2.2.1序列模型人人都能看懂的GRU强推|人人都能看懂的LSTM介绍及反向传播算法推导(非常详细)2.2.2机器学习Kaggle神器LightGBM最全解读!2.3业务总结2.3.1CTR算法大佬看了流泪,为什么这么好的... 查看详情

小白都能看得懂的教程一本教你如何在前端实现富文本编辑器(代码片段)

小白都能看得懂的教程一本教你如何在前端实现富文本编辑器博主博客文章内容导航(实时更新)更多优质文章推荐:收藏!最详细的Python全栈开发指南看完这篇你还不会Python全栈开发你来打我!!!一本教你如何在... 查看详情

人人都能看懂的python装饰器入门教程

大家好,我是萱萱!之前的文章中提到,很多人认为理解了装饰器的概念和用法后,会觉得自己的Python水平有一个明显的提高。但很多教程在一上来就会给出装饰器的定义以及基本用法,例如你一定会在很多... 查看详情

小白都能看懂的实战教程手把手教你pythonweb全栈开发(day4)(代码片段)

...家好,我叫亓官劼(qíguānjié),这个《小白都能看懂的实战教程手把手教你PythonWeb全栈开发》是一个零基础的实战教程,手把手带你开发一套系统,带你了解Pythonweb全栈开发,目前正在连续更新中,如... 查看详情

小白都能看懂的redis讲解--针对单个键操作集锦

1重命名键renamekeynewname可以对键重命名,下面的例子我们创建了一个key为name,value为luke的键值对。然后将name重命名为user,之后查询name就返回nil,而user是可以查到值的。127.0.0.1:6379>setnamelukeOK127.0.0.1:6379>getname"luke"127.0.0.1:6379&... 查看详情

猴子都能看懂的androidmenu总结(代码片段)

简单谈谈Androidmenu菜单的使用,帮助入门的同学。menu主要分类:选项菜单:最常规的菜单子菜单:多层菜单的实现方案之一上下文菜单:android中长按视图控件后出现的菜单选项菜单(OptionMenu)该菜单默认在右上... 查看详情

零基础都能看懂的stlmap详解(代码片段)

🎈作者:Linux猿🎈简介:CSDN博客专家🏆,C/C++、面试、刷题、算法尽管咨询我,关注我,有问题私聊!🎈关注专栏:C/C++面试通关集锦 (优质好文持续更新中……)... 查看详情