Spring 源码解析 - @Async 注解下的循环依赖问题原理
一、@Async 注解下的循环依赖问题
我们都知道 Spring IOC
单例模式下可以帮助我们解决循环依赖问题,比如下面自己依赖自己循环依赖的场景:
@Component
public class TestAsync {@ResourceTestAsync async;public void test() {System.out.println("test....");}
}
从容器中获取到该 bean
执行测试方法:
public class App {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext("com.demo.test");TestAsync testAsync = context.getBean("testAsync", TestAsync.class);testAsync.test();}}
可以看到正常执行,但当我们加上 @Async
注解后:
@Component
@EnableAsync
public class TestAsync {@ResourceTestAsync async;@Asyncpublic void test() {System.out.println("test....");}
}
再次执行发现报错了:
是不是很奇怪,难道代理对象就会有问题吗,如果换成 @Transactional
呢:
@Component
@EnableTransactionManagement
public class TestAsync {@ResourceTestAsync async;@Transactionalpublic void test() {System.out.println("test....");}
}
再次执行,发现可以正常运行:
那为什么 @Async
会有问题呢,其实和我们上篇文章中讲解的 BeanPostProcessor
扩展接口有关,这里先说一下解决方法:
将依赖注入换成懒加载的方式即可:
@Component
@EnableAsync
public class TestAsync {@Resource@LazyTestAsync async;@Asyncpublic void test() {System.out.println("test...." + Thread.currentThread().getName());}
}
可以看到恢复正常了,下面从源码角度分析下出现该问题的原因。
如果不了解 BeanPostProcessor
扩展接口,可以先看下下面这篇文章:
Spring 源码解析 - BeanPostProcessor 扩展接口
二、源码分析
2.1 @EnableAsync
当使用 @EnableAsync
开启异步支持时,会向 Spring
容器中注入AsyncConfigurationSelector.class
类:
在该类中,selectImports
下根据 adviceMode
选择注入配置类,adviceMode
默认为 PROXY
,会注入 ProxyAsyncConfiguration.class
配置类:
在 ProxyAsyncConfiguration.class
配置类下,注入了一个 AsyncAnnotationBeanPostProcessor
扩展类:
下面看下AsyncAnnotationBeanPostProcessor
扩展类的继承树:
AsyncAnnotationBeanPostProcessor
从 BeanPostProcessor
获得bean
初始化前后的扩展能力,从 ProxyProcessorSupport
获取代理能力。
这里重点看 BeanPostProcessor
扩展,在 BeanPostProcessor
中有两个核心的扩展方法如下:
public interface BeanPostProcessor {/*** 实例化及依赖注入完成后、bean 初始化方法触发之前执行*/Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;/*** bean 初始化方法触发后执行*/Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}
下面看挨个看AsyncAnnotationBeanPostProcessor
中这两个方法干了什么:
在AsyncAnnotationBeanPostProcessor
没有直接实现 postProcessBeforeInitialization
,实现在 AbstractAdvisingBeanPostProcessor
下的 postProcessBeforeInitialization
方法:
没有做任何操作,直接返回的原 bean
,同样 postProcessAfterInitialization
方法也在 AbstractAdvisingBeanPostProcessor
下:
在这里实际生成了代理 bean
进行返回。
到这我们需要记住 AsyncAnnotationBeanPostProcessor
的postProcessBeforeInitialization
前通知没有做任何操作, postProcessAfterInitialization
后通知创建了代理实例。
2.2 getEarlyBeanReference
了解过循环依赖的应该知道 Spring
中使用三级缓存来解决循环依赖问题,其中实例化 bean
后,会首先曝光至第三级缓存中,该逻辑在 AbstractAutowireCapableBeanFactory
类的 doCreateBean
方法下:
在 doCreateBean
方法下的 populateBean
主要是进行了依赖的注入:
在进行依赖注入时,会递归尝试从三级缓存中获取 bean
,由于这里是循环依赖,已经放入了第三级缓存中,因此可以命中,这快的源码逻辑在 DefaultSingletonBeanRegistry
类下的 getSingleton
方法:
这里可以看出三级缓存命中会执行 ObjectFactory.getObject()
方法获取一个早期的实例,获取之后存入二级缓存中,从前面放入三级缓存可以看出其实是触发的 AbstractAutowireCapableBeanFactory
下的 getEarlyBeanReference
方法:
这里会判断是否存在 SmartInstantiationAwareBeanPostProcessor
类型的 BeanPostProcessor
扩展,然后尝试使用 getEarlyBeanReference
获取一个早期的实例,从前面 AsyncAnnotationBeanPostProcessor
的继承树可以看出并没有实现 SmartInstantiationAwareBeanPostProcessor
因此这里拿到的就是原来的 bean
实例:
到这里我们需要记住,Spring
单例缓存中存储的是 TestAsync
真正的实例对象。
2.3 initializeBean
在依赖注入后会触发 initializeBean
方法,进行 bean
的初始化操作:
在 initializeBean
方法中,执行初始化前,先执行BeanPostProcessors
的前置方法,并且将前置方法返回的 bean
代替原先创建的 bean
,在 bean
初始化后执行BeanPostProcessors
的后置方法,并将后置方法返回的 bean
代替原先的 bean
,从上面对 AsyncAnnotationBeanPostProcessor
的简单分析得出,前置方法没做任何处理,后置方法会生成一个代理对象,因此initializeBean
方法最终返回的是代理对象:
可以看出最后生成的是一个代理对象,现在应该就会发现一个问题了,在 Spring
单例容器中和依赖注入中的都是 TestAsync
真正的实例,而这里返回的是代理实例,现在相当于单例的 bean
存在了两个不同的实例。
2.4 判断是否出现重复实例
在回到 doCreateBean
方法下 initializeBean
执行后:
如果是单例的话,则尝试从容器中获取当前的 beanName
实例,由于前面已经曝光到了二级缓存中,因此这里可以获取到,但容器中的bean
实例和当前的 bean
实例已经不是一个实例了,因此会进入到 else if
中,这里获取到该 beanName
所有的依赖,通过 removeSingletonIfCreatedForTypeCheckOnly
删除已经创建好的 bean
实例,因为单例模式下 Spring
仅允许有一个实例,这里可以看下 removeSingletonIfCreatedForTypeCheckOnly
方法的逻辑:
这里主要判断 alreadyCreated
中是否存在,如果不存在则删除单例缓存中的实例,那 alreadyCreated
什么时候放入的呢,其实在AbstractAutowireCapableBeanFactory
类的 doGetBean
方法中触发的 markBeanAsCreated
方法:
因此 removeSingletonIfCreatedForTypeCheckOnly
方法这里会返回 false
,在回到 doCreateBean
中继续看,由于 removeSingletonIfCreatedForTypeCheckOnly
返回 false
,正好符合条件被加入了 actualDependentBeans
集合中,再下面如果actualDependentBeans
集合不为空则抛出异常,这个异常是不是和之前报错的异样一样:
三、为什么 @Transactional 不会出现这种问题呢
从上面的分析可以得出结论假如 getEarlyBeanReference
可以获取到代理实例,是不是就不会发生后面的问题,这恰恰也是 @Transactional
情况下的不会出现该问题的关键点。
@Transactional
代理使用的是 InfrastructureAdvisorAutoProxyCreator
从 InfrastructureAdvisorAutoProxyCreator
的继承树可以看到,其继承了 SmartInstantiationAwareBeanPostProcessor
方法。并且在父类 AbstractAutoProxyCreator
中重写了 getEarlyBeanReference
方法:
下面可以 debug
一下 AbstractAutowireCapableBeanFactory
中的 getEarlyBeanReference
方法:
可以看到这里还是真正的实例对象,下面会进到 AbstractAutoProxyCreator
中重写的 getEarlyBeanReference
方法,最终进到当前类的 wrapIfNecessary
方法:
到这里就已经生成了一个代理类了,再回到 AbstractAutowireCapableBeanFactory
中的 getEarlyBeanReference
方法中,这时返回的就是代理类了。
相关文章:

