深度解析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
:俗称单例池,缓存创建完成的单例BeansingletonFactories
:映射创建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容器中增加了一个代理对象,也就是说beanA
的beanB
并不是一个原始对象,而是一个代理对象 - 接下来进行执行
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循环依赖内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!