以下是我简化后的类之间大体的依赖关系,工程内实际的依赖情况会比这个简化版本复杂一些。

@RestController
public class OldCenterSpuController {
    @Resource
    private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
@RestController
public class TimeoutNotifyController {
    @Resource
    private SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;
}
@Component
public class NewSpuApplyCheckServiceImpl {
    @Resource
    private SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;
}
@Component
@Slf4j
@Validated
public class SpuCheckDomainServiceImpl {
    @Resource
    private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}

从代码看,主要是 SpuCheckDomainServiceImpl 和 NewSpuApplyCheckServiceImpl 构成了一个依赖环。而我们从正常启动的 bean 加载顺序发现首先是从 OldCenterSpuController 开始加载的,具体情况如下所示:

OldCenterSpuController 
    ↓ (依赖)
NewSpuApplyCheckServiceImpl 
    ↓ (依赖)  
SpuCheckDomainServiceImpl 
    ↓ (依赖)
NewSpuApplyCheckServiceImpl 

异常启动的情况 bean 加载是从 TimeoutNotifyController 开始加载的,具体情况如下所示:

TimeoutNotifyController 
    ↓ (依赖)
SpuCheckDomainServiceImpl 
    ↓ (依赖)  
NewSpuApplyCheckServiceImpl 
    ↓ (依赖)
SpuCheckDomainServiceImpl 

同一个依赖环,为什么从 OldCenterSpuController 开始加载就可以正常启动,而从 TimeoutNotifyController 启动就会启动异常呢?下面我们会从现场 debug 的角度来分析解释这个问题。

3.2 问题分析

在相关知识点简介里面知悉到 spring 用三级缓存解决了循环依赖问题。为什么后台服务 admin 启动还会报循环依赖的问题呢?

要得到问题的答案,还是需要回到源码本身,前面我们分析了 spring 的创建 Bean 的主要流程,这里为了更好的分析问题,补充下通过容器获取 Bean 的。

在通过 spring 容器获取 bean 时,底层统一会调用 doGetBean 方法,大体如下:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
       @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
    
    final String beanName = transformedBeanName(name);
    Object bean;
    
