Spring解决循环依赖问题

image-20230916164608581

什么是循环依赖?

  • A依赖B的同时,B也依赖了A,就构成了循环依赖。
@Component
public class A{
   // 依赖B
   @Autowired
   private B b;
   public B getB() {
      return b;
   }
}

@Component
public class B {
   // 依赖A
   @Autowired
   private A a;
   public A getA() {
      return a;
   }
}

//比较特殊的循环依赖
@Component
public class A{
   // 依赖B
   @Autowired
   private A a;
}

在Spring官网中有提到Spring循环依赖的问题,Spring不推荐使用构造器的方式进行注入。(因为通过构造器注入是无法解决循环依赖的)而是通过setter方式注入,这样可以很好避免循环依赖的问题

Spring的循环依赖过程跟bean的生命周期密切相关。实例化A的过程:

  • 实例化A -> 填充属性B -> 发现B没有被实例化 -> 实例化B -> 填充属性A -> 发现A也没有被实例化 -> 最终失败。

那么,到底有没有办法解决这样的问题呢?

  • 有的。核心在于:提前暴露这个对象的引用。(先暴露引用,后完成属性的填充)

最后,流程就变成了:

  • 实例化A -> 暴露a对象 -> 填充属性B -> 发现B没有被实例化 -> 实例化B -> 填充属性A -> 实例化B成功> 实例化A -> 成功。

Spring的确就是这么做的。

想一想:为什么可以先提前暴露出a对象?
  • 在java中,对象使用的是引用传递。即使提前暴露了,其实是不影响的。最终属性填充完后,最终结果都是正常的。

Spring通过三级缓存解决依赖注入

spring的思路和上述思路是一致的,并且Spring更加严谨!通过三级缓存来解决循环依赖的问题,把提前暴露的对象存放到三级缓存中,二级缓存存放的是过渡的bean,一级缓存存放的是最终形态的bean。

  • 一级缓存(也叫单例池):存放已经经历了完整生命周期的Bean对象。
  • 二级缓存:主要存放过渡bean,也就是三级缓存中ObjectFactory产生的对象。主要作用是防止bean被AOP切面代理时,重复通过三级缓存对象ObjectFactory创建对象。被代理情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。这明显不满足spring单例的原则,所以需要二级缓存进行缓存。

同时需要注意:二级缓存中存放的bean也是半成品的,此时属性未填充。

  • 三级缓存:存放的对象为ObjectFactory类型,主要作用是产生bean对象。调用getObject()最终调用的是getEarlyBeanReference()。该方法的主要作用是:如果有需要,产生代理对象。如果bean被AOP切面代理,返回代理bean对象如果未被代理,就返回原始的bean对象

getEarlyBeanReference()调用的SmartInstantiationAwareBeanPostProcessor,其实是Spring留得拓展点,本质是通过BeanPostProcessor定制bean的产

生过程。绝大多数AOP(比如@Transactional)单例对象的产生,都是在这里进行了拓展,进而实现单例对象的生成。

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次都会从容器中获取一个新的对象,都是重新创建的,所以非单例的bean

是没有缓存的,不会放入到三级缓存中。

实例化A的过程:
image-20230916171058288

实例化A的时候,需要注入B。因为Spring默认bean是单例的,因此会先去Spring容器中查找B。有的话直接注入,没有再进行创建。

此时是没有B的,因此需要先实例化B。

image-20230916172816135

有意思的是实例化B的时候,发现需要注入a。因此,还是先在Spring容器中查找a。此时a是处于创建状态(提前暴露在三级缓存中),所以就直接获取了。注意:三级缓存中存放的并不是真正的bean对象,而是生成bean的ObjectFactory。到三级缓存获取的时候,会到AbstractAutowireCapableBeanFactory#getEarlyBeanReference()的处理,才能获取到bean,然后放入二级缓存中,同时返回a进行依赖注入。

image-20230916172757815

通过提前暴露对象到多级缓存,已经成功将实例b中的属性a注入了,那后面的流程自然一路畅通:继续执行b的实例化initializeBean() -> 将b从正在创建列表移出-> 将b放入一级缓存(同时将b在二级缓存和三级缓存中删除) -> 返回b。

image-20230916173050416

既然b实例化完了,那么又到回a了。后续的流程和b的后续流程一致。这里就不再赘述了。

其实这就是Spring解决循环依赖的流程,其核心思路就是:**先将bean提前暴露到三级缓存中,后续有依赖注入的话,先将这个半成品的bean进行注入。**之所以说这个bean是半成品,是因为暴露在三级缓存和二级缓存中的bean虽然已经创建成功,但是属性还没有进行填充,Aware回调等流程也没有执行,所以说它是一个不完整的bean对象。

为什么需要三级缓存?二级缓存行不行?

先说结论:不行!

在没有AOP的情况下,确实可以只通过一级缓存和三级缓存来解决循环依赖。回忆一下流程:

  1. 首先实例化A,实例化前先将半成品暴露在三级缓存中。
  2. 填充属性B,发现B还没有实例化,先去实例化B。
  3. 实例化B的过程中,需要填充属性A,从三级缓存中通过ObjectFactory#getObject()直接获取A(在没有AOP的场景下,多次获取的是同一个bean),进行依赖注入,并完成实例化流程。
  4. 获取到b,实例化A的流程继续,注入到b到a中,进而完成a的实例化。

但是如果bean被AOP代理了,那么就不一样了。最核心的区别点:就是每次调用ObjectFactory#getObject()都会产生一个新的代理对象(这一点是重中之重),我们用存在事务的场景测试一下:

@Component
public class A {
   @Autowired
   private B b;
   
   @Transactional //增加事务注解,会对bean生成代理对象
   public B getB() {
      System.out.println(Thread.currentThread().getName());
      return b;
   }
}

此时A就会被AOP代理,生成的实例a也是代理对象了。我们debug验证一下,就会发现此时A确实被CGLIB代理了:

image-20230916175956058

当我们多次调用ObjectFactory#getObject(),产生的代理对象不是同一个:

image-20230916180046279

确实不是同一个。

当有AOP时,bean的创建示意图如下:

image-20230917231411760

类A有AOP,A会把自己的引用提前暴露在缓存中,类B可以直接使用。但是当A实例化完,需要放入到一级缓存的时候,最终存放的确实代理对象。也就是说B拿到的是A的原始对象,但是一级缓存中存放的确实A的代理对象。那么就破坏了单例的规则了。

  • 出现这个问题主要是上文提到过的 AOP 是通过 BeanPostProcessor 实现的,而 BeanPostProcessor 是在 属性注入阶段后 才执行的,所以会导致注入的对象有可能和最终的对象不一致

那么问题来了:A是单例的,也就是要保证,在Spring中,使用到该bean的地方,都是同一个bean才行。但是每次执行singletonFactory.getObject()都会产生新的代理对象。假设只有一级和三级缓存,每次从三级缓存中获取代理对象,都会产生新的代理对象,忽略性能不说,是不符合单例原则的。

所以这里我们要借助二级缓存来解决这个问题,将singleFactory.getObject()产生的对象放到二级缓存中去,后面直接从二级缓存中拿,保证始终只有一个代理对象。

image-20230916180558665

不支持循环依赖的情况

  1. 非单例的bean。
  2. constructor注入的bean。
  3. @Async方式的bean。
非单例的bean无法支持循环依赖
//AbstractAutowireCapableBeanFactory.java
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {
    // 省略部分代码
    // 是否支持循环依赖
    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");
       }
       // 做循环依赖的支持 将早期实例化bean的ObjectFactory,添加到单例工厂(三级缓存)中
       addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
}

解决循环依赖的前提:把半成品bean暴露到三级缓存中。

前置条件就是:

  1. mbd.isSingleton():要求bean是单例的。
  2. this.allowCircularReferences:是否允许循环依赖,默认为true,即默认支持循环依赖。
  3. isSingletonCurrentlyInCreation(beanName):判断当前bean是否正在创建中,默认是成立的,因为在创建bean的时候,会先设置该标志。

constructor注入导致无法支持循环依赖

@Component
public class A{
   // 依赖B
   private B b;
   
   public A(B b){
       this.b = b;
   }
   
   public B getB() {
      return b;
   }
}

@Component
public class B {
   // 依赖A
   private A a;
   
   public A(A a){
       this.a = a;
   }
   
   public A getA() {
      return a;
   }
}

A实例创建时 -> 构造注入B -> 查找B,容器中不存在,先实例化创建B -> 构造注入A -> 容器中不存在A(此时A还没有添加到三级缓存中) -> 异常UnsatisfiedDependencyException。因为暴露对象放入三级缓存的过程在实例创建之后,通过构造方法注入时,还没有放入三级缓存呢所以无法支持构造器注入类型的循环依赖。

通过上述的知识,我们了解了Spring解决循环依赖的大致流程。接下来,我们深入细节,感受Spring的强大魅力!

Spring是如何通过三级缓存解决AOP的问题呢?

我们已经知道了,在三级缓存中存放的是ObjectFactory。如果使用了AOP,那么往二级缓存存放的是代理的bean对象。如果没有使用AOP,存放的就是实例的bean对象。

那么ObjectFactory到底是什么呢?

深入源码doCreateBean()中的addSingletonFactory()

this.(beanName, () -> {
    return this.getEarlyBeanReference(beanName, mbd, bean);
});

可以看到addSingletonFactory()的第二个参数就是ObjectFactory。而ObjectFactory,它是通过匿名内部类来生成的。

image-20230917214410198

进入getEarlyBeanReference(),点击进入getEarlyBeanReference(),发现它是一个接口,进入具体的实现。

image-20230917214716854

可以看到方法中的第二个参数就是 ObjectFactory 类型,并且将其添加进 三级缓存(singletonFactories) 中。

也就是说 Spring 在加入缓存时,会将 实例化后生成的原始对象 通过 lambda 表达式调用 getObject() 方法,getObject()方法里调用 getEarlyBeanReference()方法 来封装成 ObjectFactory对象。

  • earlyProxyReferences 存储的是 (beanName, bean)`键值对,这里的 bean 指的是原始对象(刚实例化后的对象)。

  • wrapIfNecessary()方法用于执行 AOP 操作,生成一个代理对象(也就是说如果有 AOP 操作最后返回的是代理对象,否则返回的还是原始对象)。

最后梳理一下整体流程

image.png

先通过getSingleton()判断bean是否存在。

image-20230917221402168

判断顺序:一级缓存 -> 二级缓存 -> 三级缓存。

第三级缓存中调用了 singletonFactories.get(beanName)按照上文所说的会触发执行:如果有 AOP 操作则返回代理对象,否则返回原始对象。并且会判断取出的数据是否存在,如果存在则存入到二级缓存中,并删除三级缓存的数据。

如果缓存中都没有的话,就会去执行 createBean()创建一个 Bean 对象出来。

真正执行创建bean的是doCreateBean()

image-20230917221720830

引用:

  1. 浅谈 Spring 如何解决 Bean 的循环依赖问题
  2. 聊透Spring循环依赖
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值