Flux、Mono 响应式编程

5 阅读20分钟

Reactive Programming

响应式编程是一种声明式编程范式,专注于数据流和变化的传播。随着软件系统对高并发、实时性需求的增加,响应式编程已经成为主流。从 Spring Boot 3 开始,Spring 框架更加重视响应式编程,并推出了 Spring WebFlux 和 Spring Data Reactive 等模块,为 Java 开发者构建响应式应用提供了强大的支持。

reactive-streams 规范

官网:www.reactive-streams.org/

  • Reactive Streams 作为一个更广泛的跨平台规范,可以被多个框架使用。
  • JDK 9 在设计时参照了 Reactive Streams 的规范,但并没有直接等同于 Reactive Streams。JDK 9 的 Flow API 可以看作是 Java 标准库中对异步流处理和背压机制的一个实现,它是为了填补 JDK 本身缺乏统一的异步流处理机制的空白。

核心接口

  • Publisher:发布者
  • Subscriber:订阅者
  • Subscription:订阅关系
  • Processor:处理器(接受数据 => 处理数据 => 发送数据)

核心概念

  • Backpressure:允许消费者根据自己的处理能力来调节生产者发送数据的速度。避免因处理速度不匹配导致的资源耗尽或数据丢失‌。
  • publishOn/subscribeOn 指定执行线程,分离 I/O 密集型与计算密集型任务。

Reactive Programming 概念提出

微软在2009年提出了响应式编程(Reactive Programming)这一概念,旨在通过数据流和变化的传播,提供一种更加声明式和灵活的方式来处理异步事件流。响应式编程的核心思想是通过“观察者模式”来处理数据流,使得代码更加简洁,易于理解和维护。

Rx(Reactive Extensions) 的推出

微软在响应式编程的基础上推出了 Reactive Extensions (Rx),一个用于 .NET 平台的库。Rx 提供了一个可组合的、声明式的方式来处理异步事件流,开发者可以像操作集合一样操作异步数据流,从而简化了异步编程中的回调地狱和复杂的错误处理。

跨平台支持和框架发展

随着响应式编程的成功,Rx 的理念和库被移植到多个平台和语言中,如:

  • RxJava:Netflix 为 Java 平台推出的响应式编程库,成为 Android 开发和 Java 领域的重要工具。
  • RxJS:用于 JavaScript 的响应式编程库,广泛应用于前端开发,尤其是在 Angular 和 React 中。
  • Akka Streams:由 LightBend 提供的用于 Scala 和 Java 的响应式编程框架,强调处理大规模分布式数据流。

Spring 5 和响应式编程的普及

2017年,Spring 5 正式发布,标志着响应式编程在 Java 生态系统中的广泛应用。Spring 5 引入了响应式编程的支持,特别是通过 Spring WebFlux 模块,使得开发者能够利用响应式编程模型构建高效、非阻塞的 Web 应用。同时,支持响应式编程的核心框架 Spring Reactor 也推出了其 3.1.0 版本,推动了响应式编程在企业级应用中的应用。

响应式框架

Reactive Streams本质上是一套标准化接口规范,其核心价值在于为异步流处理建立了背压机制的统一契约,该规范本身并不提供具体实现,而是通过定义Publisher/Subscriber等核心组件及其交互规则,为响应式编程奠定了可互操作的底层基础。

基于Reactive Streams规范的实现框架包括Project Reactor、RxJava、Akka Streams等,这些框架通过扩展核心接口形成了各具特色的技术生态。

对于开发者而言,需根据业务场景中的吞吐量需求、背压处理策略、线程调度模型等关键维度,结合框架特性和社区生态进行多维评估,最终实现精准的技术选型。这些实现框架不仅完整支持响应式宣言(Reactive Manifesto)的核心原则,更通过丰富的操作符和配置策略,为构建弹性化、响应式的分布式系统提供了标准化工具链。

Java Flow API(原生轻量级方案)

  • 核心类‌:Flow.PublisherFlow.SubscriberFlow.Subscription
  • ‌关键特性:
    • Java 9+ 原生支持,无需第三方依赖。
    • 提供基础背压控制(request(n))。
    • 兼容其他 Reactive Streams 实现。
  • 适用场景:
    • 轻量级响应式工具开发。
    • 与其他框架的兼容性适配。
  • 文档‌:Java 9 Flow API

