三级?是缓存不是片! spring的三级缓存---学习spring框架第一步

文章目录


前言

提示:这里可以添加本文要记录的大概内容:

spring的三级缓存,一个很有意思的实现方式:其实他仅仅是spring类加载过程中的一个很小的点:

从一下几个点来描述三级缓存:

1、解决了什么问题

2、是怎么解决的

3、反思总结

至于后边的补充什么的,看自己兴趣看来,想看就多看看,没有坏处


提示:以下是本篇文章正文内容,下面案例可供参考

一、解决了什么问题?

Spring的三级缓存是Spring框架用来解决Bean的循环依赖问题的一种机制。

首先要知道,在Spring中,对象(Bean)的创建并不是一蹴而就的,它需要经历创建对象、填充属性、初始化等步骤。

有些时候,对象A在创建的过程中需要用到对象B,而对象B又需要用到对象A,这就产生了循环依赖的问题。

来个贴近生活的例子--要是了解的可以直接忽略

一级缓存:就像一个已经处理完毕、准备随时可以拿给客户的邮包区。在Spring中,这是一个叫做singletonObjects的缓存,里面存放的是完全初始化好的Bean。
二级缓存:想象一个邮包已经包装好了,但是还没有贴邮票,也就是还不能完全交给客户。在Spring中,这是一个叫做earlySingletonObjects的缓存,存放的是已经实例化但还没有完成依赖注入和初始化的Bean。
三级缓存:这就像一个邮包的框架已经做好,但是里面的商品还没放进去。在Spring中,它是一个叫做singletonFactories的缓存,存放的是一个对象工厂,这个工厂知道如何创建Bean,但是Bean还没有真正创建出来。
现在假设有两个邮包A和B,它们需要彼此的内容来完成。邮局工作人员先拿到邮包A,发现需要B里面的东西,于是就去查看邮包B。如果B还在三级缓存里,邮局工作人员就可以用B的框架(工厂)先把B的外面状况弄出来,然后用这个未完全准备好的B去填充A。完成对A的填充后,A就可以移到二级缓存。然后工作人员又回来继续处理B,等B也准备到可以交给客户的程度时,它也被移到一级缓存里。

通过这个过程,邮局即使面对需要彼此内容的邮包也能处理,并且确保每个邮包最终都能完整无缺地到达客户手中。同理,在Spring中,即便Bean之间有循环依赖,三级缓存机制也能确保所有Bean都能正确创建并注入所需的依赖。

二、是怎么解决的

Spring通过引入三级缓存来解决循环依赖的问题,具体操作如下:

一级缓存(singletonObjects):存放已经完全初始化的Bean实例。当一个Bean经历完整个生命周期,包括创建、属性赋值、初始化方法调用后,它将被存放在这里,随时可以使用。

二级缓存(earlySingletonObjects):存放早期暴露的Bean实例 ,即尚未完全初始化但已经进行了实例化(调用了构造函数)和部分属性赋值的Bean。这样的Bean还没有执行初始化方法,但已经有足够的信息供其他Bean使用(主要用来解决循环依赖)。

三级缓存(singletonFactories):存放Bean工厂对象。这个工厂中有方法能返回一个Bean的早期引用(early reference),当Bean还在创建过程中,如果另一个Bean请求它时,可以通过这个工厂得到一个原始状态的Bean引用。

在Bean的创建过程中,Spring会先尝试从一级缓存中获取所需的Bean,如果获取不到,则会到二级缓存中查找,如果还是没有,最后会去三级缓存中尝试通过Bean工厂得到Bean的早期引用。在这个过程中,如果发现了循环依赖,Spring会通过提前暴露一个创建中的Bean(放入到二级或三级缓存)来打破这个循环

三、反思总结

Spring的三级缓存机制有效地解决了循环依赖的问题

使得Bean的创建和依赖注入变得更加灵活和稳定。

这种设计允许开发人员在不必考虑Bean相互依赖顺序的情况下组织他们的代码

从而大大减少了配置的复杂性。

然而,值得注意的是,三级缓存主要解决的是单例Bean之间的循环依赖。

如果是原型Bean之间存在循环依赖,Spring容器则无法为其提供支持,

这种情况仍然需要开发者自己处理。

从设计的角度来看,Spring的三级缓存体现了设计模式中的"单一原则"和"开闭原则",为Bean的生命周期管理和容器的扩展性提供了良好的支持。

这也是Spring框架能够在Java企业应用开发中广泛应用并得到认可的一个原因。

你以为结束了吗?----no 源码还没开始分析了

源码分析:

Spring框架中处理循环依赖并实现三级缓存机制的核心是在DefaultSingletonBeanRegistry这个类中。以下是对这块源码的分析概述,重点关注创建单例Bean的过程与三级缓存的作用。

DefaultSingletonBeanRegistry 类

这个类实现了SingletonBeanRegistry接口,并提供管理单例Bean缓存的通用方法。三级缓存主要在以下几个属性中体现:

1、singletonObjects:存放完全初始化完成的Bean。

2、earlySingletonObjects:存放提前暴露的Bean,尚未填充属性。

3、singletonFactories:存放Bean工厂,用来创建Bean的早期引用。

getSingleton 方法

这个方法是获取单例Bean的主要入口。当我们调用容器的getBean方法时,最终会到达这个方法,它负责尝试从缓存中获取Bean,或者通过Bean工厂创建Bean。

java 复制代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 尝试从singletonObjects缓存中获取Bean实例
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果一级缓存中不存在,并且当前Bean正在创建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 二级缓存中获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 如果二级缓存中也没有,并且允许提前暴露引用
            if (singletonObject == null && allowEarlyReference) {
                // 从三级缓存中获取对应的ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 通过ObjectFactory获取Bean的早期引用
                    singletonObject = singletonFactory.getObject();
                    // 将早期引用放入二级缓存,并从三级缓存中移除
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

三级缓存的作用

在Bean创建过程中,如果该Bean依赖于其他正在创建中的Bean,就需要通过三级缓存机制来解决:

1、首先,当开始创建Bean A时,Spring会标记Bean A正在创建中。

2、在创建Bean A的过程中,如果需要注入Bean B,Spring会尝试获取Bean B。

3、如果Bean B也在创建中(例如它们互相依赖),那么Spring会从singletonFactories这个三级缓存中通过ObjectFactory获取到Bean B的早期引用。

4、然后,将Bean B的早期引用(尚未完全初始化)放入earlySingletonObjects这个二级缓存中,并移除对应的三级缓存项。

5、使用这个早期的Bean B完成对Bean A的注入,继续Bean A的创建过程。

6、一旦Bean A和Bean B均初始化完成,它们就会被存放进singletonObjects这个一级缓存中等待使用。

反思总结

Spring的循环依赖处理展示了它在设计上的深思熟虑和高度的工程性。三级缓存所带来的复杂度是一种典型的在框架层面解决问题的例子,使得使用该框架的开发者不必自行处理这些底层的复杂情况。然而,它同时也表明了为什么通常建议在设计应用程序时还是尽量避免这种高层次的循环依赖。

需要注意的是,要完整理解并跟踪整个Bean的生命周期处理流程需要非常细致地研究AbstractAutowireCapableBeanFactory类及相关组件的实现,这个源码分析仅提供了对处理过程及三级缓存机制的粗略理解。

面试点:

1、三级缓存原理:如上

2、二级缓存就可以解决了为什么还要有三级缓存

这是一个很好的问题,确实在理解三级缓存时很多人会有这样的疑问。要回答这个问题,我们需要更深入地了解Spring的Bean生命周期管理,以及早期引用(early reference)的概念

为什么需要早期引用?

在Spring中,早期引用是指一个Bean实例在完成完全初始化(也就是执行了初始化方法、BeanPostProcessor的前置处理和后置处理等)之前的一个原始状态的引用。这个原始状态下的Bean已经实例化,但许多初始化逻辑(比如AOP代理)还没有应用。在处理循环依赖时,正是需要这样一个原始状态的Bean来打破依赖循环。

二级缓存 vs. 三级缓存

二级缓存确实存储了Bean的早期引用,但是它所存储的Bean可能已经被部分处理过(例如已经注入了一些属性)。如果此时有另一个Bean请求这个正在创建的Bean,它得到的将是一个不完整的状态的实例。

三级缓存的存在则是为了解决这个问题:三级缓存中存放的是能够生成Bean早期引用的ObjectFactory,而不是Bean的引用本身。这使得Spring在创建Bean的过程中有了更多的控制权,它可以决定何时生成一个早期引用。这有以下几个好处:

维护Bean生命周期的完整性:通过三级缓存中的ObjectFactory,Spring只在必要的时候生成早期引用,确保即使在循环依赖的场景下,每个Bean的生命周期事件也能按正确的顺序触发。

避免过早暴露原始Bean:如果没有三级缓存,当循环依赖发生时,Bean的早期引用会被早早放入二级缓存中,这意味着其他Bean或者BeanPostProcessors可能会过早地操作这个原始状态的Bean,从而造成不可预知的副作用。

允许进行更多的Bean处理操作:通过ObjectFactory,Spring可以在生成早期引用之前应用更多的Bean处理逻辑。例如,在生成代理对象之前的处理就可以在这一步骤中完成。

资源和复杂度考虑

至于资源浪费和代码复杂度的问题,实际上因为Spring框架的设计目标是适用广泛的场景和提供灵活的解决方案,所以它对于这些额外的复杂度是有充分考量的。第三级缓存让Spring能够处理更复杂的依赖情况,同时也使得框架本身在功能和扩展性上更加强大。而这一点对于维护大型企业级应用是非常重要的。

不过,在大多数应用中,循环依赖并不是一个常见的现象,且当发生循环依赖时,通常是设计上的问题。因此,尽管三级缓存提供了解决循环依赖的方法,开发者还是应当尽量避免这种依赖关系的发生,通过合理设计来减少对这类复杂特性的依赖。

总结来说

三级缓存机制的引入是对Spring框架健壮性的一种体现,它确保即使在出现循环依赖这种极端情形时,也能够保持Bean生命周期的正常管理和控制,并且它是在一个非常细致和深入的层面上考虑问题。对于那些需要同时处理成千上万个Bean的复杂应用来说,这种设计无疑是有价值的。

三级缓存是为了解决代理模式的这个对吗?-- no

三级缓存主要是为了解决循环依赖的问题,

并不是专为代理模式设计的。

然而,它在处理Spring AOP中的代理对象创建时起到了重要作用。让我们进一步解释这一点。

在Spring中,AOP(面向切面编程)通常通过代理模式实现,其中一个Bean在被创建时,可能会被包装成一个代理对象。这个代理对象会拦截对目标Bean的调用,以便添加额外的行为,比如事务管理、安全检查或日志记录等。

在Bean创建和依赖注入的过程中,特别是当涉及到AOP代理时,我们可能遇到以下情况:

Bean A依赖Bean B,而Bean B依赖Bean A。(循环依赖)

Bean B需要被AOP代理。

在Bean A创建并请求注入Bean B的引用时,Bean B尚未初始化完毕,特别是还未应用AOP增强。

通过三级缓存,Spring确保即使在Bean B被请求时还处于创建状态,Bean A也可以获取到一个对Bean B的引用(可能是未代理的引用)。这个引用是通过三级缓存中的ObjectFactory的getObject()方法产生的。

这个过程中,三级缓存允许Spring:

在必要时创建Bean的早期引用(early reference),即使这个Bean将来会被代理。

保持控制权,以便在最终Bean实例化后,能使用正确的代理将其注入到依赖它的其他Bean中。

因此,尽管三级缓存不是专为代理或AOP设计的,它对于在存在代理时处理Bean创建和依赖注入仍然是非常关键的。它提供了一种机制,确保即使在这些复杂场景下,依赖的Bean仍然能够被正确创建和注入。

总而言之,三级缓存的设计让Spring在创建Bean时具有更大的灵活性,能够适应包括AOP在内的各种场景。它确保了循环依赖的Bean可以被正确处理,同时也支持了必要的AOP相关的处理。

补充个case: A类,对应代理类为Ap B类对应代理类为Bp--可以忽略

在Spring框架中使用代理模式(比如通过AOP来实现),最终结果在存在循环依赖的情景下应该是这样的:

1、A类 和 B类 存在循环依赖,也就是说A的创建依赖于B而B的创建也依赖于A。

2、Ap 是 A类 的代理类,Bp 是 B类 的代理类。

当Spring容器启动时,假设它首先开始创建A类的实例。在A类的创建过程中,它发现需要一个B类的实例。此时Spring容器开始创建B类的实例。如果我们没有三级缓存,那么在这一步我们就会陷入无限递归的循环依赖中。

但得益于三级缓存的存在,流程会是这样的:

容器开始创建A类的实例,第一步,它会将A类的ObjectFactory放入三级缓存singletonFactories中。

在实例化A类的过程中,容器识别出A类需要一个B类的实例。因此,它开始实例化B类。

在创建B类实例的过程中,容器发现B类需要一个A类实例(循环依赖)。此时容器会去三级缓存中查找A类的ObjectFactory。

容器使用A类的ObjectFactory创建一个对A类的早期引用,并且将该早期引用放入二级缓存earlySingletonObjects中,同时从三级缓存中移除A类的ObjectFactory。

这个早期引用被注入到B类实例中,此时可以继绀B类的创建流程。

当B类创建完毕,它有可能会被一个或多个BeanPostProcessor处理,这其中可能包括创建AOP代理的处理器,所以B类实例可能会被替换成Bp(B类的代理类)。

现在B类或B类的代理(Bp)已经创建完毕,container可以继续A类实例的创建过程。

和B类一样,A类在全部属性设置完毕之后也可能会通过BeanPostProcessor被替换成它的代理类Ap。

最终,容器会将创建完毕的A类代理(Ap)和B类代理(Bp)存入到一级缓存singletonObjects中,供后续使用。

最终结果就是你会得到一个A类的代理Ap和一个B类的代理Bp,并且它们是彼此相互注入的,即使它们存在循环依赖。这样,Spring容器通过三级缓存巧妙地处理了循环依赖的问题,并确保了AOP中的代理也能被正确地创建和应用。

补充2

在Spring框架中处理代理类和循环依赖的场景时,如果你在A类中注入了B类,并且B类需要被代理(例如通过Spring AOP),那么A类中的属性最终应该是B类的代理Bp。这是因为在Spring的AOP中,代理包装通常发生在Bean初始化后的后处理步骤中。

整个流程是这样的:

当Bean A被实例化,它在构造函数或者通过setter方法需要注入Bean B的引用。

如果此时Bean B尚未创建,Spring容器将开始Bean B的创建流程。

如果Bean B在其创建过程中又需要注入Bean A的引用(循环依赖),Spring容器利用三级缓存提供了Bean A的一个早期引用(这可能是一个原始A的实例,而不是代理)。

在Bean B的创建过程完成后,通常会进行后处理,该后处理可能会应用AOP代理创建逻辑(例如通过BeanPostProcessor实现),这时Bean B将被替换为其代理Bp。

这个代理Bp会被注入到Bean A中完成循环依赖的注入。

然后,Bean A的创建过程继续,一旦完成,Bean A也可能会被AOP后处理器代理,生成代理Ap。

最终,在A类中注入的B类属性会是它的代理Bp。

需要注意的是,这个过程取决于Spring容器的具体配置,特别是AOP的相关配置,以及你指定的代理方式(例如是JDK动态代理还是CGLIB代理)。但最终,如果配置了AOP代理并且进行了正确的AOP设置,Spring会尽可能保证注入的是代理而不是原始的Bean实例。这保证了AOP提供的方面,如事务管理、安全性、日志记录等,都会像预期的那样正常工作。

如果我们继续延续之前讨论的情况

那么B类(现在是代理对象Bp)的属性应该是A类的代理对象Ap。

这是因为Spring AOP通常在Bean的后处理阶段应用,因此如果Bean A也需要被代理,那么在B类注入A类的依赖时,它最终将得到的是Bean A的代理对象Ap。

总的来说,利用Spring AOP和代理模式在存在循环依赖的情况下,最终两个相互依赖的Bean各自的属性,应该都是对方的代理版本:

A类中的属性是Bp(B类的代理)。

Bp(B类的代理)中的属性是Ap(A类的代理)。

这种行为确保了无论何时通过代理对象访问方法,AOP方面(如事务管理、安全性、日志记录等)都能按照配置被正确地应用。

再次强调,这是在你已经正确配置了Spring AOP,并且你的Bean被配置为需要代理(例如你有相关的aspect或者@Transactional注解等情况)的时候会发生的。如果你没有配置AOP相关功能,那么就不会存在代理对象,Bean会直接注入原始的对象。

相关推荐
Yz987617 分钟前
Hadoop里面MapReduce的序列化与Java序列化比较
java·大数据·jvm·hadoop·分布式·mapreduce·big data
凯哥Java19 分钟前
优化批处理流程:自定义BatchProcessorUtils的设计与应用
java·数据库·mysql
拉玛干23 分钟前
社团周报系统可行性研究-web后端框架对比-springboot,django,gin
数据库·python·spring·golang
mingzhi6124 分钟前
应届生必看 | 毕业第一份工作干销售好不好?
网络·web安全·面试
njnu@liyong28 分钟前
AOP-前置原理-怎么判断和拦截?
java·aop·拦截
白帽黑客cst28 分钟前
网络安全(黑客技术) 最新三个月学习计划
网络·数据结构·windows·学习·安全·web安全·网络安全
末央&33 分钟前
【C++】内存管理
java·开发语言·c++
八了个戒42 分钟前
【TypeScript入坑】TypeScript 的复杂类型「Interface 接口、class类、Enum枚举、Generics泛型、类型断言」
开发语言·前端·javascript·面试·typescript
心之语歌1 小时前
设计模式 享元模式(Flyweight Pattern)
java·设计模式·享元模式
MTingle1 小时前
【Java EE】文件IO
java·java-ee