从源码看Spring如何解决循环依赖的脉络?鸡生蛋与蛋生鸡的问题!

3551 篇文章 121 订阅

前言

当我从 《从Spring为什么要用IoC的支点,我撬动了整个Spring的源码脉络!》 走过来,再来看Spring中源码的各种细节是如此的舒畅。

所以让我们接着剑指Spring源码。

为什么会有循环依赖问题?

当一个 Chicken 类引用 Egg 类作为属性依赖,同样 Egg 类中也引用依赖 Chicken ,那么最终就会成为一个 闭环 。如下图所示:

这就好比鸡生蛋,蛋又生鸡,那么就会发生互相依赖又循环往复的关系就称为 循环依赖 。

循环依赖其实只是一个现象(场景),还不是一个问题。当你问 Chicken 和 Egg 两个对象循环依赖后能否相互创建成功?这才是一个问题。

例如下面代码所示,如果 Chicken 和 Egg 使用构造器相互依赖注入,那么会陷入循环依赖注入的死循环。

new Chicken( new Egg( new Chicken(new Egg(...))))
复制代码

我们再来看循环依赖问题在Spring中主要有的三种情况:

  • 构造器方式依赖注入
  • Set方式多例(原型)模式下依赖注入
  • Set方式单例依赖注入

第一种构造方法注入的情况,在上面我们构造器方式的依赖注入会造成无限循环下去。第二种多例的情况,多例跟Spring默认创建单例相反,多例是每创建一次类,实例就会多一份。而单例是确保类的实例只允许一份;所以多例每创建一个Bean时,都会产生一个新的Bean,所以导致无限循环的产生Bean,那么必然导致OOM出现。

在Spring中默认就是单例,所以Spring通过第三种set方式,可以实现循环依赖。代码如下所示:

@Component
public class Chicken {
 // Chicken中注入了Egg
 @Autowired
 private Egg egg;
}
复制代码
@Component
public class Egg {
 // Egg中也注入了Chicken
 @Autowired
 private Chicken chicken;
}
复制代码

解决循环依赖靠定义:三级缓存

Spring为了解决单例的循环依赖问题,首先会定义三级缓存做准备(你会发现很多缓存都是用key-value的方式,Spring也如此,同样用了三个Map)。

三个Map就定义在DefaultSingletonBeanRegistry类中。(现在可能对为什么要定义三级缓存感到陌生,但是别急,后面相信你会无比熟悉)

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

   /** Cache of singleton objects: bean name to bean instance. */
   private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

   /** Cache of singleton factories: bean name to ObjectFactory. */
   private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

   /** Cache of early singleton objects: bean name to bean instance. */
   private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
   
   //省略其他
}
复制代码

深入源码看Spring解决循环依赖的脉络

先来回顾一个Bena创建的主要流程:实例化Bean -> 填充Bean属性 -> 调用Bean初始化方法。