Reactor(Spring 生态首选)

  • 核心类型‌:MonoFlux(0-N元素流)。
  • ‌关键特性:
    • 深度集成 Spring 生态(如 WebFlux、Spring Data Reactive)。
    • 支持丰富的背压策略(BufferDropLatest)。
    • 提供 100+ 操作符(mapflatMapzip)。
  • ‌适用场景:
    • 高并发 Web 服务(替代 Spring MVC)。
    • 微服务间响应式通信(如 RSocket)。
  • 官网‌:Project Reactor

RxJava(复杂事件流处理)

  • 核心类型‌:Observable(非背压流)、Flowable(背压流)。
  • 关键特性:
    • 支持 300+ 操作符,功能最全的响应式库。
    • 兼容 Java 6+ 和 Android 平台。
    • 提供线程调度(observeOnsubscribeOn)。
  • ‌适用场景:
    • Android 应用异步任务。
    • 复杂事件流合并/转换(如多数据源聚合)。
  • 官网‌:ReactiveX/RxJava

RSocket(响应式通信协议)

  • 核心特性:
    • 基于 Reactive Streams 的二进制协议,支持 TCP/WebSocket。
    • 提供四种交互模式:Request-ResponseFire-and-ForgetStreamChannel
  • ‌适用场景:
    • 跨语言微服务通信(Java、Go、Node.js)。
    • 实时双向数据流(如 IoT 设备控制)。
  • ‌集成框架‌:
    • Reactor(Spring RSocket)、RxJava、Kotlin Coroutines。
  • 官网‌:RSocket

Akka Streams(分布式流处理)

  • 核心概念‌:Source(发布者)、Flow(处理器)、Sink(订阅者)。
  • 关键特性:基于 Actor 模型,支持分布式容错。
    • 内置背压传播,无需手动配置。
    • 提供流式 DSL(领域特定语言)。
  • 适用场景:
    • 分布式数据管道(如 Kafka 流处理)。
    • 高容错性实时计算(如金融风控)。
  • 官网‌:Akka Streams

框架对比

根据项目需求选择框架:‌Spring 生态优先 Reactor‌,‌Android 选 RxJava‌,‌分布式系统用 Akka Streams‌,‌轻量级场景用 Java Flow‌,‌跨语言通信用 RSocket‌。

框架技术生态背压支持适用场景学习成本
ReactorSpring/WebFluxWeb服务、微服务通信
RxJavaAndroid/Java移动端、复杂事件流
Akka StreamsAkka/Scala自动分布式系统、大数据管道
Java FlowJava原生基础轻量级工具、兼容性适配
RSocket多语言(跨平台)实时通信、IoT

projectreactor

Publisher

  • reactor 的核心发布者

    • Flux
    • Mono
  • 数据源的创建方式

    • 空数据源: 用于表示无数据的完成信号(如删除操作的结果)。
    • 动态生成:Mono.create 和 Flux.generate/Flux.create 允许手动控制元素发射(同步或异步)。
    • 异步数据源: 从 FutureCallable 或 Supplier 中获取数据,支持非阻塞操作。
    • 时间驱动: Mono.delay 延迟发射,Flux.interval 周期性发射递增数值。
    • 合并/组合: zip 严格对齐元素,merge 无序合并,concat 顺序连接。
    • 背压适配: 通过 FluxSink 或 MonoSink 手动控制背压和元素发射。
  • Mono 和 Flux 数据源创建方式分类总结