Spring 源码解析 - @Async 注解下的循环依赖问题原理
一、Async 注解下的循环依赖问题 我们都知道 Spring IOC 单例模式下可以帮助我们解决循环依赖问题,比如下面自己依赖自己循环依赖的场景: Component public class TestAsync {ResourceTestAsync async;public void test() {System.out.println("t…...

8个全球性编程比赛,天才程序员的梦想舞台
很多编程爱好者在学习之初,都渴望与全球的程序员一较高下,以证明自己的实力。 一些全球性的编程竞赛为他们提供了这样的机会,不仅可以与全世界的顶尖程序员们交流,还有机会获得丰厚的奖金和进入顶级公司的机会,更重要…...

2023年中国海洋大学计算机及电子信息考研分析
考研时间跨度: 初试时间: 2022年8月23 海大推免及创新人才计划接收通知。 2022年9月13 海大专业目录及人数,包含推免。 2022年10月18 2022年硕士研究生计划 ,不含推免。 海大2022年硕士研究生计划 网上第一次时间为2022年9月24日…...

【C++笔试强训】第六天
选择题 1. 解析:十进制转换为八进制就是不断的除8,取余数。十进制转换成其他进制的数就是除以进制,取余。 解析:注意printf的转换,%%只会打印一个%,所以选A。 解析:由于()的原因p先和*结合&…...
Redission 中的 RedLock 原理实现, springboot 你造吗?
分布锁之RedLock 锁住你的心我的爱 🚂为什么需要使用 RedLock锁被误释放时钟不一致问题锁的“延迟释放”而不是死锁Redlock是啥redlock 存在什么问题惊群效应时钟漂移Redisson 实现 RedLock在 Redisson 中, RedLock的实现类是哪一个类?这一招叫抛砖引玉springboot …...

【沐风老师】3dMax一键房屋创建者插件使用方法详解
3dmax一键房屋创建者,一键生成墙体、窗洞和门洞的插件!这个脚本主要用于创建或捕获一些架构项目所代表的平面,这是通过导入它们并在每个所需的层添加值来实现的。传统方法,但是省事儿多了! 【版本要求】 3dMax 2015及…...
C/C++ 变量详解
文章目录前言一、静态变量与动态变量1. 概念2. 区别3. 使用方法和注意事项3.1 静态变量3.2 动态变量4. 结论二、全局变量与局部变量1. 区别2. 全局变量的使用方法和注意事项3. 局部变量的使用方法和注意事项4. 总结前言 对C学习感兴趣的可以看看这篇文章哦:C/C教程…...
新SSD盘安装操作系统启动不了
今天打算给电脑升级下装备,加装一块固态硬盘。 电脑原本自带两块硬盘(SSD128GSATA1T),SSD清理了许久还是没空间,于是就买了块1TSSD,打算扩容下。 打开电脑后盖傻眼了,没有备用插槽,…...
基于Spring、SpringMVC、MyBatis的病历管理系统
文章目录 项目介绍主要功能截图:登录首页医院公告管理用户管理科室信息管理医生管理出诊信息管理预约时间段管理预约挂号管理门诊病历管理就诊评价管理轮播图管理功能架构图部分代码展示设计总结项目获取方式🍅 作者主页:Java韩立 🍅 简介:Java领域优质创作者🏆、 简历…...
QT编程从入门到精通之三十四:“第五章:Qt GUI应用程序设计”之“5.5 Qt Creator使用技巧”
目录 第五章:Qt GUI应用程序设计 5.5 Qt Creator使用技巧 第五章:Qt GUI应用程序设计 在“Qt 程序创建基础”上,本章将继续深入地介绍Qt Creator设计GUI应用程序的方法,包括Qt创建的应用程序项目的基本组织结构,可视化设计的UI界面文件的原理和运行机制,信号与槽的使用…...

网络工程方向有哪些SCI期刊推荐? - 易智编译EaseEditing
以下是网络工程领域的一些SCI期刊推荐: IEEE Transactions on Network and Service Management: 这是一个IEEE旗下的期刊,涵盖了网络与服务管理方面的研究。主要关注网络管理、服务管理和其它相关领域的创新和最新研究。 Computer Networks: 这是一本著…...

netty入门(二十六)任务加入异步线程池源码剖析
1.handler中加入线程池和Context添加线程池 1.1 源码剖析目的 (1)在 Netty 中做耗时的,不可预料的操作,比如:数据库、网络请求、会严重影响 Netty 对 Socket 的处理速度。 (2)而解决方法就是…...

神经网络算法入门和代码
文章内容 感知机(Perceptron)反向传播算法(Back Propagation algorithm)RBF(Radial Basis Function,径向基函数) 网络:单一层前馈网络,它使用径向基作为隐层神经元激活函数ART(Adaptive Resona…...

如何用一个端口同时暴露 HTTP1/2、gRPC、Dubbo 协议?
作者:华钟明 本文我们将介绍 Apache Dubbo 灵活的多协议设计原则,基于这一设计,在 Dubbo 框架底层可灵活的选用 HTTP/2、HTTP/REST、TCP、gRPC、JsonRPC、Hessian2 等任一 RPC 通信协议,同时享用统一的 API 与对等的服务治理能力。…...

ToBeWritten之杂项2
也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大 少走了弯路,也就错过了风景,无论如何,感谢经历 转移发布平台通知:将不再在CSDN博客发布新文章,敬…...

Linux三剑客之awk命令详解
1、概述 Linux三剑客:grep、sed、awk。grep主打查找功能,sed主要是编辑行,awk主要是分割列处理。本篇文章我们详细介绍awk命令。 awk其名称得自于它的创始人 Alfred Aho 、Peter Weinberger 和 Brian Kernighan 姓氏的首个字母。awk是一种编…...
C++异常处理:掌握高效、健壮代码的秘密武器
C异常处理全面解析:底层原理、编译器技巧与实用场景C异常机制:让我们迈向更安全、更可靠的代码C异常处理:掌握基本概念什么是异常?异常处理的重要性C异常处理的组成部分:try、catch、throw探索C异常处理的核心…...

Jetpack Compose基础组件之按钮组件
概述 按钮组件Button是用户和系统交互的重要组件之一,它按照Material Design风格实现,我们先看下Button的参数列表,通过参数列表了解下Button的整体功能 Composable fun Button(onClick: () -> Unit, // 点击按钮时的回调modifier: Modi…...

利用json-server快速在本地搭建一个JSON服务
1,json-server介绍 一个在前端本地运行,可以存储json数据的server。 通俗来说,就是模拟服务端接口数据,一般用在前后端分离后,前端人员可以不依赖API开发,而在本地搭建一个JSON服务,自己产生测…...

可重入函数与线程安全
指令乱序和线程安全 先来看什么是指令乱序问题以及为什么有指令乱序。程序的代码执行顺序有可能被编译器或CPU根据某种策略打乱指令执行顺序,目的是提升程序的执行性能,让程序的执行尽可能并行,这就是所谓指令乱序问题。理解指令乱序的策略是…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...

vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...