如下图所示,(可以说是Bean生命周期制定的地图,在( 《从Spring为什么要用IoC的支点,我撬动了整个Spring的源码脉络!》 有畅谈过,所以不再赘述)

Spring源码中在创建Bean的关键代码是使用 getBean() 方法。这也是创建Bean的开始。

public Object getBean(String name) throws BeansException {
   return doGetBean(name, null, null, false);
}
复制代码

接着我们进到 getBean() 中可以看到 doGetBean() ,在Spring源码中带 do 前缀的方法才是真正干活的。如下代码所示:

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实例
   // Eagerly check singleton cache for manually registered singletons.
   Object sharedInstance = getSingleton(beanName);
   if (sharedInstance != null && args == null) {
      
      //省略日志记录
       
      bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
   }

//省略其他处理
     
         //创建Bean的实例对象
         // Create bean instance.
         if (mbd.isSingleton()) {
            sharedInstance = getSingleton(beanName, () -> {
               try {
                  return createBean(beanName, mbd, args);
               }
               catch (BeansException ex) {
                  // Explicitly remove instance from singleton cache: It might have been put there
                  // eagerly by the creation process, to allow for circular reference resolution.
                  // Also remove any beans that received a temporary reference to the bean.
                  destroySingleton(beanName);
                  throw ex;
               }
            });
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
         }

         else if (mbd.isPrototype()) {
            // It's a prototype -> create a new instance.
            Object prototypeInstance = null;
            try {
               beforePrototypeCreation(beanName);
               prototypeInstance = createBean(beanName, mbd, args);
            }
            finally {
               afterPrototypeCreation(beanName);
            }
            bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
         }

         else {
            String scopeName = mbd.getScope();
            final Scope scope = this.scopes.get(scopeName);
            if (scope == null) {
               throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
            }
            try {
               Object scopedInstance = scope.get(beanName, () -> {
                  beforePrototypeCreation(beanName);
                  try {
                     return createBean(beanName, mbd, args);
                  }
                  finally {
                     afterPrototypeCreation(beanName);
                  }
               });
               bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
            }
   //省略其他处理
   return (T) bean;
}
复制代码

上述代码尽管很多,但是实际上 doGetBean() 方法主要两件事,如果从缓存中获取不到Bean,那么创建一个新的Bean。而一开始在缓存中是获取不到Bean的,所以会进行Bean的创建,在doGetBean中可以看到很醒目的 createBean() 方法。

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {
      
    // 省略其他代码
  
   try {
      Object beanInstance = doCreateBean(beanName, mbdToUse, args);
      if (logger.isTraceEnabled()) {
         logger.trace("Finished creating instance of bean '" + beanName + "'");
      }
      return beanInstance;
   }

    // 省略其他catch异常处理
}
复制代码

createBean实际做事的还是,前缀带do的 doCreateBean() 来实际创建Bean的方法,主要来看两个核心动作。第一个动作是 createBeanInstance() 来创建Bean的实例(给Bean开辟新的内存空间)。

createBeanInstance(beanName, mbd, args);
复制代码

第二个动作是执行 addSingletonFactory() 方法,将Bean添加缓存。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
复制代码

我们来看具体 addSingletonFactory() 方法,可以看到开始添加到第三级缓存。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
   Assert.notNull(singletonFactory, "Singleton factory must not be null");
   synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
         this.singletonFactories.put(beanName, singletonFactory);
         this.earlySingletonObjects.remove(beanName);
         this.registeredSingletons.add(beanName);
      }
   }
}
复制代码

但会发现 addSingletonFactory() 中添加到三级缓存中是一个 getEarlyBeanReference() 方法,而执行到这个方法时还不是一个完整Bean,那么实际上三级缓存保存是提前暴露出的对象。

在getEarlyBeanReference方法的上方是这样描述。(Spring开发者怕你看不懂源码,还写注释跟你说明白)

Obtain a reference for early access to the specified bean 获取对指定bean的早期访问的引用。

也是说 getEarlyBeanReference() 实际上只是直接将实例化创建的Bean返回了。(所以你可能会跟一颗剽悍的种子一样对三级缓存充满疑惑,图什么?别急,跟着一颗剽悍的种子去问个究竟)

接着实例化完Bean,继续按照Bean的生命周期流程就走到了Bean的填充属性,所以会看到#doCreateBean-> populateBean() 方法。

populateBean(beanName, mbd, instanceWrapper);
复制代码

对Bean的填充属性是循环依赖源头的开始,因为此时意味着 Chicken 属性对 Egg 属性注入,而当 Chicken 依赖 Egg ,那么这个时候Spring又会去 getBean(Egg) 。

因为 Egg 需要注入 Chicken ,所以在创建 Egg 的时候,又会去调用 getBean(Chicken) ,这个时候就又回到之前的流程了,但是不同的是,之前的 getBean 是为了创建Bean,而此时再调用因为上面 Chicken 在实例化后已经放到三级缓存,所以 Egg 可以从缓存中直接获取到 Chicken 。

所以此时 Egg 将从三级缓存中获取到的 Chicken 放入到二级缓存中,同时移除三级缓存中的工厂。接着 Egg 完成对 Chicken 的注入后, Egg 可以初始化;当Egg初始化完成,那么就回到了 Chicken 完成对 getBean(Egg) 的注入,最后 Chicken 初始化。

所以二级earlySingletonObjects所缓存的是完成实例化,但是还未进行属性注入及初始化的对象。

在完成初始化后,就可以在#getSingleton-> addSingleton() 方法,将初始化的Bean放入到一级缓存,同时删除二级,三级缓存。

