从源码层面深度剖析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 的过程。其中最关键的几步有:
getSingleton(beanName, true)
依次从一二三级缓存中查找bean对象,如果缓存中存在对象,则直接返回(early); createBeanInstance(beanName, mbd, args)
选一个合适的构造函数,new实例对象(instance),此时的instance中依赖的属性还都是null,属于半成品; singletonFactories.put(beanName, oneSingletonFactory)
利用上一步的instance,构建一个 singletonFactory,并将其放到三级缓存中; populateBean(beanName, mbd, instanceWrapper)
填充bean:为该bean定义的属性创建对象或赋值; initializeBean("one",oneInstance, mbd)
初始化bean:对bean进行初始化或其他加工,如生成代理对象(proxy); 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解决循环依赖 及上述图片中可知:
深层原因为:先前 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 造成循环依赖报错的先约条件为:
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 为什么同样是代理会产生两种不同的现象?
同样是生成代理对象,同样是参与到循环依赖中,会产生不同现象的原因是:当他们处在循环依赖中时,生成代理的节点不同:
为什么 @Async 不能在 getEarlyBeanReference 时生成代理呢?对比下两者执行的代码过程发现:
两者都是在 AbstractAutoProxyCreator#getEarlyBeanReference 的方法对原始实例对象进行包装,如下图
使用 @Transactional 的Bean 在 create proxy 时,获取到一个advice ,随即生成了代理对象 proxy.
而使用 @Async 的Bean 在 create proxy 时,没有获取到 advice,不能被代理.
3.1.5 为什么@Async 在 getEarlyBeanReference 时不能返回一个 advice?
在 AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法内,其主要做的事情有:
第一步返回的 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、如何解决以上循环依赖报错?

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Dubbo架构设计与源码解析(二) 服务注册
作者:黄金 一、Dubbo简介 Dubbo是一款典型的高扩展、高性能、高可用的RPC微服务框架,用于解决微服务架构下的服务治理与通信问题。其核心模块包含【RPC通信】和【服务治理】,其中服务治理又分为服务注册与发现、服务容错、负载均衡、流量调度等。今天将重点介绍Dubbo的服务注册与发现。 二、SPI机制 在介绍服务注册发现之前,先简单介绍一下贯穿整个Dubbo源码,也是Dubbo实现自适应扩展的核心--SPI机制,下图为Dubbo SPI实现的简单类图。 • 1、Dubbo SPI原理:通过读取相应的配置文件找到具体实现类,然后通过以下两种方式实例化对象:(1)通过自适应的动态字节码编译技术,生成相应的动态代理类,(2)利用反射机制实现实例化。相较于Java SPI,Dubbo SPI实现了内部的IoC和Aop • 2、Dubbo SPI 优点:(1)高扩展:用户可以根据实际业务需求扩展相应的实现模块,包含字节码编译技术、rpc协议、通信方式、注册方式等,(2)解耦:通过封装SPI调用机制,架构上实现了上层应用与底层逻辑之间的解耦,为高扩展提供了支撑条件 • 3、Dubb...
- 下一篇
【开源之夏专访】热爱和努力永远不会被辜负——曹行行
夏天渐行渐远,秋天匆匆晃过,开源之夏2022在洁白如新的冬天如期收获了350 位同学的结项成果,这些同学也在开源软件社区留下了自己的足迹,感谢各位同学的支持。今年我们依旧按照最佳质量奖、突出贡献奖、最快进步奖、最具潜力奖四个角度评选出了具有代表性的20位优秀学生。 本次采访,我们邀请了最快进步奖获奖者曹行行,分享他的经验。 姓名:曹行行 性别:男 年龄:21 学校:北京工业大学 兴趣爱好:学习感兴趣的知识并加以实践 --自我介绍-- 1、首先,请简单地介绍一下自己。 大家好!我叫曹行行,来自北京工业大学新能源科学与工程(可再生能源利用)专业的大四学生,辅修专业计算机科学与技术。我在计算机方向的学习起步较晚,踏入计算机学科也是兴趣使然,但是我相信,永远都不会太晚,重要的是现在就去做。我付出了许多精力和时间去追赶科班生的水平,现在我认为我做到了,未来我也会继续努力下去。 2、我们了解到你的专业是能源动力类新能源方向,计算机类的知识是自学的吗?学习这些知识对本专业的学习有哪些帮助? 计算机类的知识并非完全自学,我在本校学习一个计算机科学与技术辅修专业,这是我在大学第二学年处于兴趣报名的,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Hadoop3单机部署,实现最简伪集群