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

·  阅读 1152

一.前言

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

hello,everyone。

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

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

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

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

本文重点并非spring循环依赖源码解读,默认你对spring循环依赖有过简单的了解。

我贴心点吧,贴一下大神A哥的blog:一文告诉你Spring是如何利用“三级缓存“巧妙解决Bean的循环依赖问题的

二.bug缘由

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

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

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

然后项目一启动就报错了

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会导致循环依赖。

image-20211108195622193.png

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

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

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

三.bug定位

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

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

exposedObject != bean

image-20211108195849145.png

image-20211108200421743.png

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

借用一下A哥的图

2019061918335746.png

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

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加载最后的逻辑在

exposedObject = initializeBean(beanName, exposedObject, mbd);
复制代码

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

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;
}
复制代码

分别打断点在这里

image-20211108202317887.png

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

看到曙光,再进去看看

@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;
}
复制代码

断点一打

image-20211108202940526.png

逐个排查

image.png

发现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不进行校验,但是代理会失效了。

六.联系我

文中如有不正确之处,欢迎指正,写文不易,点个赞吧,么么哒~

钉钉:louyanfeng25

微信:baiyan_lou

公众号:柏炎大叔

image.png

分类:
后端
分类:
后端