文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

SpringBoot中@Async引起循环依赖的示例分析

2023-06-29 02:50

关注

这篇文章将为大家详细讲解有关SpringBoot中@Async引起循环依赖的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

事故时间线

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

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

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

SpringBoot中@Async引起循环依赖的示例分析

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

SpringBoot中@Async引起循环依赖的示例分析

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

猜想

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

什么是循环依赖

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

几个Bean之间的循环依赖

@Componentpublic class A {    @Autowired    private B b;}@Componentpublic class B {    @Autowired    private C c;}@Componentpublic class C {    @Autowired    private A a;}

效果图如下:

SpringBoot中@Async引起循环依赖的示例分析

自己依赖自己

@Componentpublic class A {    @Autowired    private A a;}

效果图如下:

SpringBoot中@Async引起循环依赖的示例分析

Spring是如何解决循环依赖的

SpringBoot中@Async引起循环依赖的示例分析

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

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中异步执行。

SpringBoot中@Async引起循环依赖的示例分析


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

@Async("asyncTaskExecutor")

被@Async标记的bean注入时机

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

SpringBoot中@Async引起循环依赖的示例分析

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

// 这个map用来缓存所有被postProcessAfterInitialization这个方法处理的beanprivate final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);// 这个方法主要是为打了@Async注解的bean生成代理对象@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {// 这里是重点,这里返回trueif (isEligible(bean, beanName)) {// 工厂模式生成一个proxyFactoryProxyFactory 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,也就是切面,直接返回falseif (this.advisor == null) {return false;}// 这里判断AsyncAnnotationAdvisor能否切入,因为我们的bean是打了@Aysnc注解,这里是一定能切入的,最终会返回trueeligible = AopUtils.canApply(this.advisor, targetClass);this.eligibleBeans.put(targetClass, eligible);return eligible;}

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

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

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

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是一个代理对象,就会增加到actualDependentBeansfor (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;}

解决循环依赖的正确姿势

关于“SpringBoot中@Async引起循环依赖的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