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

1. 背景介绍

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

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

java 复制代码
@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,由于项目是关闭了处理循环依赖的功能,所以就爆出来了以下的错误

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

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

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

2. 分析问题

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

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

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

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

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

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

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

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

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

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

java 复制代码
public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
      throws BeansException {

   String[] beanNames = getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
   Map<String, T> 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

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

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

3. 总结

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

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

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

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

注意

  • 循环依赖本身就是代码没有写好的情况,这种借助事件机制来逃避循环依赖本身就是一种能力不足的体现
相关推荐
摇滚侠2 小时前
Spring Boot 3零基础教程,WEB 开发 自定义静态资源目录 笔记31
spring boot·笔记·后端·spring
摇滚侠2 小时前
Spring Boot 3零基础教程,WEB 开发 Thymeleaf 遍历 笔记40
spring boot·笔记·thymeleaf
科兽的AI小记3 小时前
市面上的开源 AI 智能体平台使用体验
人工智能·源码·创业
橘子海全栈攻城狮3 小时前
【源码+文档+调试讲解】基于SpringBoot + Vue的知识产权管理系统 041
java·vue.js·人工智能·spring boot·后端·安全·spring
Json_4 小时前
学习springBoot框架-开发一个酒店管理系统,熟悉springboot框架语法~
java·spring boot·后端
kkkkk0211064 小时前
微服务学习笔记(黑马商城)
java·spring boot·spring·spring cloud·sentinel·mybatis·java-rabbitmq
冲鸭ONE5 小时前
新手搭建Spring Boot项目
spring boot·后端·程序员
数智顾问5 小时前
Flink ProcessFunction 与低层级 Join 实战手册:多流广告计费精确去重
java·spring boot·spring
Json____6 小时前
最近我用springBoot开发了一个二手交易管理系统,分享一下实现方式~
java·spring boot·后端
喜欢读源码的小白6 小时前
SpringBoot的启动流程原理——小白的魔法引擎探秘
java·开发语言·spring boot·springboot启动原理