Spring 循环依赖
A
需要 B
,而 B
又需要 A
,形成一个循环依赖。
1. Spring 如何解决循环依赖?
Spring 通过三级缓存机制解决了循环依赖问题,具体步骤如下:
-
实例化阶段:
- 当容器创建一个 Bean 时,首先会调用其无参构造函数完成实例化(即分配内存,但未进行属性注入)。
- 实例化完成后,将该 Bean 的“早期引用”(尚未完全初始化的对象)放入三级缓存中。
-
属性注入阶段:
- 在为当前 Bean 注入属性时,如果发现依赖的 Bean 尚未完全初始化,则从三级缓存中获取其“早期引用”,完成属性注入。
-
初始化阶段:
- 属性注入完成后,执行 Bean 的初始化方法(如
@PostConstruct
或InitializingBean.afterPropertiesSet()
)。 - 初始化完成后,将该 Bean 从三级缓存移动到一级缓存,并清除二级和三级缓存。
- 属性注入完成后,执行 Bean 的初始化方法(如
三级缓存的具体作用
-
一级缓存(
singletonObjects
):- 存储完全初始化后的 Bean 实例。
- 当一个 Bean 完成所有属性注入和初始化后,它会被放入一级缓存中,供后续依赖使用。
-
二级缓存(
earlySingletonObjects
):- 存储“早期引用”(尚未完全初始化的 Bean 实例)。
- 如果某个 Bean 在初始化过程中被其他 Bean 依赖,则会从二级缓存中获取其“早期引用”,完成属性注入。
-
三级缓存(
singletonFactories
):- 存储 Bean 的工厂对象(
ObjectFactory
),用于动态生成代理对象或其他扩展功能。 - 三级缓存的主要作用是支持 AOP 动态代理等复杂场景。例如,当一个 Bean 需要被代理时,Spring 会在三级缓存中通过工厂动态生成代理对象。
- 存储 Bean 的工厂对象(
三级缓存的工作流程
以下是一个典型的循环依赖解决过程:
-
实例化阶段:
- 创建 Bean 的实例(调用无参构造函数),但不进行属性注入。
- 将该 Bean 的工厂对象(
ObjectFactory
)放入三级缓存中。
-
属性注入阶段:
- 如果当前 Bean 需要依赖另一个 Bean,而该 Bean 尚未完全初始化,则从三级缓存中获取其工厂对象,并生成“早期引用”。
- 将生成的“早期引用”放入二级缓存中,供当前 Bean 使用。
-
初始化阶段:
- 属性注入完成后,执行 Bean 的初始化方法(如
@PostConstruct
或InitializingBean.afterPropertiesSet()
)。 - 初始化完成后,将该 Bean 从二级缓存移动到一级缓存,并清除三级缓存和二级缓存中的相关记录。
- 属性注入完成后,执行 Bean 的初始化方法(如
2.为什么需要三级缓存?一级和二级不够吗?
一级缓存的问题
- 一级缓存只能存储完全初始化后的 Bean。如果在初始化过程中发生循环依赖,无法从一级缓存中获取尚未完全初始化的 Bean。
二级缓存的问题
- 二级缓存可以存储“早期引用”,但如果涉及动态代理(如 AOP),则需要额外的机制来生成代理对象。二级缓存无法动态生成代理对象,因此需要三级缓存。
三级缓存的优势
- 三级缓存通过工厂模式动态生成代理对象,确保在复杂的依赖关系中,Bean 的状态一致且正确。
- 三级缓存的设计使得 Spring 能够支持更多的扩展功能(如 AOP 动态代理)。
三级缓存的好处和坏处
好处
-
支持复杂的依赖关系:
三级缓存允许在 Bean 的生命周期中动态生成代理对象(如 AOP 动态代理),从而支持更复杂的场景。 -
保证线程安全:
三级缓存的设计使得在多线程环境下,Bean 的创建和依赖注入更加安全。 -
灵活性:
三级缓存提供了一个灵活的机制,可以在不同阶段对 Bean 的状态进行控制。
坏处
-
增加复杂性:
三级缓存的引入增加了代码的复杂性,理解和维护成本较高。 -
性能开销:
虽然三级缓存的时间复杂度仍然是 O(1)O(1),但多级缓存的管理会增加一定的性能开销。 -
潜在问题:
如果开发者滥用循环依赖,可能导致系统设计不合理,甚至出现难以调试的问题。
3.Spring Boot 高版本为什么要手动开启循环依赖?
背景
在 Spring Boot 2.6 及更高版本中,默认情况下禁用了循环依赖(spring.main.allow-circular-references=false
)。这是因为循环依赖虽然可以通过三级缓存解决,但它通常是设计上的问题,可能会导致以下问题:
-
代码耦合度过高:
循环依赖通常意味着类之间的职责划分不清晰,容易导致代码难以维护。例如,两个类互相依赖可能表明它们的职责没有很好地分离。 -
隐藏设计缺陷:
循环依赖可能掩盖了系统设计中的不合理之处。长期来看,这种设计会导致架构变得复杂且难以扩展。 -
测试困难:
循环依赖的 Bean 在单元测试中可能更难模拟和测试,因为它们的依赖关系过于紧密。 -
性能问题:
循环依赖的解决需要额外的缓存管理和状态检查,这会增加一定的性能开销。
如何手动开启循环依赖?
在 application.properties
或 application.yml
中设置以下配置:
spring.main.allow-circular-references=true
或者通过代码方式启用:
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApplication.class);
app.setAllowCircularReferences(true);
app.run(args);
}
}
4.循环依赖的实际案例分析
案例 1:简单的循环依赖
假设有两个类 A
和 B
:
@Component
public class A {
private final B b;
@Autowired
public A(B b) {
this.b = b;
}
}
@Component
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
}
在这种情况下,Spring 会通过三级缓存解决循环依赖问题。A
和 B
之间的依赖关系会在实例化阶段被正确处理。
案例 2:涉及动态代理的循环依赖
假设 A
是一个事务管理的 Bean,Spring 会为其生成动态代理对象:
@Component
public class A {
private final B b;
@Autowired
public A(B b) {
this.b = b;
}
@Transactional
public void doSomething() {
// 事务逻辑
}
}
@Component
public class B {
private final A a;
@Autowired
public B(A a) {
this.a = a;
}
}
在这种情况下,Spring 会通过三级缓存中的工厂对象动态生成 A
的代理对象,从而解决循环依赖问题。
5.循环依赖的替代方案
为了避免循环依赖,可以采用以下替代方案:
引入中间层:
- 将共同的逻辑抽取到一个新的服务类中,减少直接依赖。
-
@Service public class CommonService { public void commonLogic() { // 共同逻辑 } } @Component public class A { private final CommonService commonService; @Autowired public A(CommonService commonService) { this.commonService = commonService; } } @Component public class B { private final CommonService commonService; @Autowired public B(CommonService commonService) { this.commonService = commonService; } }
使用事件驱动机制:
- 使用 Spring 的事件发布机制(
ApplicationEvent
)来解耦组件。 -
@Component public class A { @Autowired private ApplicationEventPublisher eventPublisher; public void doSomething() { eventPublisher.publishEvent(new CustomEvent(this)); } } @Component public class B { @EventListener public void handleCustomEvent(CustomEvent event) { // 处理事件 } }
使用懒加载(@Lazy
注解):
- 通过
@Lazy
注解延迟 Bean 的初始化,从而避免循环依赖问题。 -
@Component public class A { private final B b; @Autowired public A(@Lazy B b) { this.b = b; } } @Component public class B { private final A a; @Autowired public B(@Lazy A a) { this.a = a; } }
重构代码逻辑:
- 如果两个类之间存在循环依赖,可能表明它们的职责划分不清晰。可以通过重构代码来消除直接依赖。
-
@Service public class A { private final CommonLogic commonLogic; @Autowired public A(CommonLogic commonLogic) { this.commonLogic = commonLogic; } public void doSomething() { commonLogic.performLogic(); } } @Service public class B { private final CommonLogic commonLogic; @Autowired public B(CommonLogic commonLogic) { this.commonLogic = commonLogic; } public void doSomethingElse() { commonLogic.performLogic(); } } @Component public class CommonLogic { public void performLogic() { // 共同逻辑 } }
6.最终总结
-
循环依赖的本质:
- 循环依赖是指两个或多个 Bean 之间相互依赖,形成一个闭环。
- Spring 通过三级缓存机制解决了循环依赖问题。
-
三级缓存的作用:
- 一级缓存:存储完全初始化后的 Bean。
- 二级缓存:存储“早期引用”(尚未完全初始化的 Bean)。
- 三级缓存:存储工厂对象,用于动态生成代理对象或其他扩展功能。
-
为什么需要三级缓存?
- 一级缓存无法解决循环依赖,因为它只存储完全初始化后的 Bean。
- 二级缓存无法支持动态代理等复杂场景,因此需要三级缓存。
-
Spring Boot 高版本默认禁用循环依赖的原因:
- 推动更好的设计,避免代码耦合度过高。
- 减少潜在问题,提高系统性能。
-
如何手动开启循环依赖?
- 通过配置文件或代码方式启用
spring.main.allow-circular-references=true
。
- 通过配置文件或代码方式启用
-
最佳实践:
- 尽量避免循环依赖,采用重构代码、引入中间层或事件驱动机制等方式解决问题。
- 如果确实无法避免循环依赖,确保它不会导致系统设计问题,并且只在必要的场景下使用。