关于java:字节面试我这样回答Spring中的循环依赖拿下20k-offer

前言

Spring中的循环依赖始终是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多解决,另外一方面是因为面试的时候,如果问到Spring中比拟高阶的问题,那么循环依赖必然逃不掉。如果你答复得好,那么这就是你的必杀技,反正,那就是面试官的必杀技,这也是取这个题目的起因,当然,本文的目标是为了让你在之后的所有面试中能多一个必杀技,专门用来绝杀面试官!

文末有福利记得支付!

本文的核心思想就是,当面试官问:

“请讲一讲Spring中的循环依赖。”的时候,

咱们到底该怎么答复?

次要分上面几点

  1. 什么是循环依赖?
  2. 什么状况下循环依赖能够被解决?
  3. Spring是如何解决的循环依赖?

同时本文心愿纠正几个目前业界内经常出现的几个对于循环依赖的谬误的说法

  1. 只有在setter形式注入的状况下,循环依赖能力解决(
  2. 三级缓存的目标是为了提高效率(

OK,铺垫曾经做完了,接下来咱们开始注释

什么是循环依赖?

从字面上来了解就是A依赖B的同时B也依赖了A,就像上面这样

体现到代码档次就是这个样子

@Component
public class A {
    // A中注入了B
    @Autowired
    private B b;
}

@Component
public class B {
    // B中也注入了A
    @Autowired
    private A a;
}

当然,这是最常见的一种循环依赖,比拟非凡的还有

// 本人依赖本人
@Component
public class A {
    // A中注入了A
    @Autowired
    private A a;
}

尽管体现模式不一样,然而实际上都是同一个问题—–>循环依赖

什么状况下循环依赖能够被解决?

在答复这个问题之前首先要明确一点,Spring解决循环依赖是有前置条件的

  1. 呈现循环依赖的Bean必须要是单例
  2. 依赖注入的形式不能全是结构器注入的形式(很多博客上说,只能解决setter办法的循环依赖,这是谬误的)

其中第一点应该很好了解,第二点:不能全是结构器注入是什么意思呢?咱们还是用代码谈话

@Component
public class A {
//    @Autowired
//    private B b;
    public A(B b) {

    }
}

@Component
public class B {

//    @Autowired
//    private A a;

    public B(A a){

    }
}

在下面的例子中,A中注入B的形式是通过结构器,B中注入A的形式也是通过结构器,这个时候循环依赖是无奈被解决,如果你的我的项目中有两个这样相互依赖的Bean,在启动时就会报出以下谬误:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

为了测试循环依赖的解决状况跟注入形式的关系,咱们做如下四种状况的测试

依赖状况 依赖注入形式 循环依赖是否被解决
AB相互依赖(循环依赖) 均采纳setter办法注入
AB相互依赖(循环依赖) 均采纳结构器注入
AB相互依赖(循环依赖) A中注入B的形式为setter办法,B中注入A的形式为结构器
AB相互依赖(循环依赖) B中注入A的形式为setter办法,A中注入B的形式为结构器

具体的测试代码跟简略,我就不放了。从下面的测试后果咱们能够看到,不是只有在setter办法注入的状况下循环依赖能力被解决,即便存在结构器注入的场景下,循环依赖仍然被能够被失常解决掉。

那么到底是为什么呢?Spring到底是怎么解决的循环依赖呢?不要急,咱们接着往下看

Spring是如何解决的循环依赖?

对于循环依赖的解决形式应该要分两种状况来探讨

  1. 简略的循环依赖(没有AOP)
  2. 联合了AOP的循环依赖

简略的循环依赖(没有AOP)

咱们先来剖析一个最简略的例子,就是下面提到的那个demo

@Component
public class A {
    // A中注入了B
    @Autowired
    private B b;
}

@Component
public class B {
    // B中也注入了A
    @Autowired
    private A a;
}

通过上文咱们曾经晓得了这种状况下的循环依赖是可能被解决的,那么具体的流程是什么呢?咱们一步步剖析

首先,咱们要晓得Spring在创立Bean的时候默认是依照天然排序来进行创立的,所以第一步Spring会去创立A

与此同时,咱们应该晓得,Spring在创立Bean的过程中分为三步

  1. 实例化,对应办法:AbstractAutowireCapableBeanFactory中的createBeanInstance办法
  2. 属性注入,对应办法:AbstractAutowireCapableBeanFactorypopulateBean办法
  3. 初始化,对应办法:AbstractAutowireCapableBeanFactoryinitializeBean

这些办法在之前源码剖析的文章中都做过具体的解读了,如果你之前没看过我的文章,那么你只须要晓得

  1. 实例化,简略了解就是new了一个对象
  2. 属性注入,为实例化中new进去的对象填充属性
  3. 初始化,执行aware接口中的办法,初始化办法,实现AOP代理

基于下面的常识,咱们开始解读整个循环依赖解决的过程,整个流程应该是以A的创立为终点,前文也说了,第一步就是创立A嘛!

创立A的过程实际上就是调用getBean办法,这个办法有两层含意

  1. 创立一个新的Bean
  2. 从缓存中获取到曾经被创立的对象

咱们当初剖析的是第一层含意,因为这个时候缓存中还没有A嘛!

调用getSingleton(beanName)

首先调用getSingleton(a)办法,这个办法又会调用getSingleton(beanName, true),在上图中我省略了这一步

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}

getSingleton(beanName, true)这个办法实际上就是到缓存中尝试去获取Bean,整个缓存分为三级

  1. singletonObjects,一级缓存,存储的是所有创立好了的单例Bean
  2. earlySingletonObjects,实现实例化,然而还未进行属性注入及初始化的对象
  3. singletonFactories,提前裸露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象

因为A是第一次被创立,所以不论哪个缓存中必然都是没有的,因而会进入getSingleton的另外一个重载办法getSingleton(beanName, singletonFactory)

调用getSingleton(beanName, singletonFactory)

这个办法就是用来创立Bean的,其源码如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {

            // ....
            // 省略异样解决及日志
            // ....

            // 在单例对象创立前先做一个标记
            // 将beanName放入到singletonsCurrentlyInCreation这个汇合中
            // 标记着这个单例Bean正在创立
            // 如果同一个单例Bean屡次被创立,这里会抛出异样
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 上游传入的lambda在这里会被执行,调用createBean办法创立一个Bean后返回
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            }
            // ...
            // 省略catch异样解决
            // ...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 创立实现后将对应的beanName从singletonsCurrentlyInCreation移除
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 增加到一级缓存singletonObjects中
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

下面的代码咱们次要抓住一点,通过createBean办法返回的Bean最终被放到了一级缓存,也就是单例池中。

那么到这里咱们能够得出一个论断:一级缓存中存储的是曾经齐全创立好了的单例Bean

调用addSingletonFactory办法

如下图所示:

在实现Bean的实例化后,属性注入之前Spring将Bean包装成一个工厂增加进了三级缓存中,对应源码如下:

// 这里传入的参数也是一个lambda表达式,() -> getEarlyBeanReference(beanName, mbd, bean)
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);
        }
    }
}

