Spring压轴题:当循环依赖遇上Spring AOP

目录

  • 前言

  • 源码分析

前言

问: Spring 如何解决循环依赖?

答: Spring 通过提前曝光机制,利用三级缓存解决循环依赖(这原理还是挺简单的,参考:三级缓存、图解循环依赖原理)

再问: Spring 通过提前曝光,直接曝光到二级缓存已经可以解决循环依赖问题了,为什么一定要三级缓存?

再细问: 如果循环依赖的时候,所有类又都需要 Spring AOP 自动代理,那 Spring 如何提前曝光?曝光的是原始 bean 还是代理后的 bean?

这些问题算是 Spring 源码的压轴题了,如果这些问题都弄明白,恭喜你顺利结业 Spring 源码了。就单单对 Spring 这一块的理解,不夸张的说可以达到阿里水准了。

源码分析

进入正题,在 Spring 创建 Bean 的核心代码 doGetBean 中,在实例化 bean 之前,会先尝试从三级缓存获取 bean,这也是 Spring 解决循环依赖的开始。

| 缓存中获取 bean

// AbstractBeanFactory.java
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
            @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

        final String beanName = transformedBeanName(name);
        Object bean;

        // 2. 尝试从缓存中获取bean
        Object sharedInstance = getSingleton(beanName);
        ...
}
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 从一级缓存获取,key=beanName value=bean
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {
                // 从二级缓存获取,key=beanName value=bean
                singletonObject = this.earlySingletonObjects.get(beanName);
                // 是否允许循环引用
                if (singletonObject == null && allowEarlyReference) {
                    /**
                     * 三级缓存获取,key=beanName value=objectFactory,objectFactory中存储getObject()方法用于获取提前曝光的实例
                     *
                     * 而为什么不直接将实例缓存到二级缓存,而要多此一举将实例先封装到objectFactory中?
                     * 主要关键点在getObject()方法并非直接返回实例,而是对实例又使用
                     * SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法对bean进行处理
                     *
                     * 也就是说,当spring中存在该后置处理器,所有的单例bean在实例化后都会被进行提前曝光到三级缓存中,
                     * 但是并不是所有的bean都存在循环依赖,也就是三级缓存到二级缓存的步骤不一定都会被执行,有可能曝光后直接创建完成,没被提前引用过,
                     * 就直接被加入到一级缓存中。因此可以确保只有提前曝光且被引用的bean才会进行该后置处理
                      */
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        /**
                         * 通过getObject()方法获取bean,通过此方法获取到的实例不单单是提前曝光出来的实例,
                         * 它还经过了SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法处理过。
                         * 这也正是三级缓存存在的意义,可以通过重写该后置处理器对提前曝光的实例,在被提前引用时进行一些操作
                          */
                        singletonObject = singletonFactory.getObject();
                        // 将三级缓存生产的bean放入二级缓存中
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        // 删除三级缓存
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return singletonObject;
    }

三级缓存分别是:

singletonObject: 一级缓存,该缓存 key = beanName, value = bean;这里的 bean 是已经创建完成的,该 bean 经历过实例化->属性填充->初始化以及各类的后置处理。因此,一旦需要获取 bean 时,我们第一时间就会寻找一级缓存。

earlySingletonObjects: 二级缓存,该缓存 key = beanName, value = bean;这里跟一级缓存的区别在于,该缓存所获取到的 bean 是提前曝光出来的,是还没创建完成的。

也就是说获取到的 bean 只能确保已经进行了实例化,但是属性填充跟初始化肯定还没有做完,因此该 bean 还没创建完成,仅仅能作为指针提前曝光,被其他 bean 所引用。

singletonFactories: 三级缓存,该缓存 key = beanName, value = beanFactory;在 bean 实例化完之后,属性填充以及初始化之前,如果允许提前曝光,spring 会将实例化后的 bean 提前曝光,也就是把该 bean 转换成 beanFactory 并加入到三级缓存。

在需要引用提前曝光对象时再通过 singletonFactory.getObject() 获取。

这里抛出问题,如果我们直接将提前曝光的对象放到二级缓存 earlySingletonObjects,Spring 循环依赖时直接取就可以解决循环依赖了,为什么还要三级缓存 singletonFactory 然后再通过 getObject() 来获取呢?这不是多此一举?

| 三级缓存的添加

我们回到添加三级缓存,添加 SingletonFactory 的地方,看看 getObject() 到底做了什么操作?

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

可以看到在返回 getObject() 时,多做了一步 getEarlyBeanReference 操作,这步操作是 BeanPostProcess 的一种,也就是给子类重写的一个后处理器,目的是用于被提前引用时进行拓展。

即:曝光的时候并不调用该后置处理器,只有曝光,且被提前引用的时候才调用,确保了被提前引用这个时机触发。

| 提前曝光代理 earlyProxyReferences

因此所有的重点都落到了 getEarlyBeanReference 上,getEarlyBeanReference 方法是 SmartInstantiationAwareBeanPostProcessor 所规定的接口。

再通过 UML 的类图查看实现类,仅有 AbstractAutoProxyCreator 进行了实现。也就是说,除了用户在子类重写,否则仅有 AbstractAutoProxyCreator 一种情况。

   // AbstractAutoProxyCreator.java
    public Object getEarlyBeanReference(Object bean, String beanName) {
        // 缓存当前bean,表示该bean被提前代理了
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        this.earlyProxyReferences.put(cacheKey, bean);
        // 对bean进行提前Spring AOP代理
        return wrapIfNecessary(bean, beanName, cacheKey);
    }

wrapIfNecessary 是用于 Spring AOP 自动代理的。Spring 将当前 bean 缓存到 earlyProxyReferences 中标识提前曝光的 bean 在被提前引用之前,然后进行了 Spring AOP 代理。

但是经过 Spring AOP 代理后的 bean 就已经不再是原来的 bean 了,经过代理后的 bean 是一个全新的 bean,也就是说代理前后的 2 个 bean 连内存地址都不一样了。

这时将再引出新的问题:B 提前引用 A 将引用到 A 的代理,这是符合常理的,但是最原始的 bean A 在 B 完成创建后将继续创建,那么 Spring Ioc 最后返回的 Bean 是 Bean A 呢还是经过代理后的 Bean 呢?

这个问题我们得回到 Spring AOP 代理,Spring AOP 代理时机有 2 个:

  • 当自定义了 TargetSource,则在 bean 实例化前完成 Spring AOP 代理并且直接发生短路操作,返回 bean。

  • 正常情况下,都是在 bean 初始化后进行 Spring AOP 代理。

  • 如果要加上今天说的提前曝光代理,getEarlyBeanReference 可以说 3 种。

第一种情况就没什么好探究的了,直接短路了,根本没有后续操作。而我们关心的是第二种情况,在 Spring 初始化后置处理器中发生的 Spring AOP 代理。

  public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
            throws BeansException {

        Object result = existingBean;
        for (BeanPostProcessor processor : getBeanPostProcessors()) {
            // 调用bean初始化后置处理器处理
            Object current = processor.postProcessAfterInitialization(result, beanName);
            if (current == null) {
                return result;
            }
            result = current;
        }
        return result;
    }
   // AbstractAutoProxyCreator.java
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
        if (bean != null) {
            // 获取缓存key
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            // 查看该bean是否被Spring AOP提前代理!而缓存的是原始的bean,因此如果bean被提前代理过,这此处会跳过
            // 如果bean没有被提前代理过,则进入AOP代理
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
        return bean;
    }

earlyProxyReferences 是不是有点熟悉,是的,这就是我们刚刚提前曝光并且进行 Spring AOP 提前代理时缓存的原始 bean,如果缓存的原始 bean 跟当前的 bean 是一至的,那么就不进行 Spring AOP 代理了!返回原始的 bean。

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
            throws BeanCreationException {
        try {
            //
            /**
             * 4. 填充属性
             * 如果@Autowired注解属性,则在上方完成解析后,在这里完成注入
             *
             * @Autowired
             * private Inner inner;
             */
            populateBean(beanName, mbd, instanceWrapper);
            // 5. 初始化
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        }
        catch (Throwable ex) {
            if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
                throw (BeanCreationException) ex;
            }
            else {
                throw new BeanCreationException(
                        mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
            }
        }

        // 6. 存在提前曝光情况下
        if (earlySingletonExposure) {
            // earlySingletonReference:二级缓存,缓存的是经过提前曝光提前Spring AOP代理的bean
            Object earlySingletonReference = getSingleton(beanName, false);
            if (earlySingletonReference != null) {
                // exposedObject跟bean一样,说明初始化操作没用应用Initialization后置处理器(指AOP操作)改变exposedObject
                // 主要是因为exposedObject如果提前代理过,就会跳过Spring AOP代理,所以exposedObject没被改变,也就等于bean了
                if (exposedObject == bean) {
                    // 将二级缓存中的提前AOP代理的bean赋值给exposedObject,并返回
                    exposedObject = earlySingletonReference;
                }
                // 引用都不相等了,也就是现在的bean已经不是当时提前曝光的bean了
                else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                    // dependentBeans也就是B, C, D
                    String[] dependentBeans = getDependentBeans(beanName);
                    Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
                    for (String dependentBean : dependentBeans) {
                        if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                            actualDependentBeans.add(dependentBean);
                        }
                    }
                    // 被依赖检测异常
                    if (!actualDependentBeans.isEmpty()) {
                        throw new BeanCurrentlyInCreationException(beanName,
                                "Bean with name '" + beanName + "' has been injected into other beans [" +
                                StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                                "] in its raw version as part of a circular reference, but has eventually been " +
                                "wrapped. This means that said other beans do not use the final version of the " +
                                "bean. This is often the result of over-eager type matching - consider using " +
                                "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
                    }
                }
            }
        }

这个时候我们需要理清一下 3 个变量:

  • earlySingletonReference: 二级缓存,缓存的是经过提前曝光提前 AOP 代理的 bean

  • bean: 这个就是经过了实例化、填充、初始化的 bean

  • exposedObject: 这个是经过了 AbstractAutoProxyCreator 的 postProcessAfterInitialization 处理过后的 bean,但是在其中因为发现当前 bean 已经被 earlyProxyReferences 缓存,所以并没有进行 AOP 处理,而是直接跳过,因此还是跟第 2 点一样的 bean

理清这 3 个变量以后,就会发现,exposedObject = earlySingletonReference;

AOP 代理过的 Bean 赋值给了 exposedObject 并 返回,这时候用户拿到的 bean 就是 AOP 代理过后的 bean 了,一切皆大欢喜了。

但是中间还有一个问题!提前曝光的 bean 在提前引用时被 Spring AOP 代理了,但是此时的 bean 只是经过了实例化的 bean,还没有进行 @Autowire 的注入啊!也就是说此时代理的 bean 里面自动注入的属性是空的!

| 提前 AOP 代理对象的 属性填充、初始化

是的,确实在 Spring AOP 提前代理后没有经过属性填充和初始化。那么这个代理又是如何保证依赖属性的注入的呢?

答案回到 Spring AOP 最早最早讲的 JDK 动态代理上找,JDK 动态代理时,会将目标对象 target 保存在最后生成的代理 proxy 中,当调用proxy 方法时会回调 h.invoke,而 h.invoke 又会回调目标对象 target 的原始方法。

因此,其实在 Spring AOP 动态代理时,原始 bean 已经被保存在提前曝光代理中了。 而后原始 Bean 继续完成属性填充和初始化操作。

因为 AOP 代理 $proxy 中保存着 traget 也就是是原始 bean 的引用,因此后续原始 bean 的完善,也就相当于 Spring AOP 中的 target 的完善,这样就保证了 Spring AOP 的属性填充与初始化了!

| 循环依赖遇上 Spring AOP 图解

为了帮助大家理解,这里灵魂画手画张流程图帮助大家理解:

image.png

首先又 bean A,bean B,他们循环依赖注入,同时 bean A 还需要被 Spring AOP 代理,例如事务管理或者日志之类的操作。 原始 bean A,bean B 图中用 a,b 表示,而代理后的 bean A 我们用 aop.a 表示 。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容