深度解析SpringBoot中@Async引起的循环依赖

目录
  • 事故时间线
  • 猜想
  • 什么是循环依赖
  • 什么是@Async

啊,昨晚发版又出现了让有头大的循环依赖问题,按理说Spring会为我们解决循环依赖,但是为什么还会出现这个问题呢?为什么在本地、UAT以及PRE环境都没有出现这个问题,但是到了PROD环境就出现了这个问题呢?本文将从事故时间线、及时止损、复盘分析等几个方面为大家带来详细的分析,干货满满!

事故时间线

本着"先止损、后复盘分析"的原则,我们来看一下这次发版事故的时间线。

2021年11月16日晚23点00分00秒开始发版,此时集团的devops有点慢

2021年11月16日晚23点03分01秒,收到发版失败的消息,登录服务器发现发生了循环依赖,具体错误如下图,从日志中可以看到是dataCollectionSendMessageService这个bean出现了循环依赖

问题发现了就需要先解决,然后再去分析为什么。看到这个报错日志我心里也大概知道是为什么了,所以很快就解决了,解决方案如下:给DataCollectionSendMessageService加上@Lazy注解

2021年11月16日晚23点07分16秒,使用重新集成的代码开始发版,大概10分钟后线上节点全部发版完成。从时间线来看从发现问题到解决问题,前后一共用了接近15分钟(这期间代码集成和发布用了过多的时间),也算是做到了及时止损,没有让问题继续扩大。

猜想

我大胆的猜想是因为打了@Aysnc注解的bean生成了对象的代理,导致Spring bean最终加载的不是一个原始对象导致了此次问题的发生,那么对不对呢,接下来我们通过源码详细分析一下。

什么是循环依赖

所谓循环依赖就是Spring IOC容器在加载bean时会按照顺序加载,先去实例化 beanA。然后发现 beanA 依赖于 beanB,接在又去实例化 beanB。实例化 beanB 时,发现 beanB 又依赖于 beanA。如果容器不处理循环依赖的话,容器会无限执行上面的流程,直到内存溢出,程序崩溃,所以这个时候就会抛出BeanCurrentlyInCreationException异常,也就是我们常说的循环依赖,下面是两种常见循环依赖的场景。

几个Bean之间的循环依赖

@Component
public class A {

    @Autowired
    private B b;
}

@Component
public class B {

    @Autowired
    private C c;
}

@Component
public class C {

    @Autowired
    private A a;
}

效果图如下:

自己依赖自己

@Component
public class A {

    @Autowired
    private A a;
}

效果图如下:

Spring是如何解决循环依赖的

首先Spring维护了三个Map,也就是我们通常说的三级缓存

  • singletonObjects:俗称单例池,缓存创建完成的单例Bean
  • singletonFactories:映射创建Bean的原始工厂
  • earlySingletonObjects:映射Bean的早期引用,也就是说这个Map里的Bean不是完整的,只是完成了实例化,但还没有初始化

Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。

当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象

当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。

紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

简单一句话说:先去缓存里找Bean,没有则实例化当前的Bean放到Map,如果有需要依赖当前Bean的,就能从Map取到。

什么是@Async

@Async注解是Spring为我们提供的异步调用的注解,@Async可以作用到类或者方法上,标记了@Async注解的方法将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。从源码中可以看到标记了@Async注解的方法会被提交到org.springframework.core.task.TaskExecutor中异步执行。

或者我们可以通过value来指定使用哪个自定义线程池,比如这样子:

@Async("asyncTaskExecutor")

被@Async标记的bean注入时机

我们从源码的角度来看一下被@Async标记的bean是如何注入到Spring容器里的。在我们开启@EnableAsync注解之后代表可以向Spring容器中注入AsyncAnnotationBeanPostProcessor,它是一个后置处理器,我们看一下他的类图。

