【WebFlux】第二篇 —— Project Reactor 核心数据类型

认识 Project Reactor:响应式流的"基石"

在 Spring WebFlux 的底层,真正支撑起异步非阻塞数据流转的,是一个名为 Project Reactor 的核心库。它完全实现了 Reactive Streams 规范,为我们提供了一套声明式、函数式的 API。如果说 Reactive Streams 是响应式编程的"交通规则",那么 Project Reactor 就是按照这套规则制造出的"超级跑车"。

在 Reactor 中,万物皆流。为了应对不同的数据场景,Reactor 提供了两个最核心的数据类型(Publisher):MonoFlux。它们是整个响应式编程大厦的基石。

核心类型解析:Mono 与 Flux

要掌握 Reactor,首先要分清这两个核心概念的区别:

  • Mono(0 或 1 个元素的异步序列):Mono 代表一个最多只包含单个元素的异步计算结果。你可以把它理解为异步版的 Optional 或 CompletableFuture。
    典型场景:根据 ID 查询单个用户信息、保存一条记录、执行一次无返回值的异步操作(如 Mono)、HTTP 接口返回单个对象等。
  • Flux(0 到 N 个元素的异步序列):
    Flux 代表一个包含 0 到多个元素的有序异步序列,它甚至可以是一个无限流。你可以把它想象成一条物流传送带,或者数据库的游标。
    典型场景:查询用户列表、处理文件中的多行数据、WebSocket 消息流、实时传感器数据推送等。

声明式与惰性执行(Lazy Evaluation)

这是响应式编程中最反直觉、但也最核心的特性。在 Reactor 中,当你调用 map、filter 等操作符时,实际上并没有任何数据被处理,也没有任何业务逻辑被执行。这些操作仅仅是在构建一条"处理流水线(Pipeline)"。

只有当有 Subscriber(订阅者)调用 subscribe() 方法时,整条流水线才会被激活,数据才会像水流一样从源头开始向下流动。这种惰性执行机制使得我们可以像搭积木一样灵活地组装和复用数据流逻辑,同时也避免了不必要的资源消耗。

数据流的生命周期与"弹珠图(Marble Diagrams)"

为了直观地理解数据在 Mono 和 Flux 中的流转,响应式社区广泛使用"弹珠图"来进行可视化。在 Reactor 的数据流中,有四个关键的信号(生命周期事件):

  • onSubscribe:订阅建立。当消费者订阅了数据流,流会首先发出这个信号,表示连接已建立。
  • onNext:数据发射。流开始向下推送元素。对于 Mono,最多触发一次;对于 Flux,可以触发多次。
  • onComplete:正常完成。所有的数据都已成功发送,流正常关闭。
  • onError:异常终止。流在处理过程中遇到了错误,提前终止并向下传递异常信号。

代码示例:

java 复制代码
// 演示一个正常完成的 Flux 生命周期
Flux.just("A", "B", "C")
    .subscribe(
        data -> System.out.println("onNext: 收到数据 -> " + data), // 2. 触发 onNext
        error -> System.err.println("onError: 发生异常 -> " + error.getMessage()), // 4. 触发 onError (本例未触发)
        () -> System.out.println("onComplete: 数据流正常结束") // 3. 触发 onComplete
    ); 
// 注意:subscribe 内部会隐式触发 onSubscribe

// 演示一个发生异常的 Mono 生命周期
Mono.error(new RuntimeException("数据库连接失败"))
    .subscribe(
        data -> System.out.println("onNext: " + data),
        error -> System.err.println("onError: 捕获到异常 -> " + error.getMessage()), // 触发 onError
        () -> System.out.println("onComplete: 正常结束") // 异常时不会触发 onComplete
    );

图解示例:

  • 成功的 Flux:onSubscribe -> onNext: A -> onNext: B -> onNext: C -> onComplete
  • 出错的 Mono:onSubscribe -> onError: Exception

丰富的数据源创建方式

