解析Spring中的循环依赖问题:(AOP)再探三级缓存

前言

在之前的内容中,我们简要探讨了循环依赖,并指出仅通过引入二级缓存即可解决此问题。然而,你可能会好奇为何在Spring框架中还需要引入三级缓存singletonFactories。在前述总结中,我已经提供了答案,即AOP代理对象。接下来,我们将深入探讨这一话题。

AOP

在Spring框架中,AOP的实现是通过一个名为BeanPostProcessor的类完成的,其中一个关键的BeanPostProcessor就是AnnotationAwareAspectJAutoProxyCreator。值得一提的是,该类的父类是AbstractAutoProxyCreator。在Spring的AOP机制中,通常会使用JDK动态代理或者CGLib动态代理来实现代理对象的生成。因此,如果在某个类的方法上设置了切面,那么最终这个类将需要生成一个代理对象来应用AOP的功能。

一般的执行流程通常是这样的:A类--->生成一个普通对象-->属性注入-->基于切面生成一个代理对象-->将该代理对象存入singletonObjects单例池中。

而AOP可以说是Spring框架中除了IOC之外的另一个重要功能,而循环依赖则属于IOC的范畴。因此,为了让这两个重要功能同时存在于Spring框架中,Spring需要进行特殊处理。

三级缓存

在处理这种情况时,Spring框架利用了第三级缓存singletonFactories。下面我们来看一下关于三级缓存的源代码实现:

java 复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // Quick check for existing instance without full singleton lock
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                synchronized (this.singletonObjects) {
                    // Consistent creation of early reference within full singleton lock
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                singletonObject = singletonFactory.getObject();
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
        return singletonObject;
    }

首先,singletonFactories中存储的是某个beanName对应的ObjectFactory。在bean的生命周期中,生成完原始对象之后,Spring框架会构造一个ObjectFactory并将其存入singletonFactories中。这个ObjectFactory是一个函数式接口,因此支持Lambda表达式,形式为() -> getEarlyBeanReference(beanName, mbd, bean)。为了更清晰地理解这个过程,我提供一张图片。

getEarlyBeanReference

在上述Lambda表达式中,它实际上代表了一个ObjectFactory,执行该Lambda表达式将会调用getEarlyBeanReference方法。下面是getEarlyBeanReference方法的实现:

java 复制代码
//AbstractAutowireCapableBeanFactory
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;
}

在整个Spring框架中,值得注意的是,只有AbstractAutoProxyCreator这个类在实现getEarlyBeanReference方法时才具有真正的意义。这个类专门用于处理AOP(面向切面编程)。

java 复制代码
// 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);
}

那么,getEarlyBeanReference方法的具体操作是什么呢? 首先,它会获取一个cachekey,这个cachekey实际上就是beanName。 接着,它会将beanName和bean(即原始对象)存储到earlyProxyReferences中。 接下来,它会调用wrapIfNecessary方法进行AOP操作,这将生成一个代理对象。

那么,什么时候会调用getEarlyBeanReference方法呢?让我们再次回到循环依赖的场景中。

在我上一节的基础上,我增加了两句话,以便更好地理解触发缓存机制以解决AOP代理对象生成的时机。

一旦原始对象通过构造方法生成后,会被存储到三级缓存中,并且会与一个lambda表达式关联。然而,在这个阶段,它并不会被执行。

一旦BBean需要ABean时,系统会首先查看三级缓存以确定是否存在缓存。如果存在缓存,则lambda表达式将会被执行,其代码已在前面展示过。该lambda表达式的目的是将代理对象放入earlySingletonObjects中。需要注意的是,此时代理对象并未被放入singletonObjects中。那么代理对象何时会被放入singletonObjects中呢?

这个时候你可能已经明白了earlySingletonObjects的用途。由于只获取了A原始对象的代理对象,这个代理对象并不完整,因为A原始对象尚未进行属性填充。因此,在这种情况下,我们不能直接将A的代理对象放入singletonObjects中。因此,我们只能将代理对象放入earlySingletonObjects,这样依次类推。

在Spring框架中,在循环依赖场景下,当Bean B创建完成后,Bean A继续其生命周期。在Bean A完成属性注入后,根据其自身逻辑进行AOP操作。此时,我们知道Bean A的原始对象已经经历了AOP处理,因此对于Bean A本身而言,不需要再次进行AOP。那么,如何确定一个对象是否已经经历了AOP呢?