真正创建代理对象的代码在AbstractAdvisingBeanPostProcessor中的postProcessAfterInitialization方法中,以下代码有所删减,只保留核心逻辑代码

	// 这个map用来缓存所有被postProcessAfterInitialization这个方法处理的bean
	private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);

	// 这个方法主要是为打了@Async注解的bean生成代理对象
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		// 这里是重点,这里返回true
		if (isEligible(bean, beanName)) {
			// 工厂模式生成一个proxyFactory
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			if (!proxyFactory.isProxyTargetClass()) {
				evaluateProxyInterfaces(bean.getClass(), proxyFactory);
			}
			// 切入切面并创建一个代理对象
			proxyFactory.addAdvisor(this.advisor);
			customizeProxyFactory(proxyFactory);
			return proxyFactory.getProxy(getProxyClassLoader());
		}
		// No proxy needed.
		return bean;
	}
	protected boolean isEligible(Class<?> targetClass) {
		// 首次从eligibleBeans这个map中一定是拿不到的
		Boolean eligible = this.eligibleBeans.get(targetClass);
		if (eligible != null) {
			return eligible;
		}
		// 如果没有advisor,也就是切面,直接返回false
		if (this.advisor == null) {
			return false;
		}
		// 这里判断AsyncAnnotationAdvisor能否切入,因为我们的bean是打了@Aysnc注解,这里是一定能切入的,最终会返回true
		eligible = AopUtils.canApply(this.advisor, targetClass);
		this.eligibleBeans.put(targetClass, eligible);
		return eligible;
	}

至此打了@Aysnc注解的bean就创建完成了,结果是生成了一个代理对象

循环依赖到底是怎么生成的

经过上面的源码分析,我们可以知道有@Aysnc注解的bean最后生成了一个代理对象,我们结合Spring bean创建的流程来分析这次问题。

  • beanA开始初始化,beanA实例化完成后给beanA的依赖属性beanB进行赋值
  • beanB开始初始化,beanB实例化完成后给beanB的依赖属性beanA进行赋值
  • 因为beanA是支持循环依赖的,所以可以在earlySingletonObjects中可以拿到beanA的早期引用的,但是因为beanB打了@Aysnc注解并不能在earlySingletonObjects中可以拿到早期引用
  • 接下来执行执行initializeBean(Object existingBean, String beanName)方法,这里beanA可以正常实例化完成,但是因为beanB打了@Aysnc注解,所以向Spring IOC容器中增加了一个代理对象,也就是说beanAbeanB并不是一个原始对象,而是一个代理对象
  • 接下来进行执行doCreateBean方法时对进行检测,以下代码有所删减,只保留核心逻辑代码
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
			throws BeanCreationException {

		if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
					// 重点在这里,这里会遍历所有依赖的bean,如果beanA依赖beanB和缓存中的beanB不相等
					// 也就是说beanA本来依赖的是一个原始对象beanB,但是这个时候发现beanB是一个代理对象,就会增加到actualDependentBeans
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					// 发现actualDependentBeans不为空,就发生了我们最开始截图的错误
					if (!actualDependentBeans.isEmpty()) {
						throw new BeanCurrentlyInCreationException(beanName,
								"Bean with name '" + beanName + "' has been injected into other beans [" +
								StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
								"] 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.");
					}
				}
			}
		}

		// Register bean as disposable.
		try {
			registerDisposableBeanIfNecessary(beanName, bean, mbd);
		}
		catch (BeanDefinitionValidationException ex) {
			throw new BeanCreationException(
					mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
		}

		return exposedObject;
	}

解决循环依赖的正确姿势

  • @Lazy注解
  • 代码优化,不要让@Async的Bean参与循环依赖

至此我们就知道为什么发生了此次问题。