类别描述Mono 方法示例Flux 方法示例
空数据源创建不发射任何元素的数据流。Mono.empty()Flux.empty()
单个元素发射单个静态值或对象。Mono.just(T)Flux.just(T...)
多个元素发射多个静态值或对象(仅 Flux 支持)。N/AFlux.just(T1, T2...)
集合/数组从集合或数组生成元素。N/AFlux.fromIterable(List<T>) Flux.fromArray(T[])
流(Stream)从 Java Stream 生成元素。N/AFlux.fromStream(Stream<T>)
动态生成通过生成器函数动态生成元素。Mono.create(sink -> {...})Flux.generate(sink -> {...}) Flux.create(sink -> {...})
异步数据源从异步操作(如 FutureCallable)获取数据。Mono.fromFuture(Future) Mono.fromCallable(Callable)Flux.from(Publisher) Flux.fromStream(Supplier<Stream>)
错误信号直接发射错误信号。Mono.error(Throwable)Flux.error(Throwable)
延迟初始化惰性生成数据(订阅时才执行逻辑)。Mono.defer(() -> ...) Mono.fromSupplier(Supplier)Flux.defer(() -> ...) Flux.fromStream(Supplier<Stream>)
时间驱动基于时间生成数据(如定时、延迟)。Mono.delay(Duration)Flux.interval(Duration)
合并/组合合并多个数据源。Mono.zip(Mono1, Mono2...)Flux.merge(Flux1, Flux2...) Flux.concat(Flux1, Flux2...) Flux.zip(Flux1, Flux2...)
背压适配适配外部背压机制(如 Sink 手动控制)。Mono.create(MonoSink)Flux.create(FluxSink)
条件触发根据条件生成数据(如 firsttakeUntil)。Mono.firstWithValue(Mono1, Mono2)Flux.firstWithValue(Publisher...) Flux.takeUntil(Predicate)

数据源发布类型

Project Reactor 的发布模型是其响应式编程的核心机制,主要分为 ‌冷发布者(Cold Publisher) ‌ 和 ‌热发布者(Hot Publisher) ‌。它们的区别在于数据流的生成、共享方式以及订阅者的消费行为。

冷发布者(Cold Publisher)

定义‌:冷发布者为每个订阅者生成‌独立的数据流‌。每个订阅者都会触发数据源的完整生成过程,即使其他订阅者已订阅过。

特点‌:

  • 数据流独立‌:每个订阅者从头开始消费数据。
  • 延迟生成‌:数据在订阅时才开始生成(惰性计算)。
  • 资源隔离‌:不同订阅者的数据生成逻辑互不影响。

适用场景‌:

  • 需要每个订阅者获取完整数据(如 HTTP 请求、数据库查询)。
  • 数据源的生成成本较高,但需确保订阅者的独立性。
Flux<Integer> coldFlux = Flux.just(1,2)
        .doOnNext(i -> System.out.println("doOnNext item: " + i));
coldFlux.subscribe(i -> System.out.println("subscribe1: " + i));
coldFlux.subscribe(i -> System.out.println("subscribe2: " + i));

热发布者(Hot Publisher)

  • 定义‌:热发布者共享一个‌统一的数据流‌,所有订阅者消费同一份数据。数据源的生成与订阅者的订阅时间无关,后订阅的订阅者可能错过早期数据。

    特点‌:

    • 数据流共享‌:所有订阅者接收同一数据源。
    • 实时性‌:数据源的生成独立于订阅行为。
    • 资源复用‌:多个订阅者共享同一数据生成逻辑。

    适用场景‌:

    • 实时事件推送(如传感器数据、股票报价)。
    • 需要广播数据,避免重复生成高成本操作(如 WebSocket 消息)。

热发布者的实现方式有如下几种:

  • ConnectableFlux ‌(手动控制)

    • 通过 publish() 方法将 Flux 转换为 ConnectableFlux,需手动调用 connect() 启动数据流。
    // 创建 ConnectableFlux 并转换为热发布者
    ConnectableFlux<Integer> hotFlux = Flux.just(1, 2)
            .doOnNext(i -> System.out.println("doOnNext item: " + i))
            .publish(); // 转换为 ConnectableFlux
    hotFlux.subscribe(i -> System.out.println("subscribe1: " + i));
    hotFlux.subscribe(i -> System.out.println("subscribe2: " + i));
    // 手动触发数据流开始
    hotFlux.connect();
    
  • autoConnect() ‌(自动连接)

    • 当达到指定订阅者数量时,自动启动数据流。
    Flux<Integer> hotFlux = Flux.just(1, 2)
            .doOnNext(i -> out.println("doOnNext item: " + i))
            .publish()// 转换为 ConnectableFlux
            .autoConnect(2);// 当有 2 个订阅者时自动启动
    hotFlux.subscribe(i -> System.out.println("subscribe1: " + i));
    hotFlux.subscribe(i -> System.out.println("subscribe2: " + i));
    
  • share() ‌(简化热发布者)

    • 等价于 publish().refCount(1):当第一个订阅者到来时启动,最后一个取消订阅时终止。
    Flux<Long> sharedFlux = Flux.interval(Duration.ofSeconds(1))
            .doOnNext(i -> System.out.println("doOnNext: " + i))
            .take(5)
            .share();
    sharedFlux.subscribe(i -> System.out.println("subscribe1: " + i));
    Thread.sleep(2500);
    sharedFlux.subscribe(i -> System.out.println("subscribe2: " + i)); // subscribe2错过前2个数据
    
  • replay()‌(历史数据缓存)

    • 允许新订阅者消费订阅前的历史数据(缓存策略可配置)。
    ConnectableFlux<Integer> replayFlux = Flux.range(1, 3)
            .doOnNext(i -> System.out.println("doOnNext: " + i))
            .replay(2);// 缓存最近2个数据
    replayFlux.subscribe(i -> System.out.println("subscribe1: " + i));
    replayFlux.connect();
    Thread.sleep(1000);
    replayFlux.subscribe(i -> System.out.println("subscribe12: " + i)); // subscribe2收到最后2个数据
    

