一次离谱的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

注意

  • 循环依赖本身就是代码没有写好的情况,这种借助事件机制来逃避循环依赖本身就是一种能力不足的体现
相关推荐
2401_8574396924 分钟前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧66625 分钟前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
哎呦没4 小时前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_857600954 小时前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
NiNg_1_2348 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
种树人202408198 小时前
如何在 Spring Boot 中启用定时任务
spring boot
苹果醋311 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
Wx-bishekaifayuan11 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer0811 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源