Reactor 提供了极其丰富的工厂方法来创建 Mono 和 Flux,以适配各种业务场景:

  • 静态值创建:使用 Mono.just("Hello") 或 Flux.just("A", "B", "C") 包装已知数据。

    java 复制代码
    // 创建包含单个元素的 Mono
    Mono.just("Hello WebFlux").subscribe(System.out::println);
    
    // 创建包含多个元素的 Flux
    Flux.just("Java", "Go", "Rust").subscribe(System.out::println);
  • 空流与错误流:使用 Mono.empty() 表示无数据返回;使用 Mono.error(new RuntimeException()) 直接抛出异常信号。

    java 复制代码
    // 创建空流(订阅后直接触发 onComplete)
    Mono.empty().subscribe(data -> {}, error -> {}, () -> System.out.println("空流已完成"));
    
    // 创建错误流(订阅后直接触发 onError)
    Flux.error(new IllegalStateException("非法状态")).subscribe();
  • 延迟/惰性初始化:使用 Mono.fromSupplier(() -> ...) 或 Mono.defer(() -> ...)。这种方式只有在真正被订阅时,才会执行 Supplier 内部的逻辑,非常适合封装数据库查询等耗时操作。

    java 复制代码
    // 每次订阅都会重新执行 Supplier 中的逻辑
    Mono.fromSupplier(() -> "当前时间: " + System.currentTimeMillis())
        .subscribe(System.out::println);
  • 异步数据源转换:如果系统中已有传统的异步代码,可以使用 Mono.fromFuture() 或 Mono.fromCallable() 将其无缝转换为响应式流。

    java 复制代码
    // 包装 CompletableFuture
    Mono.fromFuture(CompletableFuture.supplyAsync(() -> "异步结果"))
        .subscribe(System.out::println);
        
    // 包装同步但耗时的 Callable
    Mono.fromCallable(() -> {
        Thread.sleep(1000); // 模拟耗时操作
        return "计算完成";
    }).subscribe(System.out::println);
  • 时间驱动:使用 Flux.interval(Duration.ofSeconds(1)) 可以创建一个每秒发射一次递增数字的无限流,这在定时任务或心跳检测中非常有用。

    java 复制代码
    // 生成 1 到 5 的整数序列
    Flux.range(1, 5).subscribe(i -> System.out.print(i + " ")); // 输出: 1 2 3 4 5
    
    // 每秒发射一个递增数字的无限流(需配合 take 限制长度,避免无限打印)
    Flux.interval(Duration.ofSeconds(1))
        .take(3) 
        .subscribe(i -> System.out.println("Tick: " + i));

避坑提示:警惕副作用(Side Effects)

由于惰性执行的存在,初学者极易踩坑。例如,如果在 map 操作符中直接打印日志或修改外部变量,这些操作只有在被订阅时才会执行。如果不小心订阅了两次,这些副作用就会被执行两次。

java 复制代码
//  错误做法:在 map 中执行副作用(如打印日志)
// 问题:如果该流被订阅了两次,"处理数据:" 就会被打印两次,产生不可控的副作用。
Flux.just("Data-1", "Data-2")
    .map(data -> {
        System.out.println("处理数据: " + data); // 副作用混入了数据转换逻辑
        return data.toUpperCase();
    })
    .subscribe();

//  正确做法:使用 doOnNext 等生命周期钩子
// 优势:语义清晰,doOnNext 仅作为"观察者"记录日志,绝不改变流中的数据,且易于在调试期移除。
Flux.just("Data-1", "Data-2")
    .doOnNext(data -> System.out.println("准备处理数据: " + data)) // 安全的副作用钩子
    .map(String::toUpperCase) // 保持纯粹的同步转换逻辑
    .doOnComplete(() -> System.out.println("所有数据处理完毕")) // 统一处理完成事件
    .subscribe();

最佳实践:永远不要在 map 或 flatMap 中执行副作用操作。如果需要记录日志或进行调试,请使用 Reactor 专门提供的"生命周期钩子"操作符,如 doOnNext、doOnError、doOnComplete 等。这些钩子只会"观察"数据流,而不会改变数据流本身,是调试响应式代码的利器。

本篇小结:Mono 和 Flux 是响应式编程的容器,理解了它们的惰性执行机制和生命周期信号,我们就掌握了控制数据流的钥匙。

下一步预告:数据流建立起来了,我们该如何对它们进行加工?下一篇笔记我们将深入实战,详解 map 与 flatMap 的核心区别,并学习如何使用操作符对数据流进行转换、过滤与异常处理。

相关推荐
三84419 小时前
笔记:在同一个物理服务器上通过nginx的虚拟主机生成多个不同的web站点
java·前端·nginx
2501_9327502620 小时前
Android 数据持久化解析
android·java
NWU_LK21 小时前
【WebFlux】第三篇 —— 常用操作符与数据流转换
java
Mr__Miss1 天前
Java泛型完全指南:从入门到精通
java·开发语言·python
jinyishu_1 天前
模拟实现 C++ 栈和队列——从适配器模式看懂 STL 容器之美
java·c++·适配器模式
前端工作日常1 天前
我学习到的Java类和对象区别
java·后端
前端工作日常1 天前
我学习到的Java类完整结构
java·后端
什巳1 天前
JAVA练习309- 二叉树的层序遍历
java·数据结构·算法·leetcode
宠友信息1 天前
消息序号如何保证即时通讯源码聊天记录稳定加载
java·spring boot·redis·python·mysql·uni-app