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


    一.前言

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

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

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

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

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

    二.bug缘由

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

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

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

    然后项目一启动就报错了

    1. 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.
    2. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:623)
    3. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
    4. at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
    5. at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    6. at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
    7. at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    8. at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
    9. at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1307)
    10. at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
    11. at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:657)
    12. ... 26 common frames omitted

    这个其实我当时还挺诧异的,使用@Async,互相注入bean会导致循环依赖。

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

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

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

    三.bug定位

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

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

    exposedObject != bean

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

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

    1. protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    2. Object exposedObject = bean;
    3. if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    4. for (BeanPostProcessor bp : getBeanPostProcessors()) {
    5. if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    6. SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
    7. exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
    8. }
    9. }
    10. }
    11. return exposedObject;
    12. }

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

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

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

    1. exposedObject = initializeBean(beanName, exposedObject, mbd);

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

    1. protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
    2. if (System.getSecurityManager() != null) {
    3. AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
    4. invokeAwareMethods(beanName, bean);
    5. return null;
    6. }, getAccessControlContext());
    7. }
    8. else {
    9. invokeAwareMethods(beanName, bean);
    10. }
    11. Object wrappedBean = bean;
    12. //后置处理器前置处理
    13. if (mbd == null || !mbd.isSynthetic()) {
    14. wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    15. }
    16. try {
    17. invokeInitMethods(beanName, wrappedBean, mbd);
    18. }
    19. catch (Throwable ex) {
    20. throw new BeanCreationException(
    21. (mbd != null ? mbd.getResourceDescription() : null),
    22. beanName, "Invocation of init method failed", ex);
    23. }
    24. //后置处理器后置处理
    25. if (mbd == null || !mbd.isSynthetic()) {
    26. wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    27. }
    28. return wrappedBean;
    29. }

    分别打断点在这里

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

    看到曙光,再进去看看

    1. @Override
    2. public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
    3. throws BeansException {
    4. Object result = existingBean;
    5. for (BeanPostProcessor processor : getBeanPostProcessors()) {
    6. Object current = processor.postProcessAfterInitialization(result, beanName);
    7. if (current == null) {
    8. return result;
    9. }
    10. result = current;
    11. }
    12. return result;
    13. }

    断点一打

    逐个排查

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

    作者:柏炎
    原文链接:spring循环依赖-不仅仅是八股文 - 掘金

  • 相关阅读:
    Word控件Spire.Doc 【表单域】教程(四):如何在 C#、VB.NET 中删除自定义属性字段
    js 模糊定位标签元素
    .NET6: 开发基于WPF的摩登三维工业软件 (7)
    基于球向量的粒子群优化(SPSO)算法在无人机路径规划中的实现(Matlab代码实现)
    实际项目中如何进行问题排查
    基于Java+SpringBoot+vue实现图书借阅和销售商城一体化系统
    算法笔记-第十章-图的定义和相关术语
    【UCIe】UCIe Data to Clock
    线上靶机prompt.ml
    Spring Cloud Gateway核心之Predicate
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/125909291