Spring 动态代理时是如何解决循环依赖的?为什么要使用三级缓存?
前言
在研究 『 Spring 是如何解决循环依赖的 』 的时候,了解到 Spring 是借助三级缓存来解决循环依赖的。
同样在上一节留下了疑问:
- 循环依赖为什么要使用三级缓存?而不是使用二级缓存?
- AOP 动态代理对循环依赖的有没有什么影响?
本篇文章也是围绕上面的内容进行展开。
笔记也在不断整理,之前可能会有点杂乱。
循序渐进,看一看什么是循环依赖?
开始先简单回顾一下 Bean 的创建过程,当然小伙伴也可以直接阅读『 单例 Bean 的创建 』这篇文章。
不过考虑到阅读本文前再阅读上一篇文章、Debug 等等,会比较耗时,所以本篇文章前面一小部分会先对之前的文章内容做简要概括,也相当于对我自己学习的知识进行一个总结。
先来回顾一下三级缓存的概念。
singletonObjects: 一级缓存,存储单例对象,Bean 已经实例化,初始化完成。
earlySingletonObjects: 二级缓存,存储 singletonObject,这个 Bean 实例化了,还没有初始化。
singletonFactories: 三级缓存,存储 singletonFactory。
Bean 的创建过程
@Service
public class CircularServiceA {
private String fieldA = "字段 A";
}
通过上面的流程,可以看出 Spring 在创建 Bean 的过程中重点是在 AbstractAutowireCapableBeanFactory 中的以下三个步骤:
- 实例化 createBeanInstance: 其中实例化 Bean 并对 Bean 进行赋值,像例子中的
fieldA
字段在这里就会赋值。 - 属性注入 populateBean: 可以理解为对 Bean 里面的属性进行赋值。(会依赖其他 Bean)
- 初始化 initializeBean: 执行初始化和 Bean 的后置处理器。
实例化赋值源码可以阅读:
BeanUtils.instantiateClass(constructorToUse)
如果要依赖其他 Bean 呢?
那如果 CircularServiceA 依赖了其他 Bean 呢?
@Service
public class CircularServiceA {
private String fieldA = "字段 A";
@Autowired
private CircularServiceB circularServiceB;
}
@Service
public class CircularServiceB {
}
当 A 依赖了 B 的时候,在 createBeanInstance
这一步,并不会对 B 进行属性赋值。
而是在 populatedBean
这里查找依赖项,并创建 B。
循环依赖下的创建过程
循环依赖的场景,在上一篇文章已经有所讲解,这里仅仅画图说明一下。
@Service
public class CircularServiceA {
private String fieldA = "字段 A";
@Autowired
private CircularServiceB circularServiceB;
}
@Service
public class CircularServiceB {
@Autowired
private CircularServiceA circularServiceA;
}
在 A 和 B 循环依赖的场景中:
B populatedBean
查找依赖项 A 的时候,从一级缓存中虽然未获取到 A,但是发现 A 在创建中。
此时,从三级缓存中获取 A 的 singletonFactory
调用工厂方法,创建 getEarlyBeanReference
A 的早期引用并返回。
B 引用到 A ,B 就可以初始化完毕,然后 A 同样也可以初始化完毕了。
二级缓存能否解决循环依赖
通过上面的图,仔细分析一下,其实把二级缓存拿掉,在 B 尝试获取 A 的时候直接返回 A 的实例,是不是也是可以的?
答案是:可以的!
但是为什么还是用三级缓存呢?
网上的很多资料说是和动态代理有关系,那就从动态代理的方面继续往下分析分析。
动态代理的场景
在 JavaConfig(配置类) 上添加 @EnableAspectJAutoProxy
注解,开启 AOP ,通过 Debug 循序渐进看一看动态代理对循环依赖的影响。
动态代理下,Bean 的创建过程
@Service
public class CircularServiceA {
private String fieldA = "字段 A";
public void methodA() {
System.out.println("方法 A 执行");
}
}
@Aspect
@Component
public class AspectA {
@Before("execution(public void com.liuzhihang.circular.CircularServiceA.methodA())")
public void beforeA() {
System.out.println("beforeA 执行");
}
}
只有 A 的情况下,给 A 添加切面,开始 Debug。
前面的流程都相同,在 initializeBean 开始出现差异。
这一步需要初始化 Bean 并执行 Bean 的后置处理器。
其中有一个处理器为: AnnotationAwareAspectJAutoProxyCreator
其实就是加的注解切面,会跳转到 AbstractAutoProxyCreator 类的 postProcessAfterInitialization 方法
如图所示:wrapIfNecessary 方法会判断是否满足代理条件,是的话返回一个代理对象,否则返回当前 Bean。
后续调用 getProxy
、createAopProxy
等等,最终执行到下面一部分。
最终会执行到这里,AOP 代理相关的就不细看了。
一路放行,直到 initializeBean 执行结束。
此时发现:A 被替换为了代理对象。
所以 doCreateBean 返回,以及后面放到一级缓存中的都是代理对象。
有循环依赖的动态代理
这一次把循环依赖打开:
@Service
public class CircularServiceA {
private String fieldA = "字段 A";
@Autowired
private CircularServiceB circularServiceB;
public void methodA() {
System.out.println("方法 A 执行");
}
}
@Aspect
@Component
public class AspectA {
@Before("execution(public void com.liuzhihang.circular.CircularServiceA.methodA())")
public void beforeA() {
System.out.println("beforeA 执行");
}
}
@Service
public class CircularServiceB {
@Autowired
private CircularServiceA circularServiceA;
public void methodB() {
}
}
@Aspect
@Component
public class AspectB {
@Before("execution(public void com.liuzhihang.circular.CircularServiceB.methodB())")
public void beforeB() {
System.out.println("beforeB 执行");
}
}
开始 Debug,前面的一些列流程,都和正常的没有什么区别。而唯一的区别在于,创建 B 的时候,需要从三级缓存获取 A。
此时在 getSingleton
方法中会调用:singletonObject = singletonFactory.getObject();
有时会比较疑惑 singletonFactory.getObject()
调用的是哪里?
所以这一块调用的是 getEarlyBeanReference
,开始遍历执行 BeanPostProcessor
。
看到 wrapIfNecessary
就明白了吧!这块会获取一个代理对象
。
也就是说此时返回,并放到二级缓存的是一个 A 的代理对象。
这样 B 就创建完毕了!
到 A 开始初始化并执行后置处理器了!因为 A 也有代理,所以 A 也会执行到 postProcessAfterInitialization
这一部分!
但是在执行 wrapIfNecessary
之前,会先判断代理对象缓存是否有 A 了。
this.earlyProxyReferences.remove(cacheKey) != bean
但是这块获取到的是 A 的代理对象。肯定是 false 。 所以不会再生成一次 A 的代理对象。
总结
可以看到,循环依赖下,有没有代理情况下的区别就在:
singletonObject = singletonFactory.getObject();
在循环依赖发生的情况下 B 中的 A 赋值时:
- 无代理:getObject 直接返回原来的 Bean
- 有代理:getObject 返回的是代理对象
然后都放到二级缓存。
为什么要三级缓存?
- 假设去掉三级缓存
去掉三级缓存之后,Bean 直接创建 earlySingletonObjects
, 看着好像也可以。
如果有代理的时候,在 earlySingletonObjects
直接放代理对象就行了。
但是会导致一个问题:在实例化阶段就得执行后置处理器,判断有 AnnotationAwareAspectJAutoProxyCreator 并创建代理对象。
这么一想,是不是会对 Bean 的生命周期有影响。
同样,先创建 singletonFactory
的好处就是:在真正需要实例化的时候,再使用 singletonFactory.getObject() 获取 Bean 或者 Bean 的代理。相当于是延迟实例化。
- 假设去掉二级缓存
如果去掉了二级缓存,则需要直接在 singletonFactory.getObject()
阶段初始化完毕,并放到一级缓存中。
那有这么一种场景,B 和 C 都依赖了 A。
要知道在有代理的情况下 singletonFactory.getObject()
获取的是代理对象。
而多次调用 singletonFactory.getObject()
返回的代理对象是不同的,就会导致 B 和 C 依赖了不同的 A。
那如果获取 B 到之后直接放到一级缓存,然后 C 再获取呢?
……
一级缓存放的是已经初始化完毕的 Bean,要知道 A 依赖了 B 和 C ,A 这时候还没有初始化完毕。
小结
循环依赖的场景有很多,本文只是通过 Debug ,来了解到循环依赖和 AOP 之间的关系,以及了解到为什么要用三级缓存。
当然,Spring 设计之初是什么样子的?如何一步一步发展成现在这种的?
肯定是不能慢慢去研究了,所以只能以现在的版本,去揣测作者的意图。
不足之处,多多指正。
相关推荐
Spring 动态代理时是如何解决循环依赖的?为什么要使用三级缓存?的更多相关文章
- Spring ioc(4)---如何解决循环依赖
前面说到对象的创建,那么在创建的过程中Spring是怎么又是如何解决循环依赖的呢.前面提到有个三级缓存.就是利用这个来解决循环依赖.打个比方说实例化A的时候,先将A创建(早期对象)放入一个池子中.这个 ...
- Spring解决循环依赖,你真的懂了吗?
导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...
- Spring如何解决循环依赖,你真的懂了?
导读 前几天发表的文章SpringBoot多数据源动态切换和SpringBoot整合多数据源的巨坑中,提到了一个坑就是动态数据源添加@Primary接口就会造成循环依赖异常,如下图: 这个就是典型的构 ...
- Spring如何解决循环依赖问题
目录 1. 什么是循环依赖? 2. 怎么检测是否存在循环依赖 3. Spring怎么解决循环依赖 本文主要是分析Spring bean的循环依赖,以及Spring的解决方式. 通过这种解决方式,我们可 ...
- 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- Spring AOP代理时 ClassCastException: $Proxy0 cannot be cast to (类型转换错误)
Spring AOP代理时 ClassCastException: $Proxy0 cannot be cast to (类型转换错误) 问题: 今天在用AfterReturningAdvice时,a ...
- Spring解决循环依赖
1.Spring解决循环依赖 什么是循环依赖:比如A引用B,B引用C,C引用A,它们最终形成一个依赖环. 循环依赖有两种 1.构造器循环依赖 构造器注入导致的循环依赖,Spring是无法解决的,只能抛 ...
- 彻底理解Spring如何解决循环依赖
Spring bean生命周期 可以简化为以下5步. 1.构建BeanDefinition 2.实例化 Instantiation 3.属性赋值 Populate 4.初始化 Initializati ...
- spring: 我是如何解决循环依赖的?
1.由同事抛的一个问题开始 最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到.平时自认为对spring循环依赖问题还是比较了解的,直到遇到这个和后面的 ...
随机推荐
- 5个有趣且不必要的 JavaScipt 技巧
前一段时间,我创建了一个标题为"7个可爱的Web开发技巧"的帖子.在这里,我描述了一些有趣的技巧,您可以使用3种主要的Web技术之一来实现这些技巧-html,css和JavaScr ...
- 技术基础 | 监测Apache Cassandra的简明方式——MCAC
点击这里在GitHub上访问我们,以便深入了解DataStax的开源项目--Apache Cassandra指标收集器(Metric Collector for Apache Cassandra, o ...
- jupyter安装插件Nbextensions,实现代码提示功能(终极方法)
jupyter安装插件,实现代码提示功能 第一步 pip install jupyter_contrib_nbextensions -i https://mirrors.tuna.tsinghua.e ...
- Syn_Flood攻击&防御手段
Syn_Flood攻击原理 攻击者首先伪造地址对服务器发起SYN请求(我可以建立连接吗?),服务器就会回应一个ACK+SYN(可以+请确认).而真实的IP会认为,我没有发送请求,不作回应.服务器没有收 ...
- Vue.nextTick()的使用
什么是Vue.nextTick()?? 定义:在下次 DOM 更新循环结束之后执行延迟回调.在修改数据之后立即使用这个方法,获取更新后的 DOM. 所以就衍生出了这个获取更新后的DOM的Vue方法.所 ...
- LeetCode200 岛屿的个数
给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量.一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的.你可以假设网格的四个边均被水包围. 示例 1: 输入: ...
- 来不及解释!Linux常用命令大全,先收藏再说
摘要:Linux常用命令,很适合你的. 一提到操作系统,我们首先想到的就是windows和Linux.Windows以直观的可视化的方式操作,特别适合在桌面端PC上操作执行相应的软件.相比较Windo ...
- Docker安装MySQL,Redis,阿里云镜像加速
Docker安装 虚拟化容器技术.Docker基于镜像,可以秒级启动各种容器.每一种容器都是一个完整的环境,容器之间相互隔离. 如果之前安装的有其他版本,卸载旧的版本. $ sudo yum remo ...
- XSS-labs通关挑战(xss challenge)
XSS-labs通关挑战(xss challenge) 0x00 xss-labs 最近在看xss,今天也就来做一下xss-labs通过挑战.找了好久的源码,终于被我给找到了,因为在GitHub上 ...
- (二)数据源处理5-excel数据转换实战(上)
把excel_oper02.py 里面实现的:通过字典的方式获取所有excel数据.放进utils: ️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️ utils: def get_al ...