冷发布者和热发布者对比表格

特性冷发布者热发布者
数据生成时机订阅时生成提前生成(或由 connect() 触发)
订阅者独立性每个订阅者独立消费完整数据共享同一数据流
资源消耗高(每个订阅者独立生成)低(共享生成逻辑)
典型场景数据库查询、静态数据实时事件、广播

核心操作符

类别操作符示例功能描述
转换操作符buffermapflatMapwindow修改流中元素结构或内容(如分组、映射、扁平化)
过滤操作符filtertakeskip按条件筛选元素(如保留满足条件的元素、跳过前N项)
组合操作符mergeconcatzip合并多个流(如按顺序连接、并行合并、元素一一配对)
条件操作符anyallhasElement判断流中元素是否满足条件(如是否存在满足条件的元素)
数学操作符countsumreduce对元素进行聚合计算(如统计总数、求和、累加)
错误处理操作符onErrorReturnonErrorResume异常时提供备选值或切换至备用流(如返回静态值、动态恢复逻辑)
工具操作符delaytimeoutlogsubscribe控制流生命周期(如延迟发送、超时中断、记录日志、触发订阅)
整个数据源操作doOnNext,,,doOnRequest,doOnSubscribe,doOnComplete其中以doOn开头的可以对整个数据链的不同状态进行操作
//转换操作符、过滤操、条件及数学操作符类似Java的Stream这里不做过多赘述
//map
Flux.just(1, 2, 3).map(i -> i + 1).subscribe(System.out::println);
//filter
Flux.just("a", "b", "c").filter(s -> s.equals("a")).subscribe(System.out::println);
//flatMap
Flux.just("a", "b", "c").flatMap(s -> Flux.just(s.toUpperCase())).subscribe(System.out::println);
//reduce
Flux.just(1, 2, 3).reduce(0, Integer::sum).subscribe(System.out::println);
//window  窗口使用
Flux.just(1, 2, 3, 4, 5, 6).window(3, 1).flatMap(e -> e.reduce(0, Integer::sum)).subscribe(System.out::println);
//buffer  背压或者批处理使用,会缓存数据
Flux.just(1, 2, 3, 4, 5, 6).buffer(3, 1).subscribe(System.out::println);

zip

zip操作符可以将多个(最多8个)流合并成一个流,合并的方式是将两个流中的元素按照顺序一一对应,然后将两个元素组合成一个元素。 如果两个流的长度不一致,那么最终合并成的流的长度就是两个流中长度较短的那个流的长度。

Flux<String> flux1 = Flux.just("a", "b", "c");
Flux<String> flux2 = Flux.just("d", "e", "f");
Flux<String> flux3 = Flux.just("1", "2", "3");
Flux.zip(flux1, flux2, flux3).subscribe(System.out::println);


//输出 
[a,d,1] 
[b,e,2] 
[c,f,3]

merge

merge 操作符可以将两个流合并成一个流,合并的方式是将两个流中的元素交替地放入到合并后的流中。同时运行,根据时间先后运行。

Flux<Integer> flux3 = Flux.just(1, 2, 3).delayElements(Duration.ofMillis(80));
Flux<Integer> flux4 = Flux.just(4, 5, 6).delayElements(Duration.ofMillis(50));
flux3.mergeWith(flux4).subscribe(System.out::println);

//输出 由于是根据时间先后处理,所以结果大概率是这样,也有可能会稍有不同
4
1
5
6
2
3

concat