    // 从三级缓存获取bean
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
       bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }else {
     if (mbd.isSingleton()) {
       sharedInstance = getSingleton(beanName, () -> {
       try {
         //如果是单例Bean,从三级缓存没有获取到bean,则执行创建bean逻辑
          return createBean(beanName, mbd, args);
       }
       catch (BeansException ex) {
          destroySingleton(beanName);
          throw ex;
       }
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }   
 }

从 doGetBean 方法逻辑看,在 spring 从一二三级缓存获取 bean 返回空时,会调用 createBean 方法去场景 bean,createBean 方法底层主要是调用前面我们提到的创建 Bean 流程的 doCreateBean 方法。

注意:doGetBean 方法里面 getSingleton 方法的逻辑是先从一级缓存拿,拿到为空并且 bean 在创建中则又从二级缓存拿,二级缓存拿到为空 并且当前容器允许有循环依赖则从三级缓存拿。并且将对象工厂移到二级缓存,删除三级缓存

doCreateBean 方法如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
          throws BeanCreationException {
    if (mbd.isSingleton()) {
      instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    // Bean初始化第一步:默认调用无参构造实例化Bean
    // 如果是只有带参数的构造方法,构造方法里的参数依赖注入,就是发生在这一步
    if (instanceWrapper == null) {
      instanceWrapper = createBeanInstance(beanName, mbd, args);
    }


    //判断Bean是否需要提前暴露对象用来解决循环依赖,需要则启动spring三级缓存
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
       isSingletonCurrentlyInCreation(beanName));
   if (earlySingletonExposure) {
     if (logger.isTraceEnabled()) {
       logger.trace("Eagerly caching bean '" + beanName +
             "' to allow for resolving potential circular references");
      }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}


    // Initialize the bean instance.
    Object exposedObject = bean;
    try {
      // bean创建第二步:填充属性(DI依赖注入发生在此步骤)
      populateBean(beanName, mbd, instanceWrapper);
      // bean创建第三步:调用初始化方法,完成bean的初始化操作(AOP的第三个入口)
      // AOP是通过自动代理创建器AbstractAutoProxyCreator的postProcessAfterInitialization()
//方法的执行进行代理对象的创建的,AbstractAutoProxyCreator是BeanPostProcessor接口的实现
      exposedObject = initializeBean(beanName, exposedObject, mbd);




   if (earlySingletonExposure) {
    Object earlySingletonReference = getSingleton(beanName, false);
     if (earlySingletonReference != null) {
        if (exposedObject == bean) {
          exposedObject = earlySingletonReference;
        }
        else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
          String[] dependentBeans = getDependentBeans(beanName);
          Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
          for (String dependentBean : dependentBeans) {
             if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                actualDependentBeans.add(dependentBean);
             }
          }
          if (!actualDependentBeans.isEmpty()) {
             throw new BeanCurrentlyInCreationException(beanName,
                   "Bean with name '" + beanName + "' has been injected into other beans [" +
                   StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                   "] 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 " +
                   "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
          }
       }
    }
}


    } catch (Throwable ex) {
      // ...
    }
    // ...
    return exposedObject;
    }

将 doGetBean 和 doCreateBean 的逻辑转换成流程图如下:

从流程图可以看出,后台服务 admin 启动失败抛出 UnsatisfiedDependencyException 异常的必要条件是存在循环依赖,因为不存在循环依赖的情况 bean 只会存在单次加载,单次加载的情况 bean 只会被放进 spring 的第三级缓存。

而触发 UnsatisfiedDependencyException 异常的先决条件是需要 spring 的第一二级缓存有当前的 bean。所以可以知道当前 bean 肯定存在循环依赖。在存在循环依赖的情况下,当前 bean 被第一次获取(即调用 doGetBean 方法)会缓存进 spring 的第三级缓存,然后会注入当前 bean 的依赖(即调用 populateBean 方法),在当前 bean 所在依赖环内其他 bean 都不在一二级缓存的情况下,会触发当前 bean 的第二次获取(即调用 doGetBean 方法),由于第一次获取已经将 Bean 放进了第三级缓存,spring 会将 Bean 从第三级缓存移到二级缓存并删除第三级缓存。

最终会回到第一次获取的流程,调用初始化方法做初始化。最终在初始化有对当前 bean 做代理增强的并且提前暴露到二级缓存的对象有被其他依赖引用到,而且 allowRawInjectionDespiteWrapping=false 的情况下,会导致抛出 UnsatisfiedDependencyException,进而导致启动异常。

注意:在注入当前 bean 的依赖时,这里 spring 将 Bean 从第三级缓存移到二级缓存并删除第三级缓存后,当前 bean 的依赖的其他 bean 会从二级缓存拿到当前 bean 做依赖。这也是后续抛异常的先决条件

结合 admin 有时候启动正常,有时候启动异常的情况,这里猜测启动正常和启动异常时 bean 加载顺序不一致,进而导致启动正常时当前 Bean 只会被获取一次,启动异常时当前 bean 会被获取两次。为了验证猜想,我们分别针对启动异常和启动正常的 bean 获取做了 debug。

debug 分析

首先我们从启动异常提取到以下关键信息,从这些信息可以知道是 spuCheckDomainServiceImpl 的加载触发的启动异常。所以我们这里以 spuCheckDomainServiceImpl 作为前面流程分析的当前 bean。

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'timeoutNotifyController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'spuCheckDomainServiceImpl': Bean with name 'spuCheckDomainServiceImpl' has been injected into other beans [...] 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 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

然后提前我们在 doCreateBean 方法设置好 spuCheckDomainServiceImpl 加载时的条件断点。我们先 debug 启动异常的情况。最终断点信息如下:

从红框 1 里面的两个引用看,很明显调 initializeBean 方法时 spring 有对 spuCheckDomainServiceImpl 做代理增强。导致 initializeBean 后返回的引用和提前暴露到二级缓存的引用是不一致的。这里 spuCheckDomainServiceImpl 有二级缓存是跟我们前面分析的吻合,是因为 spuCheckDomainServiceImpl 被获取了两次,即调了两次 doGetBean。

从红框 2 里面的 actualDependentBeans 的 set 集合知道提前暴露到二级缓存的引用有被其他 33 个 bean 引用到,也是跟异常提示的 bean 列表保持一致的。

这里 spuCheckDomainServiceImpl 的加载为什么会调用两次 doGetBean 方法呢?

从调用栈分析到该加载链如下:

TimeoutNotifyController  ->spuCheckDomainServiceImpl-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl

TimeoutNotifyController 注入依赖时第一次调用 doGetBean 获取 spuCheckDomainServiceImpl 时,从一二三级缓存获取不到,会调用 doCreateBean 方法创建 spuCheckDomainServiceImpl。

首先会将 spuDomainServiceImpl 放进 spring 的第三级缓存,然后开始调 populateBean 方法注入依赖,由于在循环中间的 newSpuApplyCheckServiceImpl 是第一次获取,一二三级缓存都获取不到,会调用 doCreateBean 去创建对应的 bean,然后会第二次调用 doGetBean 获取 spuCheckDomainServiceImpl,这时 spuCheckDomainServiceImpl 在第一次获取已经将 bean 加载到第三级缓存,所以这次 spring 会将 bean 从第三级缓存直接移到第二级缓存,并将第三级缓存里面的 spuCheckDomainServiceImpl 对应的 bean 删除,并直接返回二级缓存里面的 bean,不会再调 doCreateBean 去创建 spuCheckDomainServiceImpl。最终完成了循环中间的 bean 的初始化后(这里循环中间的 bean 初始化时依赖到的 bean 如果有引用到 spuCheckDomainServiceImpl 会调用 doGetBean 方法从二级缓存拿到 spuCheckDomainServiceImpl 提前暴露的引用),会回到第一次调用 doGetBean 获取 spuCheckDomainServiceImpl 时调用的 doCreateBean 方法的流程。继续调 initializeBean 方法完成初始化,然后将初始化完成的 bean 返回。最终拿初始化返回的 bean 引用跟二级缓存拿到的 bean 引用做对比,发现不一致,导致抛出 UnsatisfiedDependencyException 异常。

那么这里为什么 spuCheckDomainServiceImpl 调用 initializeBean 方法完成初始化后与提前暴露到二级缓存的 bean 会不一致呢?

看 spuCheckDomainServiceImpl 的代码如下:

@Component
@Slf4j
@Validated
public class SpuCheckDomainServiceImpl {
    @Resource
    private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}

发现 SpuCheckDomainServiceImpl 类有使用到 @Validated 注解。查阅资料发现 @Validated 的实现是通过在 initializeBean 方法里面执行一个 org.springframework.validation.beanvalidation.MethodValidationPostProcessor 后置处理器实现的,MethodValidationPostProcessor 会对 SpuCheckDomainServiceImpl 做一层代理。导致 initializeBean 方法返回的 spuCheckDomainServiceImpl 是一个新的代理对象,从而最终导致跟二级缓存的不一致。

debug 视图如下:

那为什么有时候能启动成功呢?什么情况下能启动成功?

我们继续 debug 启动成功的情况。最终观察到 spuCheckDomainServiceImpl 只会调用一次 doGetBean,而且从一二级缓存拿到的 spuCheckDomainServiceImpl 提前暴露的引用为 null,如下图:

这里为什么 spuCheckDomainServiceImpl 只会调用一次 doGetBean 呢?

首先我们根据调用栈整理到当前加载的引用栈:

oldCenterSpuController-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl -> newSpuApplyCheckServiceImpl

根据前面启动失败的信息我们可以知道,spuCheckDomainServiceImpl 处理依赖的环是:

spuCheckDomainServiceImpl ->newSpuApplyCommandServiceImpl-> ... ->spuCheckDomainServiceImpl

失败的情况我们发现是从 spuCheckDomainServiceImpl 开始创建的,现在启动正常的情况是从 newSpuApplyCheckServiceImpl 开始创建的。

创建 newSpuApplyCheckServiceImpl 时,发现它依赖环中间这些 bean 会依次调用 doCreateBean 方法去创建对应的 bean。

调用到 spuCheckDomainServiceImpl 时,由于是第一次获取 bean,也会调用 doCreateBean 方法创建 bean,然后回到创建 spuCheckDomainServiceImpl 的 doCreateBean 流程,这里由于没有将 spuCheckDomainServiceImpl 的三级缓存移到二级缓存,所以不会导致抛出 UnsatisfiedDependencyException 异常,最终回到 newSpuApplyCheckServiceImpl 的 doCreateBean 流程,由于 newSpuApplyCheckServiceImpl 在调用 initializeBean 方法没有做代理增强,所以也不会导致抛出 UnsatisfiedDependencyException 异常。因此最后可以正常启动。

这里我们会有疑问?类的创建顺序由什么决定的呢?

通常不同环境下,代码打包后的 jar/war 结构、@ComponentScan 的 basePackages 配置细微差别,都可能导致 Spring 扫描和注册 Bean 定义的顺序不同。Java ClassLoader 加载类的顺序本身也有一定不确定性。如果 Bean 定义是通过不同的配置类引入的,配置类的加载顺序会影响其中所定义 Bean 的注册顺序。

那是不是所有的类增强在有循环依赖时都会触发 UnsatisfiedDependencyException 异常呢?

并不是,比如 @Transactional 就不会导致触发 UnsatisfiedDependencyException 异常。让我们深入分析原因。

核心区别在于代理创建时机不同。

@Transactional 的代理时机如下:

// Spring 为 @Transactional 创建代理的流程1. 实例化原始 Bean
2. 放入三级缓存(ObjectFactory)
3. 当发生循环依赖时,调用 ObjectFactory.getObject()
4. 此时判断是否需要事务代理,如果需要则提前创建代理
5. 将代理对象放入二级缓存,供其他 Bean 使用

@Validated 的代理时机:

// @Validated 的代理创建在生命周期更晚的阶段1. 实例化原始 Bean
2. 放入三级缓存(ObjectFactory)
3. 当发生循环依赖时,调用 ObjectFactory.getObject()
4.  ❌ 问题:此时 @Validated 的代理还未创建!
5. 其他 Bean 拿到的是原始对象,而不是异步代理对象

问题根源:@Transactional 的代理增强是在三层缓存生成时触发的, @Validated 的增强是在初始化 bean 后通过后置处理器做的代理增强。

3.3 解决方案

短期方案

  • 移除 SpuCheckDomainServiceImpl 类上的 Validated 注解
  • @lazy 解耦
    • 原理是发现有 @lazy 注解的依赖为其生成代理类,依赖代理类,只有在真正需要用到对象时,再通过 getBean 的逻辑去获取对象,从而实现了解耦。

长期方案

严格执行 DDD 代码规范

这里是违反 DDD 分层规范导致的循环依赖。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