打破循环依赖的三大法宝:Spring Boot实战解析
循环依赖是什么?
想象两个朋友,小明 和 小王:
• 小明 说:“在 小王 帮我之前,我无法开始工作。”
• 小王 说:“在 小明 帮我之前,我无法开始工作。”
这就形成了一个死锁,因为双方都无法在对方未行动的情况下开始。同理,在 Spring Boot 中,当两个或多个 Bean(由 Spring 管理的 Java 对象)直接或间接相互依赖时,就会发生循环依赖,导致 Spring 无法决定先创建哪一个 Bean。
Spring Boot 中的循环依赖简介
在 Spring Boot 中,当两个或多个 Bean 相互依赖(直接或间接),形成一个依赖循环时,就会出现循环依赖。这种情况会使 Spring 的 IoC(控制反转)容器无法解析,因为它无法确定应先实例化哪个 Bean。这可能会导致运行时问题。
循环依赖示例
假设有两个类,ClassA
和 ClassB
:
•
ClassA
依赖ClassB
。•
ClassB
依赖ClassA
。
@Component
public class ClassA {
private final ClassB classB;
@Autowired
public ClassA(ClassB classB) {
this.classB = classB;
}
}
@Component
public class ClassB {
private final ClassA classA;
@Autowired
public ClassB(ClassA classA) {
this.classA = classA;
}
}
在这种情况下,Spring 无法决定先创建 ClassA
还是 ClassB
,因为它们彼此需要对方先被实例化。
另一个示例
假设有两个服务类:ServiceA
和 ServiceB
:
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
在这里:
•
ServiceA
需要ServiceB
先被创建。•
ServiceB
需要ServiceA
先被创建。
这就形成了循环依赖,Spring 不知道应从哪个开始。
为什么这是个问题?
Spring Boot 使用依赖注入来管理 Bean。在启动时,它会尝试创建所有 Bean 并注入它们的依赖。如果存在循环依赖:
1. Spring 尝试创建
ServiceA
,但需要ServiceB
。2. Spring 接着尝试创建
ServiceB
,但需要ServiceA
。
这会陷入无限循环,最终 Spring 会抛出错误。
如何解决循环依赖?
以下是几种解决方法:
1. 使用 Setter 或字段注入
与其使用构造器注入,可以使用 Setter 或字段注入。这允许 Spring 先创建 Bean,然后再注入依赖。
@Service
public class ServiceA {
private ServiceB serviceB;
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Service
public class ServiceB {
private ServiceA serviceA;
@Autowired
public void setServiceA(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
什么是 Setter 注入?
在 Spring Boot 中,依赖注入是通过提供类所需的依赖(其他对象)来实现的。主要有三种方式:
• 构造器注入:通过构造器提供依赖。
• Setter 注入:通过 Setter 方法提供依赖。
• 字段注入:直接注入到字段(不推荐)。
使用构造器注入时,Spring 在启动时尝试创建所有 Bean 及其依赖。如果存在循环依赖,会导致问题。而 Setter 注入允许 Spring 先创建 Bean,再通过 Setter 方法注入依赖,从而打破循环依赖。
使用 Setter 注入解决循环依赖的示例
假设有 UserService
和 NotificationService
:
@Service
public class UserService {
private NotificationService notificationService;
@Autowired
public void setNotificationService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void createUser(String username) {
System.out.println("用户已创建:" + username);
notificationService.sendWelcomeNotification(username);
}
}
@Service
public class NotificationService {
private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
public void sendWelcomeNotification(String username) {
System.out.println("发送欢迎通知给:" + username);
}
}
Setter 注入如何打破循环依赖?
1. Spring 先创建 Bean:
Spring 先创建UserService
和NotificationService
,此时不关心它们的依赖。2. 通过 Setter 注入依赖:
创建完成后,Spring 调用 Setter 方法注入依赖,例如将NotificationService
注入到UserService
中。3. 循环依赖被解决:
因为两个 Bean 已存在,Spring 可以成功注入依赖,避免无限循环。
为什么有效?
构造器注入要求在创建 Bean 时解析所有依赖,而 Setter 注入允许先创建 Bean 再处理依赖,从而打破循环。
2. 使用 @Lazy
注解
通过 @Lazy
注解,Spring 不会在启动时立即实例化 Bean,而是创建一个代理对象(基于 CGLIB 的动态代理)作为占位符。只有在首次访问时才实例化实际 Bean。
@Lazy
如何内部解决循环依赖
假设在 ServiceA
中对 ServiceB
使用 @Lazy
:
@Component
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}
内部执行流程
1. Spring 先初始化
ServiceA
。2. 不是直接注入真实的
ServiceB
,而是注入一个ServiceB
的代理对象。3. 该代理对象不会立即调用
ServiceB
的构造器。4. Spring 随后单独初始化
ServiceB
。5. 当
ServiceA
调用ServiceB
的方法时,代理才会触发ServiceB
的实际创建。
示例
@Service
public class ServiceA {
private final ServiceB serviceB;
@Autowired
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
System.out.println("ServiceA 已初始化");
}
public void useServiceB() {
System.out.println("调用 ServiceB...");
serviceB.doSomething();
}
}
@Service
public class ServiceB {
public ServiceB() {
System.out.println("ServiceB 已初始化");
}
public void doSomething() {
System.out.println("ServiceB 正在执行操作!");
}
}
输出解释
ServiceA 已初始化
// ServiceB 尚未初始化!
调用 ServiceB...
ServiceB 已初始化
ServiceB 正在执行操作!
ServiceA
先创建,但 ServiceB
只有在调用时才被实例化。
3. 使用事件驱动方法
事件驱动方法通过解耦服务来打破循环依赖。
什么是事件驱动方法?
在一个事件驱动架构中:
• 一个组件(发布者)在某事件发生时发布事件。
• 另一个组件(监听者)监听该事件并执行操作。
发布者和监听者通过事件通信,无需直接依赖对方。
如何解决循环依赖?
以 UserService
和 NotificationService
为例:
•
UserService
在创建用户时发布事件。•
NotificationService
监听该事件并发送欢迎邮件。
这样,两个服务之间没有直接依赖,循环被打破。
示例代码
事件类
public class UserCreatedEvent {
private String username;
private String email;
public UserCreatedEvent(String username, String email) {
this.username = username;
this.email = email;
}
public String getUsername() { return username; }
public String getEmail() { return email; }
}
UserService
(发布者)
@Service
public class UserService {
private final ApplicationEventPublisher eventPublisher;
public UserService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void createUser(String username, String email) {
System.out.println("用户已创建:" + username);
eventPublisher.publishEvent(new UserCreatedEvent(username, email));
}
}
NotificationService
(监听者)
@Service
public class NotificationService {
@EventListener
public void handleUserCreatedEvent(UserCreatedEvent event) {
System.out.println("发送欢迎通知给:" + event.getEmail());
}
}
工作原理
•
UserService
创建用户时发布UserCreatedEvent
。•
NotificationService
监听到事件并发送邮件。• 两者通过事件通信,无直接依赖。
优点
• 解耦:服务之间不再紧密耦合。
• 可扩展性:可为同一事件添加更多监听者。
• 灵活性:通过添加或移除监听者轻松调整行为。
• 避免循环依赖:无直接依赖,自然无循环问题。