掘金 后端 ( ) • 2024-04-29 09:57

关注微信公众号 “程序员小胖” 每日技术干货,第一时间送达!

引言

在Spring框架的编程征途中,循环依赖犹如一场突如其来的风暴,让无数开发者措手不及。它既是代码世界的隐形陷阱,也是检验技术水平的试金石。本文,我们将化身探险家,深入Spring的核心地带,揭开循环依赖的神秘面纱。从困惑到清晰,从理论到实践,一步步教你识别、理解直至破解这一难题,确保你的项目稳健前行。

循环依赖

循环依赖(circular dependency)简单说就是A对象依赖了B对象,B对象依赖了A对象,在A类的实例化过程中,Spring发现需要注入B类的bean,于是去创建B类的bean。同理,B类在创建过程中又需要A类的bean,于是形成了一个循环。

class A{

  @autowired
  private B b;
  
  public void test(){
    System.out.println(b.test());
  }
}

class B{

  @autowired
  private A a;
  
  public void test(){
    System.out.println(a.test());
  }
}
A a = new A();
B b = new B();

a.b = b;
b.a = a;

在Spring中,一个对象并不是简单new出来了,而是会经过一系列的Bean的生命周期,就是因为Bean的生命周期所以才会出现循环依赖问题。当然,在Spring中,出现循环依赖的场景很多,有的场景Spring自动帮我们解决了,而有的场景则需要程序员来解决,要想明白Spring中的循环依赖,得先明白Spring中Bean的生命周期。

Spring Bean的生命周期

如果想深度理解Spring Bean的生命周期,可以看我以往的文章《*******》 这里简单描述下Bean的生命周期

  1. Spring扫描class得到BeanDefinition
  2. 根据得到的BeanDefinition去生成bean
  3. 首先根据class推断构造方法
  4. 根据推断出来的构造方法,反射,得到一个原始对象对象
  5. 填充原始对象中的属性(依赖注入)
  6. 如果原始对象中的某个方法被AOP了,那么则需要根据原始对象生成一个代理对象
  7. 把最终生成的代理对象放入单例池(源码中叫做singletonObjects)中,下次getBean时就直接从单例池取即可

可以发现,在Spring中,构造一个Bean,包括了new这个步骤(第4步构造方法反射)。得到一个原始对象后,Spring需要给对象中的属性进行依赖注入,那么这个注入过程是怎样的?

A类中存在一个B类的b属性,所以,当A类生成了一个原始对象之后,就会去给b属性去赋值,此时就会根据b属性的类型和属性名去BeanFactory中去获取B类所对应的单例bean。如果此时BeanFactory中存在B对应的Bean,那么直接拿来赋值给b属性;如果此时BeanFactory中不存在B对应的Bean,则需要生成一个B对应的Bean,然后赋值给b属性。 问题来了,生成B对应Bean的过程中 B类中存在A类的a属性,在创建B的bean的过程中就需要A类对应的Bean,但是B类Bean的创建条件是A类Bean在创建过程中的依赖注入,这里就出现了循环依赖。

由上图可见 A和B的Bean一直都创建不出来,这就是Spring中的循环依赖

三级缓存

Spring就是三级缓存机制帮开发者解决了部分循环依赖的问题。接下来我们了解下三级缓存是怎么处理的。

**一级缓存为(singletonObjects):**缓存的是已经经历了完整生命周期的bean对象。

**二级缓存为(earlySingletonObjects):**比singletonObjects多了一个early,表示缓存的是早期的bean对象。早期是什么意思?表示Bean的生命周期还没走完就把这个Bean放入了earlySingletonObjects。

**三级缓存为(singletonFactories):**缓存的是ObjectFactory,表示对象工厂,表示用来创建早期bean对象的工厂。

如何把两个打架的人停止打架呢,必须得有个劝架的人,解决这个问题也是同理。

由上图可见,其实从头到尾只多引入了一个A原始对象 既然一个缓存可以解决的问题为啥还需要引入三级缓存呢?

其实这是难点 仔细想想 A的原始对象注入给B的属性后 A的原始对象进行了AOP产生了一个代理对象,对于A来说它的bean对象是AOP之后的代理对象 B的a属性其实是AOP之前的对象 其实归根到底来说 B依赖的A和最终的A不是同一个对象。

AOP就是通过一个BeanPostProcessor来实现的,这个BeanPostProcessor就是AnnotationAwareAspectJAutoProxyCreator,它的父类是AbstractAutoProxyCreator,而在Spring中AOP利用的要么是JDK动态代理,要么CGLib的动态代理,所以如果给一个类中的某个方法设置了切面,那么这个类最终就需要生成一个代理对象。

一般过程就是:A类--->生成一个普通对象-->属性注入-->基于切面生成一个代理对象-->把代理对 象放入singletonObjects单例池中。 而AOP可以说是Spring中除开IOC的另外一大功能,而循环依赖又是属于IOC范畴的,所以这两大功 能想要并存,Spring需要特殊处理。如何处理的,就是利用了第三级缓存singletonFactories。

首先,singletonFactories中存的是某个beanName对应的ObjectFactory,在bean的生命周期中, 生成完原始对象之后,就会构造一个ObjectFactory存入singletonFactories中。这个ObjectFactory 是一个函数式接口,所以支持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd,bean)

    protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
        Object exposedObject = bean;
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                    SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                    exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                }
            }
        }
        return exposedObject;
    }

该方法会去执行SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference方法, 而这个接口下的实现类中只有两个类实现了这个方法,一个是AbstractAutoProxyCreator,一个是 InstantiationAwareBeanPostProcessorAdapter,它的实现如下:

// InstantiationAwareBeanPostProcessorAdapter
@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
    return bean;
}
// AbstractAutoProxyCreator
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    return wrapIfNecessary(bean, beanName, cacheKey);
}

在整个Spring中,默认就只有AbstractAutoProxyCreator真正意义上实现了getEarlyBeanReference方法,而该类就是用来进行AOP的。上文提到的AnnotationAwareAspectJAutoProxyCreator的父类就是AbstractAutoProxyCreator。 那么getEarlyBeanReference方法到底在干什么? 首先得到一个cachekey,cachekey就是beanName。 然后把beanName和bean(这是原始对象)存入earlyProxyReferences中 调用wrapIfNecessary进行AOP,得到一个代理对象。 那么,什么时候会调用getEarlyBeanReference方法呢?回到循环依赖的场景中

总结

在深入剖析Spring框架处理循环依赖的巧妙机制中,三级缓存策略无疑是其智慧的结晶。这一旅程不仅揭示了循环依赖这一编程挑战的深层原理,还展现了Spring如何利用singletonObjects、earlySingletonObjects与singletonFactories这三个级别的缓存,优雅地解决了单例Bean之间的依赖死锁问题。