这里只是增加了一个工厂,通过这个工厂(ObjectFactory)的getObject办法能够失去一个对象,而这个对象实际上就是通过getEarlyBeanReference这个办法创立的。那么,什么时候会去调用这个工厂的getObject办法呢?这个时候就要到创立B的流程了。

当A实现了实例化并增加进了三级缓存后,就要开始为A进行属性注入了,在注入时发现A依赖了B,那么这个时候Spring又会去getBean(b),而后反射调用setter办法实现属性注入。

因为B须要注入A,所以在创立B的时候,又会去调用getBean(a),这个时候就又回到之前的流程了,然而不同的是,之前的getBean是为了创立Bean,而此时再调用getBean不是为了创立了,而是要从缓存中获取,因为之前A在实例化后曾经将其放入了三级缓存singletonFactories中,所以此时getBean(a)的流程就是这样子了

从这里咱们能够看出,注入到B中的A是通过getEarlyBeanReference办法提前裸露进来的一个对象,还不是一个残缺的Bean,那么getEarlyBeanReference到底干了啥了,咱们看下它的源码

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;
}

它实际上就是调用了后置处理器的getEarlyBeanReference,而真正实现了这个办法的后置处理器只有一个,就是通过@EnableAspectJAutoProxy注解导入的AnnotationAwareAspectJAutoProxyCreator也就是说如果在不思考AOP的状况下,下面的代码等价于:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    return exposedObject;
}

