栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring循环依赖,我本来都不想写的,但网上好多错误观点

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring循环依赖,我本来都不想写的,但网上好多错误观点

今天我们来盘 Spring 的经典面试题循环依赖。

这的面试题很经典,网上相应的文章很多,但是我还是想写一下,因为有些文章的观点在我看来是错的。

比如有构造器就无法解决循环依赖?

一定要三级缓存才能解决循环依赖?

到底为什么要三级缓存?

好了,话不多少,我们来盘一盘。

什么是循环依赖

很简单,看下方的代码就知晓了

@Service
public class A {
    @Autowired
    private B b;
}

@Service
public class B {
    @Autowired
    private A a;
}

//或者自己依赖自己
@Service
public class A {
    @Autowired
    private A a;
}

上面这两种方式都是循环依赖,应该很好理解,当然也可以是三个 Bean 甚至更多的 Bean 相互依赖,原理都是一样的,今天我们主要分析两个 Bean 的依赖。

这种循环依赖可能会产生问题,例如 A 要依赖 B,发现 B 还没创建。

于是开始创建 B ,创建的过程发现 B 要依赖 A, 而 A 还没创建好呀,因为它要等 B 创建好。

就这样 它们俩就搁这卡 bug 了 。

Spring 如何解决循环依赖

上面这种循环依赖在实际场景中是会出现的,所以 Spring 需要解决这个问题,那如何解决呢?

关键就是 提前暴露未完全创建完毕的 Bean 。

在 Spring 中,只有同时满足以下两点才能解决循环依赖的问题:

    依赖的 Bean 必须都是单例

    依赖注入的方式,必须 不全是 构造器注入,且 beanName 字母序在前的不能是构造器注入

为什么必须都是单例

如果从源码来看的话,循环依赖的 Bean 是原型模式,会直接抛错:

所以 Spring 只支持单例的循环依赖, 但是为什么呢 ?

按照理解,如果两个 Bean 都是原型模式的话。

那么创建 A1 需要创建一个 B1。

创建 B1 的时候要创建一个 A2。

创建 A2 又要创建一个 B2。

创建 B2 又要创建一个 A3。

创建 A3 又要创建一个 B3.....

就又卡 BUG 了,是吧,因为原型模式都需要创建新的对象,不能跟用以前的对象。

如果是单例的话,创建 A 需要创建 B,而创建的 B 需要的是之前的个 A, 不然就不叫单例了,对吧?

也是基于这点, Spring 就能操作操作了。

具体做法就是:先创建 A,此时的 A 是不完整的(没有注入 B),用个 map 保存这个不完整的 A,再创建 B ,B 需要 A。

所以从那个 map 得到“不完整”的 A,此时的 B 就完整了,然后 A 就可以注入 B,然后 A 就完整了,B 也完整了,且它们是相互依赖的。

 

读起来好像有点绕,但是逻辑其实很清晰。

为什么不能全是构造器注入

在 Spring 中创建 Bean 分三步:

    实例化,createBeanInstance,就是 new 了个对象

    属性注入,populateBean, 就是 set 一些属性值

    初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等

明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。

如果全是构造器注入,比如 A(B b) ,那表明在 new 的时候,就需要得到 B,此时需要 new B 。

但是 B 也是要在构造的时候注入 A ,即 B(A a) ,这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。

为什么找不到?因为 A 还没 new 完呢,所以找到不完整的 A, 因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖 。

一个set注入,一个构造器注入一定能成功?

假设我们 A 是通过 set 注入 B,B 通过构造函数注入 A,此时是 成功的 。

我们来分析下:实例化 A 之后,可以在 map 中存入 A,开始为 A 进行属性注入,发现需要 B。

此时 new B,发现构造器需要 A,此时从 map 中得到 A ,B 构造完毕。

B 进行属性注入,初始化,然后 A 注入 B 完成属性注入,然后初始化 A。

整个过程很顺利,没毛病。

假设 A 是通过构造器注入 B,B 通过 set 注入 A,此时是 失败的 。

我们来分析下:实例化 A,发现构造函数需要 B, 此时去实例化 B。

然后进行 B 的属性注入,从 map 里面找不到 A,因为 A 还没 new 成功,所以 B 也卡住了,然后就 gg。

看到这里,仔细思考的小伙伴可能会说,可以先实例化 B 啊,往 map 里面塞入不完整的 B,这样就能成功实例化 A 了啊。

确实,思路没错 但是 Spring 容器是按照字母序创建 Bean 的,A 的创建永远排在 B 前面 。

现在我们总结一下:

如果循环依赖都是构造器注入,则失败

如果循环依赖不完全是构造器注入,则可能成功,可能失败,具体跟BeanName的字母序有关系。

Spring 解决循环依赖全流程