java 复制代码
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            return wrapIfNecessary(bean, beanName, cacheKey);
        }
    }
    return bean;
}

没错,这个earlyProxyReferences确实提前缓存了对象是否已经被代理过,这样就避免了重复的AOP处理。

举例反证

那么问题来了,当A对象创建时,它可以是原始对象,但当B对象创建时,却成功创建了A的代理对象。然后再回头给A对象进行属性注入和初始化,这些操作似乎与代理对象无关?

这个问题涉及到了 Spring 中动态代理的实现。无论是使用cglib代理还是jdk动态代理生成的代理类,代理时都会将目标对象 target 保存在最终生成的代理 $proxy 中。你可以将代理对象看作是对原始对象地址的一层包装,最终仍然会回到原始对象上。因此,对原始bean的进一步完善实际上也就是对代理对象的完善。

还有一个需要注意的问题,当A创建时,由于earlyProxyReferences缓存的原因,并没有创建代理对象,因此此时A仍然保持为原始对象。我们知道,当bean创建完成后,它将被放入一级缓存中,但如果在此之后被其他对象引用,那不就会出现问题吗?别人引用的都是原始对象了,而不是代理对象,但是请不要着急,因为在实例化之后,有一行代码可以解决这个问题。

java 复制代码
if (earlySingletonExposure) {
            Object earlySingletonReference = getSingleton(beanName, false);
            if (earlySingletonReference != null) {
                if (exposedObject == bean) {
                    exposedObject = earlySingletonReference;
                }
......省略代码

在实例化原始对象后,他会首先从三级缓存中检查是否存在缓存对象。这是因为在创建B对象时,已经将A的代理对象放入二级缓存。因此,取出的对象是代理对象。接着,当进行 exposedObject == bean 的比较时,发现它们不相同。因此,以代理对象为准并将其返回。最终,最外层存储的将是代理对象。

java 复制代码
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

总结

在上一个章节中我们提到了今天要讨论三级缓存,让我们根据上面提到的三级缓存内容,做一个详尽的总结:

  • singletonObjects:缓存经过了完整生命周期的bean。

  • earlySingletonObjects缓存了未经过完整生命周期的bean。当某个bean出现循环依赖时,该bean会被提前放入earlySingletonObjects中。如果该bean需要经过AOP,那么代理对象将会被放入earlySingletonObjects;否则,原始对象将被放入其中。然而,无论是代理对象还是原始对象,它们的生命周期都尚未完全结束。因此视为未经过完整生命周期的bean。

  • singletonFactories缓存的是一个ObjectFactory,这个ObjectFactory实际上是一个Lambda表达式。在每个Bean的生成过程中,当原始对象实例化完成后,会提前基于原始对象生成一个Lambda表达式,并将其保存到三级缓存中。这个Lambda表达式可能会被使用,也可能不会。如果当前Bean不存在循环依赖,那么这个Lambda表达式将不会被使用,当前的Bean将按照正常的生命周期执行完毕,并将自身放入singletonObjects中。但是,如果在依赖注入的过程中发现了循环依赖(即当前正在创建的Bean被其他Bean所依赖),则会从三级缓存中取出Lambda表达式,并执行它以获取一个对象,然后将得到的对象放入二级缓存中。需要特别注意的是,如果当前Bean需要AOP处理,则执行Lambda表达式后得到的将是代理对象;否则,直接得到的是原始对象。

当涉及Spring框架中动态代理的实现机制时,除了已经提到的earlySingletonObjects和singletonFactories这两个缓存外,还有一个重要的缓存值得一提,那就是earlyProxyReferences。这个缓存的作用在于记录某个原始对象是否已经进行过AOP(面向切面编程)处理。

至此,整个循环依赖解决完毕。

相关推荐
小_太_阳12 分钟前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾15 分钟前
scala借阅图书保存记录(三)
开发语言·后端·scala
星就前端叭1 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者2 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu
从善若水2 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust
机器之心3 小时前
终于等来能塞进手机的文生图模型!十分之一体量,SnapGen实现百分百的效果
人工智能·后端
机器之心3 小时前
首次!大模型自动搜索人工生命,做出AI科学家的Sakana AI又放大招
人工智能·后端
运维&陈同学3 小时前
【模块一】kubernetes容器编排进阶实战之基于velero及minio实现etcd数据备份与恢复
数据库·后端·云原生·容器·kubernetes·etcd·minio·velero
waterme1onY4 小时前
Spring AOP 中记录日志
java·开发语言·笔记·后端