也就是说这个工厂啥都没干,间接将实例化阶段创立的对象返回了!所以说在不思考AOP的状况下三级缓存有用嘛?讲道理,真的没什么用,我间接将这个对象放到二级缓存中不是一点问题都没有吗?如果你说它进步了效率,那你通知我进步的效率在哪?

那么三级缓存到底有什么作用呢?不要急,咱们先把整个流程走完,在下文联合AOP剖析循环依赖的时候你就能领会到三级缓存的作用!

到这里不晓得小伙伴们会不会有疑难,B中提前注入了一个没有通过初始化的A类型对象不会有问题吗?

答:不会

这个时候咱们须要将整个创立A这个Bean的流程走完,如下图:

从上图中咱们能够看到,尽管在创立B时会提前给B注入了一个还未初始化的A对象,然而在创立A的流程中始终应用的是注入到B中的A对象的援用,之后会依据这个援用对A进行初始化,所以这是没有问题的。

联合了AOP的循环依赖

之前咱们曾经说过了,在一般的循环依赖的状况下,三级缓存没有任何作用。三级缓存实际上跟Spring中的AOP相干,咱们再来看一看getEarlyBeanReference的代码:

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;
}

如果在开启AOP的状况下,那么就是调用到AnnotationAwareAspectJAutoProxyCreatorgetEarlyBeanReference办法,对应的源码如下:

public Object getEarlyBeanReference(Object bean, String beanName) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    this.earlyProxyReferences.put(cacheKey, bean);
    // 如果须要代理,返回一个代理对象,不须要代理,间接返回以后传入的这个bean对象
    return wrapIfNecessary(bean, beanName, cacheKey);
}

回到下面的例子,咱们对A进行了AOP代理的话,那么此时getEarlyBeanReference将返回一个代理后的对象,而不是实例化阶段创立的对象,这样就意味着B中注入的A将是一个代理对象而不是A的实例化阶段创立后的对象。

看到这个图你可能会产生上面这些疑难

  1. 在给B注入的时候为什么要注入一个代理对象?

答:当咱们对A进行了AOP代理时,阐明咱们心愿从容器中获取到的就是A代理后的对象而不是A自身,因而把A当作依赖进行注入时也要注入它的代理对象

  1. 明明初始化的时候是A对象,那么Spring是在哪里将代理对象放入到容器中的呢?

在实现初始化后,Spring又调用了一次getSingleton办法,这一次传入的参数又不一样了,false能够了解为禁用三级缓存,后面图中曾经提到过了,在为B中注入A时曾经将三级缓存中的工厂取出,并从工厂中获取到了一个对象放入到了二级缓存中,所以这里的这个getSingleton办法做的工夫就是从二级缓存中获取到这个代理后的A对象。exposedObject == bean能够认为是必然成立的,除非你非要在初始化阶段的后置处理器中替换掉失常流程中的Bean,例如减少一个后置处理器:

@Component
public class MyPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("a")) {
            return new A();
        }
        return bean;
    }
}

不过,请不要做这种骚操作,徒增懊恼!

  1. 初始化的时候是对A对象自身进行初始化,而容器中以及注入到B中的都是代理对象,这样不会有问题吗?

答:不会,这是因为不论是cglib代理还是jdk动静代理生成的代理类,外部都持有一个指标类的援用,当调用代理对象的办法时,理论会去调用指标对象的办法,A实现初始化相当于代理对象本身也实现了初始化

  1. 三级缓存为什么要应用工厂而不是间接应用援用?换而言之,为什么须要这个三级缓存,间接通过二级缓存裸露一个援用不行吗?

答:这个工厂的目标在于提早对实例化阶段生成的对象的代理,只有真正产生循环依赖的时候,才去提前生成代理对象,否则只会创立一个工厂并将其放入到三级缓存中,然而不会去通过这个工厂去真正创建对象

咱们思考一种简略的状况,就以独自创立A为例,假如AB之间当初没有依赖关系,然而A被代理了,这个时候当A实现实例化后还是会进入上面这段代码:

