当前位置: 首页 > news >正文

打破循环依赖的三大法宝: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. 1. Spring 尝试创建 ServiceA,但需要 ServiceB

  2. 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. 1. Spring 先创建 Bean
    Spring 先创建 UserService 和 NotificationService,此时不关心它们的依赖。

  2. 2. 通过 Setter 注入依赖
    创建完成后,Spring 调用 Setter 方法注入依赖,例如将 NotificationService 注入到 UserService 中。

  3. 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. 1. Spring 先初始化 ServiceA

  2. 2. 不是直接注入真实的 ServiceB,而是注入一个 ServiceB 的代理对象。

  3. 3. 该代理对象不会立即调用 ServiceB 的构造器。

  4. 4. Spring 随后单独初始化 ServiceB

  5. 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 监听到事件并发送邮件。

  • • 两者通过事件通信,无直接依赖。

优点
  • • 解耦:服务之间不再紧密耦合。

  • • 可扩展性:可为同一事件添加更多监听者。

  • • 灵活性:通过添加或移除监听者轻松调整行为。

  • • 避免循环依赖:无直接依赖,自然无循环问题。

http://www.dtcms.com/a/99277.html

相关文章:

  • 基于三维数字图像相关(DIC)全场应变测量技术的基础设施结构健康监测与安全评估方法研究
  • Msys2安装编译Redis
  • LearnOpenGL-笔记-其六
  • 数据分析概述
  • tauri配置全局变量
  • neo4j中导入csv格式的三元组数据
  • 访问者模式_行为型_GOF23
  • 人工智能入门(2)
  • 心理咨询法律咨询预约咨询微信小程序系统源码独立部署
  • Python函数中的*args与**kwargs详解:灵活处理可变参数
  • LabVIEW 燃气轮机气路故障诊断
  • 算法中常见的求和问题,数学公式
  • 通俗版解释:SecureLink 和 Xshell 的区别与联系
  • 管理系统-接口信息
  • 烽火MR622-KK融合机_海思MV300芯片_1+8G_强刷卡刷固件包
  • 计算机网络 用deepseek帮助整理的复习资料(一)
  • 工程项目管理软件赋能制造工程高效变革
  • VRRP虚拟路由器冗余协议
  • 网络探索之旅:网络原理(第二弹)
  • powershell7.5.0不支持conda的问题
  • 2.pycharm部署Ai - 编程好助手
  • Day17 -实例:利用不同语言不同框架的特征 进行识别
  • Anaconda安装-Ubuntu-Linux
  • 【数据结构】树与森林
  • Epoll 的本质与原理:高性能网络编程的基石
  • 【Java全栈】Java + Vue 项目框架与运行流程详解
  • 2024年零知识证明(ZK)研究进展
  • Baklib驱动企业知识管理数字化转型
  • jetson orin nano super AI模型部署之路(三)stable diffusion部署
  • 【深度学习入门_机器学习理论】极致梯度提升原理(XGBoost)