经过上面的铺垫,我想你对 Spring 如何解决循环依赖应该已经有点感觉了,接下来我们就来看看它到底是如何实现的。

明确了 Spring 创建 Bean 的三步骤之后,我们再来看看它为单例搞的三个 map:

    一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)

    二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean

    三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存

这三个 map 是如何配合的呢?

    首先,获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2。

    看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3

    去 singletonFactories (三级缓存)通过 BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,并且放置到 earlySingletonObjects 中。

    如果三个缓存都没找到,则返回 null。

从上面的步骤我们可以得知,如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。

返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean。

doCreateBean 这个方法就会执行上面我们说的三步骤:

    实例化

    属性注入

    初始化

在实例化 Bean 之后, 会往 singletonFactories 塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean 。

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

要注意,此时 Spring 是不知道会不会有循环依赖发生的, 但是它不管 ,反正往 singletonFactories 塞这个工厂,这里就是 提前暴露 。

然后就开始执行属性注入,这个时候 A 发现需要注入 B,所以去 getBean(B),此时又会走一遍上面描述的逻辑,到了 B 的属性注入这一步。

此时 B 调用 getBean(A),这时候一级缓存里面找不到,但是发现 A 正在创建中的,于是去二级缓存找,发现没找到,于是去三级缓存找,然后找到了。

并且通过上面提前在三级缓存里暴露的工厂得到 A,然后将这个工厂从三级缓存里删除,并将 A 加入到二级缓存中。

然后结果就是 B 属性注入成功。

紧接着 B 调用 initializeBean 初始化,最终返回,此时 B 已经被加到了一级缓存里 。

这时候就回到了 A 的属性注入,此时注入了 B,接着执行初始化,最后 A 也会被加到一级缓存里,且从二级缓存中删除 A。

Spring 解决依赖循环就是按照上面所述的逻辑来实现的。

重点就是在对象实例化之后,都会在三级缓存里加入一个工厂,提前对外暴露还未完整的 Bean,这样如果被循环依赖了,对方就可以利用这个工厂得到一个不完整的 Bean,破坏了循环的条件。

为什么循环依赖需要三级缓存,二级不够吗?

上面都说了那么多了,那我们思考下,解决循环依赖需要三级缓存吗?

很明显,如果仅仅只是为了破解循环依赖,二级缓存够了,压根就不必要三级。

你思考一下,在实例化 Bean A 之后,我在二级 map 里面塞入这个 A,然后继续属性注入。

发现 A 依赖 B 所以要创建 Bean B,这时候 B 就能从二级 map 得到 A ,完成 B 的建立之后, A 自然而然能完成。

所以 为什么要搞个三级缓存,且里面存的是创建 Bean 的工厂呢 ?

我们来看下调用工厂的 getObject 到底会做什么,实际会调用下面这个方法:

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

重点就在中间的判断,如果 false,返回就是参数传进来的 bean,没任何变化。

如果是 true 说明有 InstantiationAwareBeanPostProcessors 。

且循环的是 smartInstantiationAware 类型, 如有这个 BeanPostProcessor 说明 Bean 需要被 aop 代理 。

我们都知道如果有代理的话,那么我们想要直接拿到的是代理对象。

也就是说如果 A 需要被代理,那么 B 依赖的 A 是已经被代理的 A,所以我们不能返回 A 给 B,而是返回代理的 A 给 B。

这个工厂的作用就是判断这个对象是否需要代理,如果否则直接返回,如果是则返回代理对象。

看到这明白的小伙伴肯定会问,那跟三级缓存有什么关系,我可以在要放到二级缓存的时候判断这个 Bean 是否需要代理,如果要直接放代理的对象不就完事儿了。

是的,这个思路看起来没任何问题, 问题就出在时机 ,这跟 Bean 的生命周期有关系。

正常代理对象的生成是基于后置处理器,是 在被代理的对象初始化后期调用生成的 , 所以如果你提早代理了其实是违背了 Bean 定义的生命周期 。

所以 Spring 先在一个三级缓存放置一个工厂,如果产生循环依赖,那么就调用这个工厂提早得到代理对象。

如果没产生依赖,这个工厂根本不会被调用,所以 Bean 的生命周期就是对的。

至此,我想你应该明白为什么会有三级缓存了。

也明白,其实破坏循环依赖,其实只有二级缓存就够了,但是碍于生命周期的问题,提前暴露工厂延迟代理对象的生成。

对了,不用担心三级缓存因为没有循环依赖,数据堆积的问题,最终单例 Bean 创建完毕都会加入一级缓存,此时会清理下面的二、三级缓存。

最后

好了,看到这里想必你应该对 Spring 的循环依赖很清晰了,并且面试的时候肯定也难不倒你了。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/759990.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号