// A是单例的,mbd.isSingleton()条件满足
// allowCircularReferences:这个变量代表是否容许循环依赖,默认是开启的,条件也满足
// isSingletonCurrentlyInCreation:正在在创立A,也满足
// 所以earlySingletonExposure=true
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                  isSingletonCurrentlyInCreation(beanName));
// 还是会进入到这段代码中
if (earlySingletonExposure) {
    // 还是会通过三级缓存提前裸露一个工厂对象
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

看到了吧,即便没有循环依赖,也会将其增加到三级缓存中,而且是不得不增加到三级缓存中,因为到目前为止Spring也不能确定这个Bean有没有跟别的Bean呈现循环依赖。

假如咱们在这里间接应用二级缓存的话,那么意味着所有的Bean在这一步都要实现AOP代理。这样做有必要吗?

不仅没有必要,而且违反了Spring在联合AOP跟Bean的生命周期的设计!Spring联合AOP跟Bean的生命周期自身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来实现的,在这个后置解决的postProcessAfterInitialization办法中对初始化后的Bean实现AOP代理。如果呈现了循环依赖,那没有方法,只有给Bean先创立代理,然而没有呈现循环依赖的状况下,设计之初就是让Bean在生命周期的最初一步实现代理而不是在实例化后就立马实现代理。

三级缓存真的进步了效率了吗?

当初咱们曾经晓得了三级缓存的真正作用,然而这个答案可能还无奈压服你,所以咱们再最初总结剖析一波,三级缓存真的进步了效率了吗?分为两点探讨:

  1. 没有进行AOP的Bean间的循环依赖

从上文剖析能够看出,这种状况下三级缓存基本没用!所以不会存在什么进步了效率的说法

  1. 进行了AOP的Bean间的循环依赖

就以咱们上的A、B为例,其中A被AOP代理,咱们先剖析下应用了三级缓存的状况下,A、B的创立流程

假如不应用三级缓存,间接在二级缓存中

下面两个流程的惟一区别在于为A对象创立代理的机会不同,在应用了三级缓存的状况下为A创立代理的机会是在B中须要注入A的时候,而不应用三级缓存的话在A实例化后就须要马上为A创立代理而后放入到二级缓存中去。对于整个A、B的创立过程而言,耗费的工夫是一样的

综上,不论是哪种状况,三级缓存进步了效率这种说法都是谬误的!

总结

面试官:”Spring是如何解决的循环依赖?“

答:Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为晚期曝光对象earlySingletonObjects,三级缓存为晚期曝光对象工厂(singletonFactories)。当A、B两个类产生循环援用时,在A实现实例化后,就应用实例化后的对象去创立一个对象工厂,并增加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创立B,同时B又依赖了A,所以创立B的同时又会去调用getBean(a)来获取须要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject办法来获取到对应的对象,失去这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包含初始化、后置处理器等。当B创立完后,会将B再注入到A中,此时A再实现它的整个生命周期。至此,循环依赖完结!

面试官:”为什么要应用三级缓存呢?二级缓存能解决循环依赖吗?“

答:如果要应用二级缓存解决循环依赖,意味着所有Bean在实例化后就要实现AOP代理,这样违反了Spring设计的准则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最初一步来实现AOP代理,而不是在实例化后就立马进行AOP代理。

一道思考题

为什么在下表中的第三种状况的循环依赖能被解决,而第四种状况不能被解决呢?

提醒:Spring在创立Bean时默认会依据天然排序进行创立,所以A会先于B进行创立

依赖状况 依赖注入形式 循环依赖是否被解决
AB相互依赖(循环依赖) 均采纳setter办法注入
AB相互依赖(循环依赖) 均采纳结构器注入
AB相互依赖(循环依赖) A中注入B的形式为setter办法,B中注入A的形式为结构器
AB相互依赖(循环依赖) B中注入A的形式为setter办法,A中注入B的形式为结构器

如果本文对你由帮忙的话,记得点个赞吧!也欢送关注我,跟着我一起认认真真学Java。作为浏览福利我也整顿了一些Java学习材料,蕴含微服务、MySQL、分布式、SSM框架、Java根底、Redis、数据结构与算法、网络、Linux、Spring全家桶、JVM、高并发等,当初收费分享给浏览到本篇文章的Java程序员,须要的自行支付~

最全学习笔记大厂真题+微服务+MySQL+分布式+SSM框架+Java+Redis+数据结构与算法+网络+Linux+Spring全家桶+JVM+高并发+各大学习思维脑图+面试汇合

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理