一次离谱的SpringBoot循环依赖的分析逻辑

2023年 9月 16日 90.7k 0

1. 背景介绍

首先大家应该都明白SpringBoot中循环依赖是怎么产生的,简单来说就是 A引入B, B引入A

故事的发生是这样的,我有一位同事是写了类似于下面这样的代码

@Component
public class OrdersService implements ApplicationContextAware {

   @Resource
   private OrdersGoodsService ordersGoodsService;
   
}

@Component
public class OrdersGoodsService {

   @Resource
   private UserService userService;
   
}

@Component
public class UserService {

   @Resource
   private OrdersService ordersService;
   
}

代码很简单,明显就是 OrdersService -> OrdersGoodsService -> UserService -> OrdersService,由于项目是关闭了处理循环依赖的功能,所以就爆出来了以下的错误

image.png

当然这个同事也是知道这是循环依赖,所以他去谷歌寻求了答案,于是将OrdersService改造了一下,变成了下面这个样子

image.png

然后点击运行,离谱的事情就发生了,控制台没有打印循环依赖的标记,程序还是正常的启动,只是这个map中是没有OrdersGoodsService的实例的

image.png

于是我同事就开始怀疑人生,然后就找上了我,命运的齿轮就此开始转动

image.png

2. 分析问题

由于此时我同事只给我说了通过ApplicationContext获取不到OrdersGoodsService实例,我初步就往实例没有注册到容器中去分析了

于是我自信的来到ConfigurationClassParserdoProcessConfigurationClass方法中开始分析,这个方法是判断@ComponentScans注解有没有扫描到我们的实例

image.png

然后经过我一顿瞎分析后,发现我们的OrdersService, OrdersGoodsService, UserService,都已经转为了BeanDefinition了

image.png

此时我眉头一皱,发现事情有点棘手了,我一想是不是这个OrdersService本身有点问题,然后我去检查检查,发现除了业务代码,也就只有把applicationContext.getBeansOfType(OrdersGoodsService.class);当成@Resource来用,然后形成循环依赖的可能了,但是控制台又没有提示

image.png

这个时候就没办法了,只能看OrdersService是怎么注册的了

SpringBoot默认注册的Bean工厂是DefaultListableBeanFactory,这个时候我们来到其doGetBean(...)方法中,这个方法正是创建Bean的入口方法

此时我通过Debug + 堆栈分析:

  • 程序是先初始化OrdersGoodsService发现有一个依赖需要注入也就是UserService
  • 然后UserService发现也有一个依赖需要注入也就是OrdersService
    • 此时由于SpringBoot会将正在创建的Bean保存起来,也就是说OrdersGoodsServiceUserService对应Bean名称就会在下面的集合中了,这里就是重点,一定要记住
      image.png
  • 然后当OrdersService初始化完成后会执行ApplicationContextAwareProcessor的invokeAwareInterfaces(...)方法
    image.png

最终程序会到OrdersServicesetApplicationContext()中去执行getBeansOfType(...)方法了

image.png

然后我们看最底层的getBeansOfType(...)方法,如果看过这块源码的观众就已经明白了原因了,这里竟然try catch了一个BeanCreationException异常

public  Map getBeansOfType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit)
      throws BeansException {

   String[] beanNames = getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
   Map result = new LinkedHashMap(beanNames.length);
   for (String beanName : beanNames) {
      try {
         Object beanInstance = getBean(beanName);
         if (!(beanInstance instanceof NullBean)) {
            result.put(beanName, (T) beanInstance);
         }
      }
      catch (BeanCreationException ex) {
         Throwable rootCause = ex.getMostSpecificCause();
         if (rootCause instanceof BeanCurrentlyInCreationException) {
            BeanCreationException bce = (BeanCreationException) rootCause;
            String exBeanName = bce.getBeanName();
            if (exBeanName != null && isCurrentlyInCreation(exBeanName)) {
               if (logger.isTraceEnabled()) {
                  logger.trace("Ignoring match to currently created bean '" + exBeanName + "': " +
                        ex.getMessage());
               }
               onSuppressedException(ex);
               // Ignore: indicates a circular reference when autowiring constructors.
               // We want to find matches other than the currently created bean itself.
               continue;
            }
         }
         throw ex;
      }
   }
   return result;
}

然后大家按照我写的方法,进入getBean(...) -> doGetBean(...) -> getSingleton(...) -> beforeSingletonCreation(...)方法

这个方法中的两个集合正是我上面讲的保存了正在创建Bean名称的集合,然后这里就会抛出BeanCurrentlyInCreationException

image.png

然后这个异常就会被getBeansOfType(...)中的try catch捕获到,然后continue, 最终我们得到一个没有实例的Map

分析到这里彻底真相大白了,原来就是这个getBeansOfType(...)方法自己做主,自己捕获了异常,导致异常没有继续往上抛,也就无法在控制台打印日志了

image.png

3. 总结

由于Bean工厂的getBeansOfType(...)方法会自己捕获异常,所以一般情况下就算我们要用Bean工厂获取实例,也可以使用getBean()方法来获取,在这种情况下就会提示循环依赖了

image.png

当然就算是项目中关闭了处理循环依赖的机制,我们依旧可以操作

大家想想,循环依赖是怎么产生的,是在Bean初始化阶段出现的,那如果说当所有Bean初始化完成后才开始进行手动注入呢?

我们可以借助SpringBoot的监听器机制,监听特定的事件,就像下面的代码一样,在容器初始化完毕后才注入OrdersGoodsService

image.png

注意:

  • 循环依赖本身就是代码没有写好的情况,这种借助事件机制来逃避循环依赖本身就是一种能力不足的体现

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论