concat 操作符可以将两个流合并成一个流,合并的方式是将两个流中的元素按照顺序放入到合并后的流中。按照顺序分别运行,flux1运行完成以后再运行flux2

Flux<Integer> flux1 = Flux.just(1, 2, 3).delayElements(Duration.ofMillis(80));
Flux<Integer> flux2 = Flux.just(4, 5, 6).delayElements(Duration.ofMillis(50));
flux1.concatWith(flux2).subscribe(System.out::println);
//输出
1
2
3
4
5
6

数据源操作

Project Reactor 提供了大量的以doOn开头的方法,这些方法用于在数据流的生命周期中插入副作用逻辑(如日志、监控或资源管理),‌不修改数据流本身,仅用于观察或触发行为‌。

Flux.just(1, 2, 3, 4, 5, 6)
        .doOnNext(s -> System.out.println("doOnNext: " + s)).subscribe();
System.out.println("----------------");
Flux.just(1, 2, 3)
        .doOnRequest(s -> System.out.println("doOnRequest: " + s)).subscribe(System.out::println);
方法触发时机参数类型典型场景
doOnSubscribe订阅时Consumer<Subscription>资源初始化
doOnNext元素推送时Consumer<T>日志记录、状态更新
doOnError发生错误时Consumer<Throwable>错误监控、报警
doOnComplete流正常结束时Runnable完成通知
doOnRequest下游请求数据时Consumer<Long>背压调试、请求量监控
doOnCancel取消订阅时Runnable资源释放
doOnEach所有事件发生时Consumer<Signal<T>>统一事件处理
doOnTerminate流终止前(完成/错误前)Runnable终止前清理逻辑
doAfterTerminate流终止后(完成/错误后)Runnable终止后统计
doOnDiscard元素被丢弃时Consumer<T>资源回收、数据一致性检查

订阅与调度

订阅机制

subscribe 操作符用来订阅流中的元素。 当流中的元素没有被订阅的时候,所有的操作都不会触发,只有当流中的元素被订阅的时候,所有的操作才会触发。

调度策略

Schedulers 是管理线程和并发任务的核心工具,用于控制响应式流的执行上下文。通过合理选择调度器,可以优化资源利用、避免阻塞,并提升应用性能

调度器线程模型适用场景注意事项
immediate当前线程轻量级同步操作避免阻塞
single单线程严格顺序执行避免长时间阻塞
boundedElastic动态线程池阻塞 I/O 操作控制最大线程数和队列容量
parallel固定大小线程池计算密集型并行任务线程数默认等于 CPU 核心数
fromExecutorService自定义线程池集成现有线程池需自行管理生命周期
默认调度器

在 Project Reactor 中,可以很方便的通过publishOn和subscribeOn来切换使用的线程调度器。

Flux.range(1, 10)
        .publishOn(Schedulers.boundedElastic()) //切换调度器
        .log("publish thread:")
        .flatMap(n -> Mono.fromCallable(() -> n).subscribeOn(Schedulers.parallel()))  //切换调度器
        .log("subscribe thread:")
        .subscribe();
自定义虚拟线程调度器
Scheduler customSchedule = Schedulers.fromExecutor(Executors.newScheduledThreadPool(5));
Flux.range(1, 10)
        .publishOn(customSchedule)
        .log("publish thread:")
        .flatMap(n -> Mono.fromCallable(() -> n).subscribeOn(Schedulers.parallel()))
        .log("subscribe thread:")
        .subscribe();

高级控制组件

Processor与Sink的关系

在 Project Reactor 中,‌**Processor‌ 曾是一个关键组件,但随着 Reactor 3.4+ 版本的演进,官方逐渐将其标记为‌弃用(Deprecated)** ‌,并推荐使用更现代的 ‌**Sink API**‌ 替代。以下是弃用原因、两者核心区别。

1. ‌线程安全
  • processor: 大多数 Processor 实现(如 DirectProcessorUnicastProcessor)‌非线程安全‌,直接调用 onNextonComplete 等方法需手动同步。
  • Sink:  原子性操作‌:Sink 提供 tryEmitNexttryEmitError 等方法,确保多线程推送数据时的安全性。
2. ‌角色定位
  • Processor:  同时作为 Publisher 和 Subscriber,这种设计虽然灵活,但导致职责不清晰,容易误用。
  • Sink: 仅作为纯生产者(仅生成数据流)