到此这篇关于深度解析SpringBoot中@Async引起的循环依赖的文章就介绍到这了,更多相关SpringBoot中@Async循环依赖内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 带有@Transactional和@Async的循环依赖问题的解决

    今天我们来探讨一个有意思的spring源码问题,也是一个学生告诉了我现象我从源码里面找到了这个有意思的问题. 首先我们看service层的代码案例,如下: @Service("transationServiceImpl") public class TransationServiceImpl implements TransationService { @Autowired TransationService transationService; @Transactional @Asy

  • 深度解析SpringBoot中@Async引起的循环依赖

    目录 事故时间线 猜想 什么是循环依赖 什么是@Async 啊,昨晚发版又出现了让有头大的循环依赖问题,按理说Spring会为我们解决循环依赖,但是为什么还会出现这个问题呢?为什么在本地.UAT以及PRE环境都没有出现这个问题,但是到了PROD环境就出现了这个问题呢?本文将从事故时间线.及时止损.复盘分析等几个方面为大家带来详细的分析,干货满满! 事故时间线 本着"先止损.后复盘分析"的原则,我们来看一下这次发版事故的时间线. 2021年11月16日晚23点00分00秒开始发版,此时集

  • springboot中@Async默认线程池导致OOM问题

    前言: 1.最近项目上在测试人员压测过程中发现了OOM问题,项目使用springboot搭建项目工程,通过查看日志中包含信息:unable to create new native thread 内存溢出的三种类型: 1.第一种OutOfMemoryError: PermGen space,发生这种问题的原意是程序中使用了大量的jar或class 2.第二种OutOfMemoryError: Java heap space,发生这种问题的原因是java虚拟机创建的对象太多 3.第三种OutOfM

  • Spring处理@Async导致的循环依赖失败问题的方案详解

    目录 简介 问题复现 原因分析 解决方案 方案1:懒加载 方案2:不让@Async的类有循环依赖 方案3:allowRawInjectionDespiteWrapping设置为true 为什么@Transactional不会导致失败 简介 说明 本文介绍SpringBoot中的@Async导致循环依赖失败的原因及其解决方案. 概述 我们知道,Spring解决了循环依赖问题,但Spring的异步(@Async)会使得循环依赖失败.本文将用实例来介绍其原因和解决方案. 问题复现 启动类 启动类添加@

  • Java中的Spring 如何处理循环依赖

    目录 前言 什么是循环依赖 构造器循环依赖 Setter循环依赖 构造器循环依赖处理 那么Spring到底是如何做的呢? DefaultSingletonBeanRegistry#getSingleton AbstractAutowireCapableBeanFactory#autowireConstructor setter循环依赖处理 AbstractAutowireCapableBeanFactory#doCreateBean prototype模式的循环依赖 总结 前言 Spring如何

  • 深度解析Java中volatile的内存语义实现以及运用场景

    volatile内存语义的实现 下面,让我们来看看JMM如何实现volatile写/读的内存语义. 前文我们提到过重排序分为编译器重排序和处理器重排序.为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型.下面是JMM针对编译器制定的volatile重排序规则表: 举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作. 从上表我们可以看出: 当第二个操作是volatile写时,

  • spring循环依赖策略解析

    循环依赖 所谓循环依赖就是多个Bean之间依赖关系形成一个闭环,例如A->B->C->...->A 这种情况,当然,最简单的循环依赖就是2个Bean之间互相依赖:A->B(A依赖B), B->A(B依赖A) .在Spring中,如果A->B,那么在创建A的过程中会去创建B,在创建B(或B的依赖)的过程中又发现B->A,这个时候就出现了循环依赖的现象. 循环依赖的解决 spring中的循环依赖只有当 1.Bean是单例, 2.通过属性注入的情况 这两个条件满足

  • 关于SpringBoot禁止循环依赖解说

    前言: Spring的Bean管理,一直是整个体系中津津乐道的东西.尤其是Bean的循环依赖,更是很多面试官最喜欢考察的2B知识点之一. 但事实上,项目中存在Bean的循环依赖,是代码质量低下的表现.多数人寄希望于框架层来给擦屁股,造成了整个代码的设计越来越糟,最后用一些奇技淫巧来填补犯下的错误. 还好,SpringBoot终于受不了这种滥用,默认把循环依赖给禁用了! 从2.6版本开始,如果你的项目里还存在循环依赖,SpringBoot将拒绝启动! 验证代码小片段: 为了验证这个功能,我们只需要

  • springboot中pom.xml文件注入test测试依赖时报错的解决

    目录 pom.xml文件注入test测试依赖时报错 分析原因 解决方法 springboot中pom.xml之间的依赖 依赖关系 所用到的技术 talkischeap,详见配置文件 pom.xml文件注入test测试依赖时报错 报错:Failed to read artifact descriptor for org.springframework.boot:spring-boot-starter-test:jar:2.0.4.RELEASE 分析原因 有可能是默认版本太高 解决方法 降低版本

  • spring boot启动时mybatis报循环依赖的错误(推荐)

    自己在做项目时,想使用热部署减少部署时间,于是添加了springboot-devtools 在maven中添加了依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> 然后正常的启动项目时发现控制台一直在不停的输出错误,错误如图 不明所以,然后就准备去调

  • Spring源码剖析之Spring处理循环依赖的问题

    前言 你是不是被这个骚气的标题吸引进来的,_ 喜欢我的文章的话就给个好评吧,你的肯定是我坚持写作最大的动力,来吧兄弟们,给我一点动力 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题,在前面Bean实例化流程中,对属性注入一文多多少少对循环依赖有过介绍,这篇文章详细讲一下Spring中的循环依赖的处理方案. 什么是循环依赖 依赖指的是Bean与Bean之间的依赖关系,循环依赖指的是两个或者多个Bean相互依赖,如: 构造器循环依赖 代码示例: public class BeanA

随机推荐