Spring循环依赖与三级缓存

2023-03-30 19:27:53 浏览数 (1)

常见的循环依赖

代码语言:javascript复制
@Component
public class Aoo {
    @Autowired
    Boo boo;
}

@Component
public class Boo {
    @Autowired
    Aoo aoo;
}
  1. A依赖B B依赖A
  2. 首先判断三级缓存中存不存在A
    1. 不存在则通过DefaultSingletonBeanRegistry#getSingleton(String, ObjectFactory<?>)最终调用AbstractAutowireCapableBeanFactory#createBean(String, RootBeanDefinition, Object[]) 实例化A
    2. 通过三个条件(1. A是否单例 2. 当前容器是否允许循环依赖 3. 是否正在创建A的过程中 DefaultSingletonBeanRegistry#isSingletonCurrentlyInCreation)判断是否先将A放入三级缓存
    3. 若允许, 则先将A以ObjectFactory的形式加入singletonFactories(三级缓存)提前暴露, 再准备填充属性
    4. 发现需要注入依赖B, 进行第3步
  3. 判断三级缓存中存不存在B
    1. 实例化B
    2. 执行与A一样的判断, 放入三级缓存暴露出来, 再准备填充属性
    3. 发现依赖A, 依次判断一/二/三级缓存, 最终从三级缓存中获取到A的ObjectFactory
    4. 调用ObjectFactory#getObject, 获取到对象A, 并将A从三级缓存移除, 加入二级缓存
    5. 将A对象设置进B的属性中, 自身完成初始化, 调用addSingleton将B加入一级缓存
  4. A获取到B对象, 自身完成初始化, 调用getSingleton("aoo", false)获取到二级缓存的对象, 调用addSingleton从二级缓存移除, A加入一级缓存
  • Spring怎么解决的循环依赖 将对象提前暴露在三级缓存中
  • 假如移除掉二级缓存. 先说结论, 是没问题的
    1. 如果A是需要代理的情况下, 上文步骤3d: ObjectFactory#getObject会返回代理A对象, 直接将其存入一级缓存. 然后A再进行初始化, A的初始化阶段不会进行再次代理. 即最终完成品的对象前后是一致的. 因为org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization这个方法利用earlyProxyReferences这个属性判断早期暴露的对象是否已经被代理过了(注意其他BeanPostProcessor可能不会有这样的逻辑, 比如下文将提到的@Async), 如果已经被代理过, 则不再进行代理. 那Spring为什么不使用两级缓存而使用三级, 可能是一二级缓存合并后, 职责会混乱. 一级缓存存储完成品Bean, 二级缓存则是半成品.
    2. 如果将代理逻辑提前到对象实例化后就进行, 那么一级和三级缓存同样可以解决循环依赖问题. 那spring为什么不这么做, 可能是这样违背了Spring在结合AOP跟Bean的生命周期的设计, Spring结合AOP跟Bean的生命周期本身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理
  • 假如移除掉三级缓存, A实例化后直接放入二级缓存
    1. B注入属性时, 无法判断A是否需要代理. 如果硬要改, 当然也是能改的.
  • 假如A不是单例
    1. 则A不允许早期暴露, 则B无法从三级缓存中拿到A, 则循环依赖抛出异常
  • 假如A依赖A, 即自身依赖自身 @Component public class Aoo { @Autowired Aoo aoo; } 实例化A后, 开始注入属性, 发现依赖A, 调用getBean方法获取A对象, 其中getSingleton方法将提前暴露的A对象转移到二级缓存后, 返回注入需要的A对象. A完成属性注入, 完成初始化, 再将A转移到一级缓存, 完成

但即使有三级缓存也无法解决构造器的循环依赖, 对象无法正常实例化, 没有操作的空间

相关方法源码注释

DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

代码语言:javascript复制
// 返回已注册在给定名称下的单例对象(原始)。
// 检查已实例化的单例对象,还允许早期引用当前创建的单例对象(以解决循环引用)
// @beanName 要查找的bean名称
// @allowEarlyReference 是否应创建早期引用
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 判断一级缓存
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null &amp;&amp; isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 判断二级缓存
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null &amp;&amp; allowEarlyReference) {
                // 判断三级缓存
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 从三级缓存中拿到的工厂Bean获取到对象后, 从三级缓存中移除
                    singletonObject = singletonFactory.getObject();
                    // 加入二级缓存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

DefaultSingletonBeanRegistry#addSingleton

代码语言:javascript复制
// 将给定的单例对象添加到当前工厂的单例缓存中 用于立即注册单例对象。
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 加入一级缓存
        this.singletonObjects.put(beanName, singletonObject);
        // 从三级缓存移除
        this.singletonFactories.remove(beanName);
        // 从二级缓存移除
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

另外一个跟循环依赖相关的问题

@Async注解的坑 https://www.cnblogs.com/zzyang/p/16469700.html

总结一句话就是@Async的代理是由AsyncAnnotationBeanPostProcessor实现的, 其他代理比如@Transactional@AspectAnnotationAwareAspectJAutoProxyCreator实现的, AnnotationAwareAspectJAutoProxyCreator调用父类AbstractAutoProxyCreator#postProcessAfterInitialization方法, 里面会判断当前bean是否是被早期暴露的, 如果是, 不进行处理. 但是AsyncAnnotationBeanPostProcessor会直接进行代理, 所以造成前后对象并不一致, 然后抛出异常

代码语言:javascript复制
if (earlySingletonExposure) {
  // 获取到早期暴露出去的对象
  Object earlySingletonReference = getSingleton(beanName, false);
  // 早期暴露的对象不为null, 说明出现了循环依赖  
  if (earlySingletonReference != null) {
      // exposedObject是由上文initializeBean(beanName, exposedObject, mbd)返回的
      // 上文的initializeBean逻辑会调用实现了BeanPostProcessor的类对bean进行处理
      // 比如 @Async注解调用的就AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization进行处理
      // 如果被代理了initializeBean方法返回的exposedObject 就会和 初始bean 不一样
      // 这个判断的意思就是指 postProcessAfterInitialization 回调没有进行动态代理
      // 如果没有那么就将早期暴露出去的对象赋值给最终生成出去的对象并返回
      // 这样就实现了早期暴露出去的对象和最终生成的对象是同一个了
      // 但是一旦 postProcessAfterInitialization 回调生成了动态代理
      // 那么就不会走这, 也就是加了@Aysnc注解,是不会走这的
      if (exposedObject == bean) {
          exposedObject = earlySingletonReference;
      }
      else if (!this.allowRawInjectionDespiteWrapping &amp;&amp; hasDependentBean(beanName)) {
               // allowRawInjectionDespiteWrapping 默认是false
               String[] dependentBeans = getDependentBeans(beanName);
               Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
               for (String dependentBean : dependentBeans) {
                   if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                       actualDependentBeans.add(dependentBean);
                  }
               }
               if (!actualDependentBeans.isEmpty()) {
                   // 抛出异常 放一下翻译的异常原文
                   // 创建名为'aoo'的bean时出错:
                   // 名为'aoo'的bean以其原始版本的形式作为循环引用的一部分注入到其他bean [boo]中
                   // 但最终已被包装。这意味着其他bean不使用bean的最终版本。
                   // 这通常是过于热衷于类型匹配的结果
                   // - 例如,考虑使用“getBeanNamesForType”并关闭“allowEagerInit”标志。
                   // 也就是说你这个bean被人给用了, 你现在又改变了这个bean
                   // 所以两边不一致了, 需要抛出异常终止
                   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.");
               }
      }
   }
}

注意版本不一样实现逻辑可能有改变, 当前版本SpringBoot2.3.4

0 人点赞