前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >spring循环依赖-不仅仅是八股文

spring循环依赖-不仅仅是八股文

作者头像
柏炎
发布2022-08-23 14:44:01
4360
发布2022-08-23 14:44:01
举报
文章被收录于专栏:深入浅出java后端

一.前言

spring的循环依赖问题是面试的时候经常会碰到的问题。相信很多朋友都看相关spring使用三级缓存解决循环依赖的博文。面试官问你的时候除了想要了解你对spring框架的熟悉程度,还想要了解你对spring循环依赖的思考。

你上来直接说spring使用了三级缓存解决了循环依赖,那你就要回家等通知了。

前几天写需求的时候,整合了几个方法逻辑的时候,碰到了一个循环依赖的bug。

借着这个bug的排查思路给大家讲讲spring循环依赖中几个小坑。

本文重点并非spring循环依赖源码解读,默认你对spring循环依赖有过简单的了解。 我贴心点吧,贴一下大神A哥的blog:一文告诉你Spring是如何利用“三级缓存“巧妙解决Bean的循环依赖问题的

二.bug缘由

博主在进行一个需求开发的时候,需要调用几个现有接口的逻辑,但是它们原先的方法是私有的,并且好几个逻辑定义在controller层【历史原因,强烈谴责此种做法!】

为了方法复用,我将controller中对应通用逻辑进行剥离同步到对应的service中,并且注入了相关依赖的一些bean。

然后代码结构变成了,serviceA注入了serviceB,serviceB注入了serviceA

然后项目一启动就报错了

代码语言:javascript
复制
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'appManagerServiceImpl': Bean with name 'appManagerServiceImpl' has been injected into other beans [deviceManagerServiceImpl] 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 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
​
​
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:623)
  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
  at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1307)
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
  at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:657)
  ... 26 common frames omitted

这个其实我当时还挺诧异的,我在不看绝对后悔的@Async深度解析【不仅仅是源码那么简单】这篇文章中4.3提到过使用@Async,互相注入bean会导致循环依赖。

但是我在这两个bean中全局搜索@Aysnc,也没有搜索到。

然后也没有看到构造器注入的场景。

so,看来还是只能调一下源码吧。

三.bug定位

看到这个bug,直接定位到堆栈报出来的错误行

报错比较简单直观,暴露对象与关联的bean不是同一个对象,在这里打一个条件断点:

exposedObject != bean

发现bean是原始对象,exposedObject是代理对象。

借用一下A哥的图

spring解决循环依赖时,beanB去获取beanA时,beanA如果切面处理,那么beanB关联beanA时,会调用

代码语言:javascript
复制
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
   Object exposedObject = bean;
   if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
         if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
         }
      }
   }
   return exposedObject;
}

生成代理对象,将beanA从三级缓存中删除,生成代理对象放置到二级缓存中。

但是由于getEarlyBeanReference方法中仅对类型为SmartInstantiationAwareBeanPostProcessor的后置处理器进行代理处理。如果是其他的类型的BeanPostProcessor,将不会在此处做增强。

ok,我们再回过头看一下上面的流程图,bean加载最后的逻辑在

代码语言:javascript
复制
exposedObject = initializeBean(beanName, exposedObject, mbd);

这一行,最后处理bean的逻辑

代码语言:javascript
复制
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
   if (System.getSecurityManager() != null) {
      AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
         invokeAwareMethods(beanName, bean);
         return null;
      }, getAccessControlContext());
   }
   else {
      invokeAwareMethods(beanName, bean);
   }
​
   Object wrappedBean = bean;
   //后置处理器前置处理
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
   }
​
   try {
      invokeInitMethods(beanName, wrappedBean, mbd);
   }
   catch (Throwable ex) {
      throw new BeanCreationException(
            (mbd != null ? mbd.getResourceDescription() : null),
            beanName, "Invocation of init method failed", ex);
   }
   //后置处理器后置处理
   if (mbd == null || !mbd.isSynthetic()) {
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
   }
​
   return wrappedBean;
}

分别打断点在这里

能够看到这两行的wrappedBean对象不一样,一个是原始对象,换一个是代理对象。

看到曙光,再进去看看

代码语言:javascript
复制
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
      throws BeansException {
​
   Object result = existingBean;
   for (BeanPostProcessor processor : getBeanPostProcessors()) {
      Object current = processor.postProcessAfterInitialization(result, beanName);
      if (current == null) {
         return result;
      }
      result = current;
   }
   return result;
}

断点一打

逐个排查

发现bean被MethodValidationPostProcessor增强处理了!!!!

点击这个类进去,被标注了@Validated的类将被代理增强。

四.bug解决

bug解决就很简单了,发现在service上标注了 @Validated,为了校验方法入参。

我就简单粗暴,把校验逻辑写了一个方法单独处理。

五.总结

其实这篇文章的排查思路跟@Async那篇文章的排查思路是一模一样的,但是我在排查的时候有增加了不少的思考。

1.spring本身帮助我们解决了属性注入方式的循环依赖。但是如果循环依赖的bean,被除SmartInstantiationAwareBeanPostProcessor的后置处理器代理到,那么还是会产生循环依赖的报错。

2.spring无法为我们解决构造器循环依赖,因为三级缓存的最开始操作就是要对bean实例化放入到三级缓存。

3.使用 @Lazy去解决类似本文的这种bug,是可行的。比如B希望依赖进来的是最终的代理对象进来,所以B加上即可,A上并不需要加。但是实际上,此种情况下B里持有A的引用和Spring容器里的A并不是同一个【本强迫症患者看来,治标不治本】

4.其实二级缓存也能解决注入循环依赖,但是为什么要使用三级缓存?spring还是期望bean的声明周期是符合spring的设计规范的,类似于二级缓存的早期曝光提前生成代理的方式,是为了系统的健壮性考虑。

5.谨慎使用:allowRawInjectionDespiteWrapping,把这个置为true后会针对循环内的bean不进行校验,但是代理会失效了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-11-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.前言
  • 二.bug缘由
  • 三.bug定位
  • 四.bug解决
  • 五.总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档