Spring概念:三级缓存
Spring 的三级缓存介绍
在 Spring 中,为了解决单例 Bean 的循环依赖问题,使用了三级缓存机制。这三级缓存本质上是三个 Map,分别如下:
1. 一级缓存(singletonObjects)
- 含义:也被称作单例池,它存储的是已经完全初始化好的单例 Bean 实例。当需要获取一个单例 Bean 时,Spring 会优先从这个缓存中查找。
- 数据结构:
Map<String, Object>,其中键是 Bean 的名称,值是对应的 Bean 实例。
2. 二级缓存(singletonFactories)
- 含义:存放的是单例 Bean 的工厂对象。当 Bean 实例化完成,但还未完成属性注入和初始化时,会将一个创建该 Bean 代理对象的工厂存入此缓存。
- 数据结构:
Map<String, ObjectFactory<?>>,键为 Bean 的名称,值是用于创建 Bean 的工厂对象。
3. 三级缓存(earlySingletonObjects)
- 含义:存储的是提前暴露的单例 Bean 实例,这些 Bean 虽然还未完成全部的初始化流程,但已经可以被引用。通过这个缓存,其他 Bean 在依赖该 Bean 时可以获取到一个早期的实例。
- 数据结构:
Map<String, Object>,键是 Bean 的名称,值是早期暴露的 Bean 实例。
应用场景
主要应用场景就是解决单例 Bean 的循环依赖问题。在 Spring 应用中,当多个单例 Bean 之间存在相互依赖形成循环时,利用三级缓存机制可以保证这些 Bean 能够正常创建和初始化。例如,在一个 Web 应用中,ServiceA 依赖 ServiceB,而 ServiceB 又依赖 ServiceA,这种情况下就会出现循环依赖,三级缓存就可以发挥作用来解决该问题。
缓存解决循环依赖问题的原理
下面通过一个具体的循环依赖场景(BeanA 依赖 BeanB,BeanB 依赖 BeanA)来详细解释三级缓存是如何解决循环依赖问题的:
步骤 1:创建 BeanA
- Spring 容器开始创建
BeanA,首先将BeanA的创建标记为正在创建状态。 - 实例化
BeanA,此时BeanA只是一个空的实例,还未进行属性注入和初始化。 - 将创建
BeanA的工厂对象存入二级缓存singletonFactories中,这个工厂对象可以在需要时返回BeanA的早期实例。
步骤 2:BeanA 注入 BeanB
- 在对
BeanA进行属性注入时,发现BeanA依赖BeanB,于是 Spring 容器开始创建BeanB。
步骤 3:创建 BeanB
- 同样,将
BeanB的创建标记为正在创建状态。 - 实例化
BeanB,然后将创建BeanB的工厂对象存入二级缓存singletonFactories中。
步骤 4:BeanB 注入 BeanA
- 在对
BeanB进行属性注入时,发现BeanB依赖BeanA。此时,Spring 会先从一级缓存singletonObjects中查找BeanA,发现没有找到。 - 接着从二级缓存
singletonFactories中查找BeanA的工厂对象,找到后通过该工厂对象获取BeanA的早期实例,并将这个早期实例存入三级缓存earlySingletonObjects中,同时从二级缓存中移除该工厂对象。 - 将这个早期的
BeanA实例注入到BeanB中。
步骤 5:完成 BeanB 的创建
BeanB完成属性注入和初始化后,将BeanB存入一级缓存singletonObjects中,并从正在创建状态标记中移除。
步骤 6:完成 BeanA 的创建
- 此时
BeanA可以从一级缓存中获取到已经完全初始化好的BeanB实例,完成属性注入和初始化。 - 最后将
BeanA存入一级缓存singletonObjects中,并从三级缓存earlySingletonObjects中移除。
通过以上步骤,利用三级缓存机制,Spring 容器可以在存在循环依赖的情况下,成功创建和初始化所有的单例 Bean。
下面是一个简单的示例代码,展示了 BeanA 和 BeanB 的循环依赖:
// BeanA 类
class BeanA {
private BeanB beanB;
public void setBeanB(BeanB beanB) {
this.beanB = beanB;
}
}
// BeanB 类
class BeanB {
private BeanA beanA;
public void setBeanA(BeanA beanA) {
this.beanA = beanA;
}
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 配置类
@Configuration
class AppConfig {
@Bean
public BeanA beanA() {
BeanA beanA = new BeanA();
beanA.setBeanB(beanB());
return beanA;
}
@Bean
public BeanB beanB() {
BeanB beanB = new BeanB();
beanB.setBeanA(beanA());
return beanB;
}
}
// 主类
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
BeanA beanA = context.getBean(BeanA.class);
BeanB beanB = context.getBean(BeanB.class);
System.out.println("BeanA 和 BeanB 成功创建");
}
}
在这个示例中,BeanA 和 BeanB 相互依赖,但由于 Spring 的三级缓存机制,它们能够正常创建和初始化。
为什么还需要第三级缓存?
在 Spring 的三级缓存设计中,第三级缓存(earlySingletonObjects)的存在是为了优化早期实例的获取效率,并支持代理对象的提前暴露。以下是其必要性的详细解释:
1. 为什么需要第三级缓存?
场景背景
在解决循环依赖时,Spring 需要提前暴露未完全初始化的 Bean 实例。此时,二级缓存(singletonFactories)存储的是工厂对象,而三级缓存存储的是工厂对象生成的早期实例。两者的协作可以避免重复生成实例,同时支持 AOP 代理的创建。
具体原因
-
避免重复调用工厂方法
二级缓存中的工厂对象(ObjectFactory)每次调用都会生成一个新的早期实例。如果多个 Bean 依赖同一个未初始化的 Bean,直接通过工厂生成实例会导致多次创建,造成性能浪费。
三级缓存的作用:将工厂生成的早期实例缓存起来,后续依赖方可以直接从缓存中获取,避免重复生成。 -
支持 AOP 代理的提前暴露
在 Spring 中,若 Bean 需要被代理(如通过@Transactional或@Async),代理对象的创建必须在属性注入前完成。三级缓存存储的是工厂生成的代理对象,这样其他 Bean 注入的是代理而非原始实例,确保代理逻辑的正确性。
2. 三级缓存与二级缓存的协作流程
以 BeanA 和 BeanB 的循环依赖为例:
- 创建
BeanA- 实例化
BeanA,将其工厂对象存入二级缓存(singletonFactories)。
- 实例化
BeanA注入BeanB- 创建
BeanB,同样将其工厂存入二级缓存。
- 创建
BeanB注入BeanA- 从二级缓存中取出
BeanA的工厂,调用工厂生成早期实例(可能是代理对象)。 - 将该实例存入三级缓存(
earlySingletonObjects),并移除二级缓存中的工厂。 - 将该实例注入到
BeanB。
- 从二级缓存中取出
- 完成初始化后
BeanB初始化完成,存入一级缓存,BeanA同样完成初始化并存入一级缓存。- 三级缓存中的早期实例被移除。
3. 为什么不能只用二级缓存?
若仅有二级缓存(工厂对象):
- 每次获取早期实例都需要调用工厂,导致重复创建。
- 无法处理代理对象的提前暴露。例如,若
BeanA是代理对象,工厂需在 Bean 实例化后立即生成代理,而三级缓存能确保后续依赖方直接获取代理对象。
4. 总结
| 缓存层级 | 作用描述 | 关键场景 |
|---|---|---|
| 一级缓存 | 存储完全初始化的单例 Bean | 正常获取 Bean 时使用 |
| 二级缓存 | 存储工厂对象,用于生成早期实例 | 处理循环依赖时生成早期实例的工厂 |
| 三级缓存 | 缓存工厂生成的早期实例(可能是代理) | 优化性能,避免重复生成实例;支持代理对象的提前暴露 |
第三级缓存通过缓存已生成的早期实例,确保循环依赖中的 Bean 能高效获取实例,并正确处理代理逻辑,是 Spring 解决循环依赖的关键设计。
Spring 是如何通过第三级缓存来避免 AOP 问题的?
在 Spring 中,当存在 AOP(面向切面编程)和循环依赖的情况时,第三级缓存(earlySingletonObjects)发挥着至关重要的作用,下面详细解释 Spring 如何利用第三级缓存来避免 AOP 带来的问题。
1. AOP 和循环依赖可能产生的问题
在 AOP 场景下,Spring 会为目标 Bean 创建代理对象,这个代理对象会包含额外的增强逻辑,如事务管理、日志记录等。当存在循环依赖时,如果处理不当,可能会导致以下问题:
- 代理对象创建时机不当:如果在 Bean 初始化过程中,没有正确处理代理对象的创建和注入,可能会导致一个 Bean 注入的是原始对象而非代理对象,从而使得 AOP 增强逻辑无法生效。
- 重复创建代理对象:若没有合理的缓存机制,在解决循环依赖的过程中,可能会多次创建代理对象,造成资源浪费。
2. 第三级缓存解决 AOP 问题的原理
2.1 提前暴露代理对象的工厂
在 Spring 容器创建 Bean 时,当 Bean 实例化完成但还未进行属性注入和初始化时,会将一个创建该 Bean 代理对象的工厂存入二级缓存(singletonFactories)中。这个工厂对象会根据 Bean 是否需要进行 AOP 代理来决定返回原始对象还是代理对象。
2.2 利用第三级缓存存储早期代理对象
当另一个 Bean 在属性注入过程中需要依赖这个还未完全初始化的 Bean 时,Spring 会从二级缓存中获取该 Bean 的工厂对象,并调用工厂对象的方法来获取早期实例。如果该 Bean 需要进行 AOP 代理,工厂对象会创建代理对象并返回。这个早期的代理对象会被存入第三级缓存(earlySingletonObjects)中,同时从二级缓存中移除对应的工厂对象。
2.3 保证注入的是代理对象
通过将早期的代理对象存入第三级缓存,其他 Bean 在依赖该 Bean 时,会从第三级缓存中获取到这个代理对象并进行注入。这样就保证了在循环依赖的情况下,每个 Bean 注入的都是正确的代理对象,从而避免了 AOP 增强逻辑失效的问题。
2.4 避免重复创建代理对象
由于第三级缓存存储了早期的代理对象,后续再需要获取该 Bean 的实例时,会直接从第三级缓存中获取,而不会再次调用二级缓存中的工厂对象来创建代理对象,从而避免了重复创建代理对象的问题。
3. 示例代码及流程分析
3.1 示例代码
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
// 切面类
@Aspect
class MyAspect {
@Before("execution(* com.example.demo.ServiceA.*(..))")
public void beforeAdvice() {
System.out.println("Before method execution");
}
}
// ServiceA 类
class ServiceA {
private ServiceB serviceB;
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
public void doSomething() {
System.out.println("ServiceA is doing something");
}
}
// ServiceB 类
class ServiceB {
private ServiceA serviceA;
public void setServiceA(ServiceA serviceA) {
this.serviceA = serviceA;
}
public void doSomething() {
System.out.println("ServiceB is doing something");
}
}
// 配置类
@Configuration
@EnableAspectJAutoProxy
class AppConfig {
@Bean
public ServiceA serviceA() {
ServiceA serviceA = new ServiceA();
serviceA.setServiceB(serviceB());
return serviceA;
}
@Bean
public ServiceB serviceB() {
ServiceB serviceB = new ServiceB();
serviceB.setServiceA(serviceA());
return serviceB;
}
@Bean
public MyAspect myAspect() {
return new MyAspect();
}
}
// 主类
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
ServiceA serviceA = context.getBean(ServiceA.class);
serviceA.doSomething();
}
}
3.2 流程分析
- 创建
ServiceA:Spring 容器开始创建ServiceA,实例化ServiceA后,将创建ServiceA代理对象的工厂存入二级缓存。 ServiceA注入ServiceB:在对ServiceA进行属性注入时,发现依赖ServiceB,于是开始创建ServiceB。- 创建
ServiceB:实例化ServiceB后,将创建ServiceB代理对象的工厂存入二级缓存。 ServiceB注入ServiceA:在对ServiceB进行属性注入时,发现依赖ServiceA。从二级缓存中获取ServiceA的工厂对象,调用工厂方法创建ServiceA的代理对象,并将该代理对象存入第三级缓存,同时从二级缓存中移除ServiceA的工厂对象。将ServiceA的代理对象注入到ServiceB中。- 完成
ServiceB的创建:ServiceB完成属性注入和初始化后,存入一级缓存。 - 完成
ServiceA的创建:ServiceA从一级缓存中获取到已经完全初始化好的ServiceB实例,完成属性注入和初始化。最后将ServiceA存入一级缓存,同时从第三级缓存中移除ServiceA的早期代理对象。
通过以上流程,Spring 利用第三级缓存确保了在循环依赖的情况下,ServiceA 和 ServiceB 注入的都是正确的代理对象,避免了 AOP 问题的出现。

浙公网安备 33010602011771号