3. ‌背压处理
  • processor

    对背压的支持差异大:

    • DirectProcessor 完全忽略背压(无界队列)。
    • UnicastProcessor 支持单订阅者的背压,但需手动配置缓冲区。
  • Sink‌:内置配置,通过 onBackpressureBufferonBackpressureError 等链式方法直接定义背压行为。

4. ‌生命周期管理复杂
  • processor‌:需显式调用 onComplete 或 onError 结束流,若遗漏可能导致资源泄漏或订阅者挂起。
  • Sink: 通过 tryEmitComplete 和 tryEmitError 明确结束流,避免资源泄漏。
5. ‌API 设计
  • processor‌:Processor 的 API 未针对现代响应式编程模式优化(如缺少对重试、重播的内置支持)。
  • Sink: 灵活简单,通过 Sinks.Many 的 multicast()unicast() 或 replay() 快速配置多订阅者行为。

Sink的使用

发送单个数据
Sinks.One<String> sink = Sinks.one();
Mono<String> mono = sink.asMono();
mono.subscribe(
        value -> System.out.println("Received: " + value),
        error -> System.err.println("Error: " + error),
        () -> System.out.println("Completed")
);
sink.tryEmitValue("Hello");  // 等效于 tryEmitNext + tryEmitComplete
发送多个数据
// 创建多播 Sink, 并设计缓冲被压策略
Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();
//转换为flux
Flux<String> hotFlux = sink.asFlux().map(String::toUpperCase);
// 订阅者A
hotFlux.subscribe(i -> System.out.println("订阅者A: " + i));
// 订阅者B
hotFlux.subscribe(i -> System.out.println("订阅者B: " + i));
// 发送数据
sink.tryEmitNext("hello");
sink.tryEmitNext("world");
sink.tryEmitComplete();
支持历史数据
// 创建重播 Sink,保留最近 2 个元素
Sinks.Many<String> sink = Sinks.many().replay().limit(2);
sink.tryEmitNext("A");
sink.tryEmitNext("B");
// 订阅者1 (接收历史数据 A, B)
sink.asFlux().subscribe(s -> System.out.println("Sub1: " + s));
// 推送新数据
sink.tryEmitNext("C");
// 订阅者2(接收历史数据 B, C)
sink.asFlux().subscribe(s -> System.out.println("Sub2: " + s));

//输出
Sub1: A
Sub1: B
Sub1: C
Sub2: B
Sub2: C

背压

背压策略

1.onBackpressureBuffer(缓冲策略)
  • 行为‌:将未消费的数据存储在缓冲区中,等待下游请求时发送。
  • 配置选项
    • 缓冲区大小‌:可指定有界或无界(默认无界,需谨慎使用)。
    • 溢出策略
      • ERROR:缓冲区满时抛出 IllegalStateException
      • DROP_LATEST:丢弃新数据,保留旧数据。
      • DROP_OLDEST:丢弃最旧数据,保留新数据。
  • 适用场景‌:允许短暂的速度不匹配,但需控制内存占用。
2. onBackpressureError(错误策略)
  • 行为‌:当缓冲区满或下游未请求时,‌立即抛出错误‌(IllegalStateException)。
  • 适用场景‌:严格要求实时性,容忍数据丢失但需快速失败。
3. directBestEffort(尽力而为策略)
  • 行为‌:无缓冲区,直接推送数据到下游。如果下游未请求,‌静默丢弃新数据‌。
  • 特点‌:避免内存占用,但可能导致数据丢失。
  • 适用场景‌:实时事件处理(如日志、指标采集),允许偶尔丢失。
4. replay(重播策略)
  • 行为新订阅者重播历史数据
    • 同时支持实时数据推送。
    • 可配置重播的缓冲区大小(如保留最近的 N 个元素)。
  • 背压处理
    • 对新订阅者:重播历史数据时遵循背压请求。
    • 对实时数据:使用 onBackpressureBuffer 或 directBestEffort 策略。
  • 适用场景‌:需要新订阅者获取历史数据的场景。
‌ 默认策略
  • multicast() ‌:默认使用 directBestEffort(无缓冲区)。
  • unicast() ‌:默认使用 onBackpressureBuffer(无界缓冲区)。
  • replay() ‌:默认保留所有历史数据(无界缓冲区)。

Hooks与Context

