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

4次阅读

共计 10398 个字符,预计需要花费 26 分钟才能阅读完成。

前言

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+ 高并发 + 各大学习思维脑图 + 面试汇合

正文完
 0