Spring循环依赖问题一篇文章给你说明白
三级缓存与 Spring 循环依赖
三级缓存是 Spring Framework 中用来解决 循环依赖问题 的一种机制,特别是在 Spring 创建 Bean 时,防止 Bean 的递归依赖导致的无限循环。Spring 的三级缓存机制可以帮助在 Bean 创建过程中部分暴露 Bean 的早期引用,打破循环依赖。
三级缓存的基本概念
在 Spring 的 BeanFactory 中,Bean 的创建是通过 三级缓存 来管理的。三级缓存对应的是 Spring 管理 Bean 实例的不同阶段:
- 一级缓存(
singletonObjects
):用于保存完全初始化后的 Bean 实例,Bean 完全可用了。 - 二级缓存(
earlySingletonObjects
):保存已经实例化但还没有完全初始化的 Bean 实例,也就是 早期引用。 - 三级缓存(
singletonFactories
):保存能够生成早期 Bean 的工厂(ObjectFactory
),通常用来存储通过 AOP 代理生成的 Bean。该工厂在需要时可以生成早期 Bean 实例。
通过三级缓存机制,Spring 在遇到循环依赖时,可以使用早期引用解决循环依赖问题。
循环依赖及其问题
循环依赖的经典场景是 两个 Bean 互相依赖,例如 A
依赖 B
,而 B
又依赖 A
。当 Spring 创建 A
时,发现它依赖 B
,于是去创建 B
,但 B
又依赖于 A
,此时如果没有特殊的机制处理,Spring 将陷入无限递归,无法完成 Bean 的初始化。
三级缓存的工作流程
当 Spring 通过 getBean
创建 Bean 时,Spring 会按照以下步骤使用三级缓存:
-
查找一级缓存:先检查一级缓存(
singletonObjects
),看是否已经存在完全初始化的 Bean。 -
查找二级缓存:如果一级缓存中不存在,Spring 会检查二级缓存(
earlySingletonObjects
),看是否有正在初始化中的早期 Bean。 -
查找三级缓存:如果二级缓存中也没有,Spring 会检查三级缓存(
singletonFactories
),并通过ObjectFactory
提供一个早期引用。这时候,Bean 并没有完全初始化,可能是一个代理对象(如 AOP 代理对象)。Spring 会将生成的早期引用放入二级缓存中,同时从三级缓存中移除。 -
放入一级缓存:当 Bean 完全初始化完成后,Spring 会将该 Bean 从二级缓存移到一级缓存。
通过这种机制,Spring 可以部分暴露 Bean 的早期引用,打破循环依赖,从而成功完成 Bean 的创建。
三级缓存解决循环依赖的具体过程
假设 A
和 B
互相依赖,以下是三级缓存解决循环依赖的过程:
-
开始创建 Bean A:
- Spring 开始实例化
A
,但发现A
依赖于B
。
- Spring 开始实例化
-
开始创建 Bean B:
- Spring 开始创建
B
,但发现B
依赖于A
。 - Spring 将
A
的早期引用放入三级缓存,并返回A
的早期引用给B
,这使得B
可以继续初始化。
- Spring 开始创建
-
完成 Bean B 的初始化:
B
的依赖(A
的早期引用)已经解决,B
完全初始化,将B
放入一级缓存。
-
完成 Bean A 的初始化:
- Spring 回到
A
的创建过程,B
已经完全初始化,所以A
的依赖可以被注入。 A
也完成初始化,将A
从三级缓存移到一级缓存。
- Spring 回到
通过这样的流程,Spring 成功解决了 A
和 B
之间的循环依赖。
Spring Boot 3.x 后循环依赖的变化
从 Spring Framework 5.0 开始,Spring Boot 3.x 也随着这个变化逐步禁止了默认的循环依赖处理,特别是对 构造函数注入的循环依赖。在 Spring Boot 3.x 版本中,Spring 已不再允许处理构造函数的循环依赖。这种变更的原因和解决方法如下:
为什么禁止构造函数的循环依赖?
-
构造函数循环依赖更难处理:
- 构造函数的循环依赖本质上更难解决,因为在构造函数执行时,Spring 还没有机会将 Bean 的部分引用暴露出来(早期引用),因此无法通过三级缓存的机制解决。
- 构造函数注入要求依赖在构造函数被调用之前就必须准备好,导致无法使用原来的三级缓存机制处理。
-
确保代码健壮性:
- 允许构造函数循环依赖会让代码变得复杂且难以调试,同时可能会引发不可预测的行为。
- 禁止这种依赖可以促使开发者通过更清晰的设计和更好的依赖管理来解决问题,比如通过重构代码避免循环依赖。
如何解决循环依赖问题?
-
避免循环依赖:
- 最佳的解决方法是从设计上避免循环依赖。你可以通过重构代码,解除 Bean 之间的紧密耦合。
- 将依赖注入到
@PostConstruct
或者通过 setter 方法注入,以减少构造函数注入的使用。
-
使用 setter 注入或
@Autowired
注解:- 如果遇到循环依赖问题,尽量避免使用构造函数注入,改用 setter 注入或者字段注入,Spring 的三级缓存可以处理这些类型的循环依赖。
-
显式配置依赖注入顺序:
- 使用
@DependsOn
注解,显式地告诉 Spring 哪个 Bean 应该在另一个 Bean 之前初始化。
- 使用
-
Spring Boot 3.x 中允许字段或 setter 循环依赖:
- 虽然 Spring Boot 3.x 禁止了构造函数的循环依赖,但它仍然允许通过字段或 setter 方法的依赖注入来处理循环依赖。
- 当使用字段或 setter 注入时,Spring 依然使用三级缓存机制解决循环依赖。
Spring Boot 3.x 与 之前版本的区别总结
-
Spring Boot 2.x 及之前版本:
- 默认允许循环依赖,无论是构造函数注入还是 setter 注入,Spring 会尝试使用三级缓存解决。
-
Spring Boot 3.x:
- 构造函数循环依赖被禁用,Spring 不再允许自动解决这种循环依赖,会抛出异常。
- setter 注入和字段注入的循环依赖依然支持,通过三级缓存机制解决。
为什么二级缓存不可以,具体原因如下:
1. AOP(面向切面编程)依赖代理对象
Spring 的 AOP 实现是基于代理模式的,当使用诸如 @Transactional
、@Aspect
之类的注解时,Spring 需要在目标对象的方法执行之前或之后执行额外的逻辑。这些额外的逻辑是通过代理对象完成的。
-
代理对象:是目标对象的一个代理,它拦截目标对象的方法调用,并在调用方法之前或之后执行一些附加的操作。例如,
@Transactional
注解就要求在方法执行之前开启事务,执行完之后提交或回滚事务。如果 Spring 返回的不是代理对象,而是原始对象,那么 Spring 无法拦截方法调用,自然也无法在调用时进行事务管理或执行其他横切逻辑。
例子:@Transactional
注解
1
2
3
4
5
6
7
8
9
@Service
public class MyService {
@Transactional
public void performTask() {
// 事务处理的业务逻辑
System.out.println("Performing task in transaction.");
}
}
- 当调用
performTask()
方法时,如果返回的是原始对象,那么该方法将直接执行,而不会触发任何事务逻辑。 - 但是如果返回的是代理对象,Spring 会拦截方法调用,在方法执行前开启事务,方法执行完毕后提交或回滚事务。这一切都通过代理对象的拦截来完成。
因此,如果使用的是原始对象,Spring 无法在方法执行前后织入事务管理等逻辑。
2. 动态代理使得功能可扩展
Spring 的代理机制不仅仅用于事务管理,也广泛用于其他 AOP 功能,比如日志记录、权限检查、监控等。
例如:
- 日志记录:可以通过代理对象在方法执行前后自动记录日志。
- 权限检查:可以在方法调用前通过代理对象检查当前用户是否有权限执行该操作。
这些横切逻辑都依赖于代理对象拦截方法调用,如果 Spring 返回的是原始对象而不是代理对象,拦截功能将无法生效。
3. 为什么二级缓存无法处理代理?
在 Spring 的三级缓存机制中:
- 一级缓存 保存完全初始化的 Bean。
- 二级缓存 保存的是原始对象的早期引用。
- 三级缓存 保存的是一个
ObjectFactory
,它可以用来动态生成代理对象。
如果没有三级缓存,只有二级缓存,那么在循环依赖的场景中,Spring 只能提前暴露原始对象,而不是代理对象。如果 Bean 需要 AOP 增强(比如 @Transactional
),原始对象就无法满足需求。
例如,假设有一个循环依赖的场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
@Transactional
public void methodA() {
System.out.println("Executing methodA");
}
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
public void methodB() {
System.out.println("Executing methodB");
}
}
在创建 ServiceA
时,如果直接返回二级缓存中的原始对象(没有被代理),当调用 methodA()
时,@Transactional
的增强逻辑不会生效。因为只有代理对象能够执行事务拦截,而原始对象没有事务功能。
因此:
- 二级缓存只能存放原始的早期对象,无法处理需要代理增强的 Bean。
- 三级缓存保存
ObjectFactory
,可以根据需要动态生成代理对象,确保返回的是经过 AOP 增强后的对象。