Hooks

在 Project Reactor 中,‌Hooks‌ 是一组全局回调机制,允许对 Reactor 库的默认行为进行‌定制化扩展‌,用于调试、监控或修改响应式流的执行逻辑。

Hooks 的核心用途
  • 全局错误处理‌:捕获未被下游处理的异常。
  • 操作符生命周期监控‌:在操作符执行前后插入自定义逻辑。
  • 调试与追踪‌:增强堆栈跟踪信息,定位异步流问题。
  • 行为修改‌:动态替换或包装操作符的实现。
常用 Hooks 及功能

1. onOperatorError

  • 作用‌:捕获操作符执行过程中抛出的‌未处理异常‌。
  • 典型场景‌:统一日志记录、转换错误类型。
Hooks.onOperatorError((error, context) -> {
    System.err.println("全局捕获异常: " + error);
    return error; 
});

2. onNextDropped

  • 作用‌:处理因下游取消订阅、背压溢出等原因被‌丢弃的 onNext 元素‌。
  • 典型场景‌:记录丢失的数据,用于审计或补偿。
Hooks.onNextDropped(item -> 
    System.out.println("元素被丢弃: " + item)
);

3. onErrorDropped

  • 作用‌:处理因下游已终止(如已调用 onComplete)而被‌丢弃的 onError 信号‌。
  • 典型场景‌:避免静默忽略错误。
    Hooks.onErrorDropped(error -> 
        System.err.println("错误被丢弃: " + error)
    );
    

4. onOperatorDebug

  • 作用‌:启用‌调试模式‌,为异步操作符生成增强的堆栈跟踪信息(含订阅点位置)。
  • 代价‌:增加性能开销,‌仅限开发环境使用‌。
    Hooks.onOperatorDebug(); // 启用调试模式
    

‌5. onEachOperator / onLastOperator

  • 作用‌:在‌每个操作符执行前后‌插入自定义逻辑(如日志、指标采集)。
  • 典型场景‌:性能监控、动态修改数据流。
    Hooks.onEachOperator(operator -> {
        long start = System.currentTimeMillis();
        return original -> original.doFinally(signal -> 
            System.out.println("操作符耗时: " + (System.currentTimeMillis() - start) + "ms")
        );
    });
    
  1. 重置 Hooks
  • 恢复默认行为
    javaCopy CodeHooks.resetOnOperatorError();
    Hooks.resetOnNextDropped();
    Hooks.resetOnOperatorDebug();
    

Context

在 Project Reactor 中,‌**Context‌ 是用于在响应式流的各个阶段之间传递‌上下文数据**‌的核心机制。它解决了传统 ThreadLocal 在异步、多线程环境中的局限性,允许数据在操作符链中安全传递。以下是 Context 的详细解析,涵盖其设计思想、API 使用及典型场景。

1. 为什么需要 Context?
  • 问题‌:在异步响应式流中,数据可能由不同线程处理,ThreadLocal 无法跨线程传递。
  • 解决方案‌:Context 提供一种与订阅链绑定的、不可变的键值存储,确保上下文数据在流的生命周期内可被安全访问。
2. Context 的特点
  • 不可变性‌:每次修改会生成新实例,确保线程安全。

  • 订阅链绑定‌:数据跟随订阅链传递,而非依赖线程(需要注意的是Context的传递是从底部往上传递的)。

  • 键值存储‌:类似 Map 结构,支持类型安全的键(ContextKey)。

  • 自底向上(Downstream → Upstream)

    • 写入顺序‌:后调用的 contextWrite 会覆盖先调用的。
    • 读取顺序‌:下游(靠近订阅点)的 Context 优先被访问。

通过 contextWrite 操作符将 Context 写入响应式流,通过deferContextual在流中读取 Context

//注意由于onNext的传递是从底部往上传递的,所以必须在下面(A点)先写入才能在(B点读取到)
Flux.just("A", "B", "C", "D")
        //记为B点  拼接 Context 中的值
        .flatMap(s -> {
                    System.out.println("flat item:" + s);
                    return Mono.deferContextual(ctx -> Mono.just(s + ctx.get("suffix")));
                }
        )
        //记为A点  写入 Context(关键:必须在读取操作之前调用)
        .contextWrite(Context.of("suffix", "-ctx"))
        // 订阅输出结果
        .subscribe(System.out::println);