protected void addSingleton(String beanName, Object singletonObject) {
   synchronized (this.singletonObjects) {
      // 添加到一级缓存
      this.singletonObjects.put(beanName, singletonObject);
      // 移除二级缓存
      this.singletonFactories.remove(beanName);
      // 移除三级缓存
      this.earlySingletonObjects.remove(beanName);
      // 将完成的BeanName添加到已经注册的单例集合
      this.registeredSingletons.add(beanName);
   }
}
复制代码

到此循环依赖完全打开。所以一级缓存中已经是完全创建好的单例Bean。

为什么要用三级缓存:一、二级缓存能解决循环依赖吗?

当再次回过头来,你会发现只用一、二级缓存似乎也可以解决缓存依赖,但是还存在如果循环依赖还使用Spring中的AOP代理时,此时没有三级缓存,就意味着二级缓存中刚实例化的Bean要完成 AOP 代理。

如下图所示,回顾 《从Spring为什么要用IoC的支点,我撬动了整个Spring的源码脉络!》 中有畅谈到,BeanPostProcessor后置处理器是AOP实现的关键。所以Bean在制定生命周期时,代理是放在最后一步来完成的,而不是在实例化后就立刻代理。

所以没有三级缓存,只使用二级缓存,那么就破坏了Spring对Bean生命周期的制定。

而通过三级缓存就可以很好的解决循环依赖后还有AOP代理问题。因为AOP代理有可能会把之前的原来的Bean换成代理Bean,所以导致Bean的版本不对,最终也会造成异常。

所以最后,再来看一级、二级、三级缓存中分别存放的是什么状态的Bean时,是不是亲切多了呢?

先是三级缓存为早期曝光对象工厂( singletonFactories ),是lambada表达式。然后是二级缓存为早期曝光对象 earlySingletonObjects ,是完成实例化,但是还未进行属性注入及初始化的对象。最后是一级缓存为单例池( singletonObjects ),是完全创建好的单例Bean。

最后

其实现在在写的这篇,要比前些天刚发布的 《从Spring为什么要用IoC的支点,我撬动了整个Spring的源码脉络!》 文章还要早的,但是迟迟没发原因是我被困住了,我发现尽管我看了再多的源码,对Spring的理解却是一块一块的碎片,既不系统,也不结构化,也不融会贯通;可以说是在不断的捡芝麻,而丢了西瓜。

所以当我在梳理了Spring脉络后,再来看这些Spring中的各种细节时,就正如众里寻她千百度,蓦然回首,那细节却在灯火阑珊处。

好了,夜深了,这些天熬的夜,也到这里了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
源码深度解析是一种深入研究源代码的方法,通过仔细阅读和理解源代码中的细节和逻辑,以获得对代码的深刻理解和洞察。这样的分析可以帮助开发者更好地理解代码的实现方式,从而更好地理解并使用该代码库。 关于spring如何解决循环依赖问题,我们可以从源码的角度来分析。Spring采用了三级缓存来解决循环依赖问题。 第一级缓存是singletonFactories缓存,用于存储正在创建的Bean的工厂对象。当容器正在创建一个Bean时,会将这个Bean的工厂对象存储在singletonFactories缓存中。 第二级缓存是earlySingletonObjects缓存,用于存储已经完成了属性填充但尚未初始化完成的Bean。当容器创建一个Bean时,会将正在创建的Bean存储在earlySingletonObjects缓存中。 第三级缓存是singletonObjects缓存,用于存储已经完成初始化的Bean。当一个Bean初始化完成后,会将其存储在singletonObjects缓存中。 Spring在创建Bean的过程中,会先查找一级缓存,如果找到了对应的工厂对象,则直接返回该对象,避免了创建过程中的循环依赖。如果一级缓存中没有找到对应的工厂对象,则通过递归的方式创建依赖的Bean。 在创建Bean的递归过程中,如果发现正在创建的Bean已经在二级缓存中,说明发循环依赖。此时,Spring会从二级缓存中获取正在创建的Bean的代理对象,以解决循环依赖。 当一个Bean创建完成后,会将其放入三级缓存中,并从一级缓存和二级缓存中移除。 总结来说,Spring通过三级缓存的方式解决循环依赖问题,保证了Bean的创建过程中不会陷入无限递归的循环。这种机制的实现使得Spring解决循环依赖问题上具有较好的性能和效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值