一、引言
在处理高并发 HTTP 调用时,传统的同步阻塞式 HTTP 客户端(如 RestTemplate
)往往依赖多线程来提升并发性能:一个线程对应一条连接,请求发出后线程阻塞等待响应。尽管可以通过增加线程数来提高并发量,但这通常会带来上下文切换开销巨大、内存占用的大幅上升,严重时甚至可能导致 OOM(内存溢出)问题。
这个问题本质上与我们在学习 Java BIO 和 NIO 时遇到的阻塞模型痛点如出一辙。那么,是否存在一个成熟的、基于非阻塞模型的 HTTP 客户端,用以更高效地应对高并发场景呢?
答案是肯定的 ------ Spring 提供了 WebClient
,一个基于响应式编程模型、构建于 Project Reactor
之上的非阻塞 HTTP 客户端,作为 RestTemplate
的现代替代者,已经在众多实际项目中得到了验证和广泛应用。
本文将从响应式编程模型的基本概念出发,介绍Reactive Streams规范、Project Reactor
的核心机制。
下一篇文章讲介绍其在 WebClient 中的应用;再深入讲解 WebClient
的基本用法与核心特性,并通过对比 WebClient
与 RestTemplate
在压力测试下的性能表现,直观感受 WebClient 在高并发场景下的优势。
二、响应式编程
WebClient 是基于 Project Reactor 实现的,而 Project Reactor 又是建立在响应式编程模型之上的。因此,在深入理解 WebClient 之前,我们有必要先了解什么是响应式编程。
根据维基百科的定义,响应式编程(Reactive Programming)是一种声明式编程范式,关注数据流及其变化的传播:
"响应式编程是一种基于异步数据流和变化传播的声明式编程范式。借助这一范式,程序可以更自然地表达静态(如数组)或动态(如事件源)的数据流,并通过执行模型中推断出的依赖关系,实现数据变化的自动传播。"
通俗地讲,响应式编程可以理解为一种"事件驱动 + 数据流处理"的编程范式。程序不再显式地控制流程,而是通过"响应"外部事件或数据的变化来驱动计算流程。例如,当某个数据源产生新的数据时,程序会自动触发相关的处理逻辑,无需人为干预。
这一模型非常适合处理高并发、异步、IO 密集型的场景,因为它避免了线程阻塞,能够以更少的资源处理更多的任务。
响应式编程的核心特性
响应式编程具有以下几个显著特性:
- 异步非阻塞:数据的产生与消费彼此独立,通过回调或事件通知机制解耦,从而避免线程阻塞。
- 数据流驱动:程序中的数据以流的形式存在,开发者通过操作流(如
map
、filter
、flatMap
等)来定义处理逻辑。 - 背压机制(Backpressure):在数据生产者与消费者速率不匹配时,背压机制可以有效协调两者之间的节奏,避免内存溢出或消息堆积。
- 声明式编程风格:开发者更多地关注"做什么"而不是"怎么做",代码更简洁、可读性更强。
三、Reactive Streams规范
为了统一响应式编程中的数据流处理标准,在 2013 年,Lightbend(当时是 Typesafe)、Netflix、Pivotal、Red Hat 等公司联合提出了 Reactive Streams 规范,其核心目标是为异步流处理建立一个 非阻塞、具备背压机制的通用标准。
Reactive Streams 的核心组成
Reactive Streams 定义了四个核心接口(Java 9 之后被正式纳入 JDK 标准包 java.util.concurrent.Flow
):
接口 | 说明 |
---|---|
Publisher | 表示一个可以发布数据的源(生产者),它可以发出多个元素或信号(包括错误或完成)。 |
Subscriber | 表示数据的消费者,用于接收来自 Publisher 的数据流。 |
Subscription | 连接 Publisher 和 Subscriber 的桥梁,允许 Subscriber 向 Publisher 请求数据(背压控制)。 |
Processor<T, R> | 既是 Publisher 又是 Subscriber,用于数据的中间处理。 |
基本的交互流程如下:
Subscriber
向Publisher
订阅。Publisher
返回一个Subscription
给Subscriber
。Subscriber
通过Subscription.request(n)
请求数据。Publisher
根据请求量n
发送最多n
个数据项。- 若过程中发生异常,
Publisher
会调用onError
;正常结束时调用onComplete
。
这种 请求-响应(pull-based)+ 事件驱动(push-based) 的混合模型,是 Reactive Streams 背压机制的关键所在。

接口定义
可能说概念还是比较模糊,直接看Reactive Streams是怎么定义这几个接口的
2.1 Publiser(发布者)
从接口定义及注释信息,可以知道:
- Publisher提供了subscribe方法,通过该方法可以将Subscriber传入
- 调用subscribe方法之后,就意味着订阅开始,Publisher就向Subscriber开始数据传输了
- subscribe可以被多次调用,也就是说一个Publisher可以被多个Subscriber订阅。每次订阅就会产生一个Subscription
- Publisher不能被同一个Subscriber订阅多次
java
package org.reactivestreams;
/**
* A {@link Publisher} is a provider of a potentially unbounded number of sequenced elements, publishing them according to
* the demand received from its {@link Subscriber}(s).
* <p>
* A {@link Publisher} can serve multiple {@link Subscriber}s subscribed {@link Publisher#subscribe(Subscriber)} dynamically
* at various points in time.
*
* @param <T> the type of element signaled
*/
public interface Publisher<T> {
/**
* Request {@link Publisher} to start streaming data.
* <p>
* This is a "factory method" and can be called multiple times, each time starting a new {@link Subscription}.
* <p>
* Each {@link Subscription} will work for only a single {@link Subscriber}.
* <p>
* A {@link Subscriber} should only subscribe once to a single {@link Publisher}.
* <p>
* If the {@link Publisher} rejects the subscription attempt or otherwise fails it will
* signal the error via {@link Subscriber#onError(Throwable)}.
*
* @param s the {@link Subscriber} that will consume signals from this {@link Publisher}
*/
public void subscribe(Subscriber<? super T> s);
}
2.2 Subscriber(订阅者)
从Subscriber的接口定义,我们可以得出以下信息:
- Subscriber包含四个回调方法,Publisher会分别在不同状态下调用
- onSubscribe:当调用Publisher的subscribe方法之后,Publisher会将Subscription通过方法onSubscribe返还给Subscriber,类似于一个回执的东西。
- onNext:通过该方法,Publisher向Subscriber发送单个数据
- onError:当处理出现异常之后,Publisher会调用该方法,并且流就此中断,不会继续了。
- onComplete:当成功结束了,Publisher会调用该方法。
- 背压:Subscriber需要根据自己的处理情况,通过Subscription.request向Publisher索取数据
java
package org.reactivestreams;
/**
* Will receive call to {@link #onSubscribe(Subscription)} once after passing an instance of {@link Subscriber} to {@link Publisher#subscribe(Subscriber)}.
* <p>
* No further notifications will be received until {@link Subscription#request(long)} is called.
* <p>
* After signaling demand:
* <ul>
* <li>One or more invocations of {@link #onNext(Object)} up to the maximum number defined by {@link Subscription#request(long)}</li>
* <li>Single invocation of {@link #onError(Throwable)} or {@link Subscriber#onComplete()} which signals a terminal state after which no further events will be sent.
* </ul>
* <p>
* Demand can be signaled via {@link Subscription#request(long)} whenever the {@link Subscriber} instance is capable of handling more.
*
* @param <T> the type of element signaled
*/
public interface Subscriber<T> {
/**
* Invoked after calling {@link Publisher#subscribe(Subscriber)}.
* <p>
* No data will start flowing until {@link Subscription#request(long)} is invoked.
* <p>
* It is the responsibility of this {@link Subscriber} instance to call {@link Subscription#request(long)} whenever more data is wanted.
* <p>
* The {@link Publisher} will send notifications only in response to {@link Subscription#request(long)}.
*
* @param s the {@link Subscription} that allows requesting data via {@link Subscription#request(long)}
*/
public void onSubscribe(Subscription s);
/**
* Data notification sent by the {@link Publisher} in response to requests to {@link Subscription#request(long)}.
*
* @param t the element signaled
*/
public void onNext(T t);
/**
* Failed terminal state.
* <p>
* No further events will be sent even if {@link Subscription#request(long)} is invoked again.
*
* @param t the throwable signaled
*/
public void onError(Throwable t);
/**
* Successful terminal state.
* <p>
* No further events will be sent even if {@link Subscription#request(long)} is invoked again.
*/
public void onComplete();
}
2.3 Subscription(订阅)
从Subscription的接口定义,我们可以得出以下信息:
- Subscription代表了订阅整个生命周期,与Subscriber是一对一的关系
- Subscription是用于Subscriber向Publisher发送信号的
- 通过request方法,Subscriber可以向Publisher请求指定数量的数据
- 通过cancel方法,Subscriber可以向Publisher停止发送数据并且清理资源
java
package org.reactivestreams;
/**
* A {@link Subscription} represents a one-to-one lifecycle of a {@link Subscriber} subscribing to a {@link Publisher}.
* <p>
* It can only be used once by a single {@link Subscriber}.
* <p>
* It is used to both signal desire for data and cancel demand (and allow resource cleanup).
*/
public interface Subscription {
/**
* No events will be sent by a {@link Publisher} until demand is signaled via this method.
* <p>
* It can be called however often and whenever needed---but if the outstanding cumulative demand ever becomes Long.MAX_VALUE or more,
* it may be treated by the {@link Publisher} as "effectively unbounded".
* <p>
* Whatever has been requested can be sent by the {@link Publisher} so only signal demand for what can be safely handled.
* <p>
* A {@link Publisher} can send less than is requested if the stream ends but
* then must emit either {@link Subscriber#onError(Throwable)} or {@link Subscriber#onComplete()}.
*
* @param n the strictly positive number of elements to requests to the upstream {@link Publisher}
*/
public void request(long n);
/**
* Request the {@link Publisher} to stop sending data and clean up resources.
* <p>
* Data may still be sent to meet previously signalled demand after calling cancel.
*/
public void cancel();
}
四、Project Reactor 基础与核心用法
Project Reactor 就是基于响应式编程特性构建的响应式库,它实现了 Reactive Streams 规范,提供了两个核心类型:Mono
(表示 0 或 1 个元素)和 Flux
(表示 0 到 N 个元素)。这两个类型构成了响应式编程的基础抽象,贯穿整个 WebClient 的使用过程。
接下来,我们将介绍 Project Reactor 的核心机制,并通过一些简单示例来帮助理解 Mono 和 Flux 是如何工作的。
1. Mono/Flux
1.1 概述
Mono和Flux是Publisher的两个实现,主要用于生产数据序列,下面将介绍这两者。
- 作用:数据流的源头,负责生产数据。
- 核心实现:
Flux<T>
:发射 0-N个元素 的流(例如集合、流式数据)。Mono<T>
:发射 0或1个元素 的流(例如异步任务、Optional结果)。
- 示例:
java
Flux<String> flux = Flux.just("A", "B", "C"); // 多元素
Mono<String> mono = Mono.just("Hello"); // 单元素
1.2 创建方式
1.2.1 Flux 创建方式
Flux.just
用于创建一个包含固定元素的 Flux。
java
Flux<String> flux = Flux.just("A", "B", "C");
这个方法适合在已知数据元素的情况下快速创建一个数据流。元素按顺序发出,然后完成。
Flux.fromArray
从数组创建 Flux。
java
Flux<String> flux = Flux.fromArray(new String[]{"A", "B"});
常用于已有数组数据的场景,将其转换为响应式流进行处理。
Flux.fromIterable
从 List
、Set
等集合类型创建 Flux。
java
Flux<String> flux = Flux.fromIterable(Arrays.asList("A", "B"));
适合处理 Java 集合类,将其转化为流式处理形式。
Flux.fromStream
从 Java 8 Stream 创建 Flux。
java
Flux<String> flux = Flux.fromStream(Stream.of("A", "B"));
注意:Stream 只能消费一次,不能重复订阅该 Flux。
Flux.range
生成一个整数范围内的序列。
java
Flux<Integer> flux = Flux.range(1, 5); // 输出 1 到 5
适用于需要生成定长数字序列的场景,常用于测试或批量操作。
Flux.interval
以固定时间间隔生成递增数字(异步)。
java
Flux<Long> flux = Flux.interval(Duration.ofSeconds(1));
每隔一秒发出一个值,适用于定时任务、轮询等场景。
Flux.empty
创建一个空的 Flux,订阅后立即完成。
java
Flux<Object> flux = Flux.empty();
可以用于占位或无数据时的默认返回。
Flux.error
创建一个立即以错误终止的 Flux。
java
Flux<Object> flux = Flux.error(new RuntimeException("Something went wrong"));
常用于异常测试或显式地向下游传递错误信号。
Flux.never
创建一个永不发送数据和完成信号的 Flux。
java
Flux<Object> flux = Flux.never();
主要用于测试或特定逻辑阻塞场景。
Flux.generates
通过同步状态生成数据,按需发出元素。
java
Flux<Integer> flux = Flux.generate(() -> 0, (state, sink) -> {
sink.next(state);
return state + 1;
});
适合基于状态的单线程数据生成流程。
Flux.create
支持异步、回调式数据流生成,同时支持背压。
java
Flux<String> flux = Flux.create(sink -> {
asyncEventSource.register(e -> sink.next(e));
});
适用于需要手动推送数据的异步场景,如事件监听。
Flux.concat
连接多个 Publisher,顺序发出各序列中的元素。
java
Flux<String> flux = Flux.concat(Flux.just("A"), Flux.just("B"));
适用于按顺序合并多个流。
Flux.merge
合并多个异步 Publisher,元素可能交错。
java
Flux<Long> flux = Flux.merge(
Flux.interval(Duration.ofMillis(100)),
Flux.interval(Duration.ofMillis(150))
);
适合多个异步源并发发出数据的场景。
Flux.defer
延迟创建 Flux,每次订阅时生成一个新的序列。
java
Flux<Long> flux = Flux.defer(() -> Flux.just(System.currentTimeMillis()));
用于每次订阅都需要最新数据的场景。
1.2.2 Mono 创建方式
Mono.just
创建一个包含单个元素的 Mono。
java
Mono<String> mono = Mono.just("A");
用于返回单一结果的场景,如数据库单条查询。
Mono.justOrEmpty
根据输入是否为 null 创建 Mono,null 会变成 Mono.empty()。
java
Mono<String> mono = Mono.justOrEmpty(null);
安全地处理可能为 null 的值。
Mono.fromCallable
从 Callable
延迟生成 Mono,自动捕获异常。
java
Mono<String> mono = Mono.fromCallable(() -> "Result");
适用于需要延迟执行、且可能抛出异常的操作。
Mono.fromSupplier
从 Supplier
延迟生成 Mono,不捕获异常。
java
Mono<String> mono = Mono.fromSupplier(() -> "Result");
类似 fromCallable
,但不处理异常,适合无异常风险的延迟执行。
Mono.fromFuture
将 CompletableFuture
转为 Mono。
java
Mono<String> mono = Mono.fromFuture(CompletableFuture.completedFuture("Result"));
常用于与异步方法或非响应式 API 集成。
Mono.fromRunnable
执行 Runnable
后返回空 Mono。
java
Mono<Void> mono = Mono.fromRunnable(() -> System.out.println("Done"));
用于触发副作用动作(如日志、清理)但不返回数据。
Mono.empty()
创建一个空的 Mono,立即完成。
java
Mono<Object> mono = Mono.empty();
适用于无返回值的场景,如删除操作等。
Mono.error
创建一个立即错误终止的 Mono。
java
Mono<Object> mono = Mono.error(new RuntimeException("Error occurred"));
用于错误流转,或测试异常处理。
Mono.never
创建一个永不发出任何信号的 Mono。
java
Mono<Object> mono = Mono.never();
适合测试超时等场景。
Mono.create
通过回调方式手动生成 Mono,适用于异步或自定义场景。
java
Mono<String> mono = Mono.create(sink -> {
asyncTask(result -> {
if (success) sink.success(result);
else sink.error(e);
});
});
可以精细控制完成、失败等状态。
Mono.defer
每次订阅时重新生成 Mono,确保数据时效性。
java
Mono<Long> mono = Mono.defer(() -> Mono.just(System.currentTimeMillis()));
适用于动态计算或需要实时数据的情况。
Mono.zip
组合多个 Mono,等待所有 Mono 完成后合并结果。
java
Mono<String> mono = Mono.zip(monoA, monoB, (a, b) -> a + b);
用于聚合多个异步结果。
Mono.when
等待多个 Mono 完成,但忽略返回值。
java
Mono<Void> mono = Mono.when(monoA, monoB);
常用于多个任务同步执行的场景。
1.3 关键区别说明
- Flux:
- 适用于 0..N 个元素的异步序列。
- 支持复杂流操作(过滤/转换/背压等)。
- 生成方式更侧重序列的连续性(如
range
/interval
)。
- Mono:
- 适用于 0..1 个元素的异步结果。
- 常用于单次异步操作(如 HTTP 请求、Future 封装)。
- 延迟创建(
fromSupplier
/defer
)避免立即执行逻辑。
1.4 使用场景
- Flux:流式数据处理(文件读取、消息队列消费)。
- Mono:单值异步操作(数据库查询、结果聚合)。
2. 订阅者
2.1 概述
在 Project Reactor 中,订阅者是响应式流中处理数据的核心组件,负责接收和处理 Publisher(Mono/Flux)发出的数据流。
2.2 订阅方式
下面将介绍订阅者的几种实现方式
2.1 基础订阅(Lambda 表达式)
2.1.1 最简单的 subscribe(什么都不处理)
最简单的方式,只触发流,不处理任何数据或错误
方法为:
java
public final Disposable subscribe()
出参:Disposable可用于取消订阅、判断是否已取消订阅
示例:
java
Flux.just(1, 2, 3).subscribe();
2.1.2 只处理 onNext
只处理 onNext
,不处理错误或完成
方法为:
java
public final Disposable subscribe(Consumer<? super T> consumer)
入参:consumer用于处理onNext,也就是消费每一个数据
出参:Disposable可用于取消订阅、判断是否已取消订阅
示例:
java
Flux.just(1, 2, 3).subscribe(System.out::println);
2.1.3 处理 onNext 和 onError
方法为:
java
public final Disposable subscribe(@Nullable Consumer<? super T> consumer, Consumer<? super Throwable> errorConsumer)
入参:consumer用于处理onNext,也就是消费每一个数据;errorConsumer用于处理异常信号
出参:Disposable可用于取消订阅、判断是否已取消订阅
示例:
java
Flux.just(1, 2, 3)
.map(i -> 10 / (i - 2))
.subscribe(
System.out::println,
error -> System.err.println("Error: " + error)
);
2.1.4 处理 onNext、onError 和 onComplete
方法为:
java
public final Disposable subscribe(
@Nullable Consumer<? super T> consumer,
@Nullable Consumer<? super Throwable> errorConsumer,
@Nullable Runnable completeConsumer)
入参:consumer用于处理onNext,也就是消费每一个数据;errorConsumer用于处理异常信号;completeConsumer用于处理完成信号
出参:Disposable可用于取消订阅、判断是否已取消订阅
示例:
java
Flux.just(1, 2, 3).subscribe(
System.out::println,
System.err::println,
() -> System.out.println("Stream completed.")
);
2.2 自定义订阅者
如果想完全控制流的行为,BaseSubscriber
是一个非常强大的选择。通过继承 BaseSubscriber
,可以在 hookOnSubscribe
中控制请求的数量,在 hookOnNext
中处理每个元素,并灵活响应背压。

示例:
java
Flux.range(1, 10).subscribe(new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
System.out.println("Subscribed");
request(2); // 控制每次请求数量
}
@Override
protected void hookOnNext(Integer value) {
System.out.println("Value: " + value);
request(1); // 每处理一个再请求下一个
}
});
BaseSubscriber
包含以下钩子方法,可通过重写以下方法在指定状态下执行指定操作:
hookOnSubscribe
当订阅刚建立时调用。你可以在这里决定是否立即请求数据、请求多少数据,或保存 Subscription
以备后用。
用途:
- 初始化请求(如
request(1)
、request(Long.MAX_VALUE)
) - 延迟请求
- 打印日志
示例:
java
@Override
protected void hookOnSubscribe(Subscription subscription) {
System.out.println("订阅已建立");
request(1); // 主动请求一个数据项
}
hookOnNext(T value)
每当有一个元素被成功发送下来时调用。你可以在这里处理元素,并决定是否继续请求下一个。
用途:
- 消费数据
- 动态调整请求数量(如手动调用
request(1)
) - 过滤或变换数据(一般不建议在这做变换,推荐用上游操作符)
示例:
java
@Override
protected void hookOnNext(String value) {
System.out.println("收到数据: " + value);
request(1); // 每处理完一个,再请求下一个
}
hookOnComplete
当所有数据发送完成时调用(即流正常终止)。在这里你可以做一些清理或结束操作。
用途:
- 日志记录
- 释放资源
- 通知业务方完成状态
示例:
java
@Override
protected void hookOnComplete() {
System.out.println("数据发送完毕,流结束");
}
hookOnError(Throwable throwable)
当流在处理过程中发生错误时调用。可以记录错误、发送告警等。
用途:
- 错误日志
- 尝试恢复(通常不在这里做)
- 报警、通知
示例:
java
@Override
protected void hookOnError(Throwable throwable) {
System.err.println("发生错误: " + throwable.getMessage());
}
hookOnCancel
当流被取消时调用(如调用了 cancel()
或 dispose()
)。适合进行取消后的清理逻辑。
用途:
- 释放资源
- 中止连接或任务
- 记录取消原因
示例:
java
@Override
protected void hookOnCancel() {
System.out.println("订阅被取消");
}
hookFinally
无论是正常结束、出错、取消,最终都会调用该方法,类似于 try-finally 中的 finally。
用途:
- 最终清理逻辑
- 统一处理所有终止状态(完成、出错、取消)
- 日志记录
示例:
java
@Override
protected void hookFinally(SignalType type) {
System.out.println("流终止,终止类型:" + type);
}
3. Subscription
当你订阅一个 Flux
或 Mono
时,底层就会建立一个 Subscription
,并调用订阅者的 onSubscribe(Subscription s)
方法,将这个 Subscription
实例传递下去。
对于外部调用者来说,对Subscription的感知不强。它的作用一般体现在以下几个方面
3.1 订阅的使用
Disposable
调用发布者的subscribe方法之后,会返回一个Disposable。主要包含以下方法
- dispose:通过Disposable可以对订阅进行取消
- isDisposed:判断订阅是否已取消
java
@FunctionalInterface
public interface Disposable {
void dispose();
default boolean isDisposed() {
return false;
}
BaseSubscriber
从上面看到BaseSubscriber的继承关系可以看出,该类实现了Subscription。因此,只要能拿到BaseSubscriber,就能拿到Subscription;BaseSubscriber内部也可以直接调用Subscription的request方法
java
BaseSubscriber<Integer> baseSubscriber = Flux.range(0, 100)
.delayElements(Duration.ofMillis(100))
.subscribeWith(new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
log.info("subscribe");
request(10); // 请求数据
}
@Override
protected void hookOnNext(Integer value) {
log.info("receive: {}", value);
request(1);
}
@Override
protected void hookOnCancel() {
log.info("cancel");
}
});
TimeUnit.SECONDS.sleep(1);
baseSubscriber.cancel(); // 取消订阅
4. Operators(操作符)
在响应式编程中,操作符(Operator)是构建数据流的核心工具。它们负责在数据从上游流向下游的过程中,进行各种变换、过滤、合并、拆分、分组、切换线程等处理。对响应式流而言,操作符就像传统集合中的 map()
、filter()
、reduce()
等函数式编程工具,但它们是以 异步、非阻塞、流式 的方式工作。
Project Reactor 中的 Flux
和 Mono
提供了大量内置操作符,使得我们可以使用链式 API 来描述数据流的处理逻辑,从而构建出清晰、简洁且强大的异步流程。
在响应式编程中,操作符是构建数据流处理管道的核心工具。Reactor 提供了丰富的操作符,可归类为以下几类:
1. 转换类操作符(Transformation Operators)
map操作符
是最基础的元素转换工具。它对流中的每个元素应用给定的转换函数,将输入元素转换为另一种形式。当您需要简单地对元素进行一对一转换时(如类型转换、字段提取或简单计算),map是理想选择。
java
Flux.just(1, 2, 3)
.map(String::valueOf)
.subscribe(log::info); // "1", "2", "3"
flatMap操作符
用于处理一对多的转换场景。当转换函数返回的是一个新的Publisher(Mono或Flux)时,flatMap会将这些Publisher展平为单个数据流。这在需要异步操作(如调用其他服务)时特别有用:
java
Flux<Integer> flux = Flux.range(1, 100)
.flatMap(new Function<Integer, Flux<Integer>>() {
@Override
public Flux<Integer> apply(Integer integer) {
return Flux.just(integer)
.delayElements(Duration.ofSeconds(1))
.doOnNext(integer1 -> log.info("return value {}", integer1));
}
}, 10); // 可以同时并行执行10个Publisher
flux.subscribe(integer -> log.info("subscribe: {}", integer));
因此,效果就是每隔1秒,会有十个Flux执行完毕将数据交给订阅者
concatMap操作符
与flatMap功能相似,但严格保持元素顺序。它确保前一个元素的流完全处理完毕后再处理下一个元素,适用于需要严格顺序保证的场景:
java
Flux<Integer> flux = Flux.range(1, 100)
.concatMap(new Function<Integer, Flux<Integer>>() {
@Override
public Flux<Integer> apply(Integer integer) {
return Flux.just(integer)
.delayElements(Duration.ofSeconds(1))
.doOnNext(integer1 -> log.info("return value {}", integer1));
}
}, 10); // 向发布者request(10)个数据元素
flux.subscribe(integer -> log.info("subscribe: {}", integer));
因此,效果就是每隔1秒,会有一个Flux执行完毕将数据交给订阅者
switchMap操作符
在接收到新元素时会取消前一个元素的处理。这特别适合实现实时搜索功能,当用户连续输入时,只处理最新的搜索请求:
java
@Test
public void testSwitchMap() throws InterruptedException {
Flux.just(1, 2, 3)
.delayElements(Duration.ofSeconds(1))
.switchMap(new Function<Integer, Publisher<?>>() {
@Override
public Publisher<?> apply(Integer integer) {
return Flux.just(integer)
.doOnCancel(() -> log.info("cancel"))
.delayElements(Duration.ofSeconds(2));
}
})
.subscribe(o -> log.info("subscribe: {}", o));
TimeUnit.SECONDS.sleep(10);
}
因此,效果就是前两个元素都被取消了,最后一个元素能被订阅者拿到
2. 过滤类操作符(Filtering Operators)
filter操作符
根据条件筛选元素,只允许满足条件的元素通过:
java
Flux.range(1,10)
.filter(n -> n % 2 == 0)
.subscribe(System.out::println); // 2,4,6,8,10
take操作符
限制从流中获取的元素数量:
java
// 只取前三个元素
Flux.range(1,10)
.take(3)
.subscribe(System.out::println); //1,2,3
skip操作符
跳过指定数量的元素:
java
// 跳过前7个元素
Flux.range(1,10)
.skip(7)
.subscribe(System.out::println); // 8,9,10
distinct操作符
去除重复元素:
java
Flux.just(1, 1, 2, 3, 4, 5, 6, 3, 4)
.distinct()
.subscribe(System.out::println); // 1,2,3,4,5,6
3. 组合类操作符(Combination Operators)
merge操作符
将多个流合并为一个流,元素按实际发出时间交错出现:
java
Flux<String> flux1 = Flux.interval(Duration.ofMillis(100)).map(i -> "A" + i);
Flux<String> flux2 = Flux.interval(Duration.ofMillis(150)).map(i -> "B" + i);
Flux.merge(flux1, flux2).subscribe(System.out::println); // A0, B0, A1, A2, B1...
zip操作符
将多个流的元素按顺序一一配对组合:
java
Flux<String> names = Flux.just("Alice", "Bob", "Charlie");
Flux<Integer> ages = Flux.just(28, 32, 45);
Flux.zip(names, ages, (name, age) -> name + ":" + age)
.subscribe(System.out::println);
// Alice:28, Bob:32, Charlie:45
concat操作符
按顺序连接多个流,先完整消费前一个流,再处理下一个:
java
Flux<Integer> first = Flux.just(1, 2, 3);
Flux<Integer> second = Flux.just(4, 5, 6);
Flux.concat(first, second).subscribe(System.out::println); // 1,2,3,4,5,6
4. 错误处理类操作符(Error Handling Operators)
onErrorResume操作符
在发生错误时提供备用数据流:
java
Flux.just(1, 2, 3, 4, 5)
.map(i -> {
if (i == 3) {
throw new RuntimeException("error");
}
return i;
})
.onErrorResume(e -> Flux.just(6, 7, 8))
.subscribe(System.out::println); // 1,2,6,7,8
onErrorReturn操作符
在错误时返回默认值:
java
Flux.just(1, 2, 3, 4, 5)
.map(i -> {
if (i == 3) {
throw new RuntimeException("error");
}
return i;
})
.onErrorReturn(-1)
.subscribe(System.out::println); // 1,2,-1
retry操作符
在失败时自动重试,并且可以配置不同的策略进行重试(比如说立即重试、定时重试、指数退避重试):
重试若干次:
java
externalService.call()
.retry(3) // 最多重试3次
java
externalService.call()
.retryWhen(Retry.max(3)) // 最多重试3次
固定频率重试:
java
externalService.call()
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1))) // 最多重试3次,时间间隔1秒
无限次重试:
java
externalService.call()
.retryWhen(Retry.indefinitely()) // 无限次重试
指数退避重试:
java
externalService.call()
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(3))
.jitter(0.2))
当外部服务调用失败时,会进行最多 3 次重试,初始延迟 1 秒,之后每次重试间隔按指数增长(但最大不超过 3 秒),并添加 ±20% 的随机抖动以避免重试风暴
并且除此之外,还可以配置在出现指定异常的情况下,才进行重试:
java
externalService.call()
.retryWhen(Retry.max(3)
.filter(throwable -> throwable instanceof WebClientResponseException.InternalServerError))
5. 工具类操作符(Utility Operators)
doOnNext操作符
在不改变流的情况下执行副作用操作:
java
Flux.just("a", "b", "c")
.doOnNext(s -> System.out.println("Processing: " + s))
.subscribe();
log操作符
提供详细的流处理日志:
java
Flux.range(1, 3)
.log("range-logger")
.subscribe();
// 输出:onSubscribe, request(unbounded), onNext(1), onNext(2), onNext(3), onComplete
elapsed操作符
记录每个元素与前一个元素的时间间隔:
java
Flux.range(1, 10)
.delayElements(Duration.ofMillis(100))
.elapsed()
.subscribe(new Consumer<Tuple2<Long, Integer>>() {
@Override
public void accept(Tuple2<Long, Integer> objects) {
log.info("{}ms: {}", objects.get(0), objects.get(1));
}
});
// 输出:112ms: 1,108ms: 2,...
7. 条件类操作符(Conditional Operators)
takeUntil操作符
在条件满足时停止流:
java
Flux.range(1, 10)
.takeUntil(i -> i == 5) // 取到5(包含5)
.subscribe(System.out::println); //1,2,3,4,5
skipUntil操作符
在条件满足前跳过元素:
java
Flux.range(1, 10)
.skipUntil(i -> i > 5) // 跳过直到i>5(不包含触发条件的元素)
.subscribe(System.out::println); //6,7,8,9,10
8. 时间相关操作符(Time-based Operators)
delayElements操作符
延迟每个元素的发射:
java
Flux.just("A", "B", "C")
.delayElements(Duration.ofSeconds(1))
.subscribe();
/*
13:39:57.018 [parallel-1] INFO - A
13:39:58.021 [parallel-2] INFO - B
13:39:59.025 [parallel-3] INFO - C
*/
timeout操作符
设置元素发出的超时时间:
java
Flux.range(1, 5)
.concatMap(integer -> Flux.just(integer).delayElements(Duration.ofSeconds(integer)))
.timeout(Duration.ofSeconds(3))
.subscribe(System.out::println, Throwable::printStackTrace);
/*
1
2
java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 3000ms in 'concatMapNoPrefetch' (and no fallback has been configured)
*/
9. 聚合操作符(Aggregation Operators)
reduce操作符
通过二元函数对数据流进行累积计算,最终输出单个结果。
java
Mono<Integer> reduce = Flux.range(1, 5) // 1,2,3,4,5
.reduce(0, Integer::sum);
reduce.subscribe(sum -> System.out.println("总和: " + sum));
// 输出: 总和: 15
scan操作符
与 reduce
类似,但输出每次累积的中间结果。
java
Flux<Integer> scan = Flux.range(1, 5)
.scan(0, Integer::sum);
scan.subscribe(System.out::println);
// 输出:0 1 3 6 10 15
collect操作符
作用:将流元素收集到集合容器中(List, Set, Map等)。
java
// 收集到List
Flux.just("Apple", "Banana", "Orange")
.collectList()
.subscribe(list -> System.out.println("水果列表: " + list));
// 输出: 水果列表: [Apple, Banana, Orange]
// 自定义收集器
Flux.range(1, 10)
.collect(Collectors.partitioningBy(n -> n % 2 == 0))
.subscribe(map -> System.out.println("奇偶分组: " + map));
// 输出: 奇偶分组: {false=[1,3,5,7,9], true=[2,4,6,8,10]}
常用变体:
collectList()
→Mono<List<T>>
collectMap(keyMapper)
→Mono<Map<K, T>>
collectMultimap(keyMapper)
→Mono<Map<K, Collection<T>>>
collect(Collector)
count操作符
作用:统计流中元素数量。
java
Flux.range(1,5)
.count()
.subscribe(c -> System.out.println("元素数量: " + c));
// 输出: 元素数量: 5
all/any操作符
作用:检查流是否满足条件
java
// 所有元素都满足条件?
Flux.just(2, 4, 6, 8)
.all(n -> n % 2 == 0)
.subscribe(result -> System.out.println("全是偶数? " + result));
// 输出: 全是偶数? true
// 至少一个元素满足条件?
Flux.just(1, 3, 5, 6)
.any(n -> n % 2 == 0)
.subscribe(result -> System.out.println("包含偶数? " + result));
// 输出: 包含偶数? true
hasElements操作符
作用:检查流是否包含任何元素。
java
Flux.empty()
.hasElements()
.subscribe(has -> System.out.println("有元素? " + has));
// 输出: 有元素? false
groupBy操作符
作用:按key分组后对每组进行聚合处理。
java
Flux.just(
new Order("A", 100),
new Order("B", 200),
new Order("A", 150),
new Order("C", 50)
)
.groupBy(Order::getCustomerId) // 按客户ID分组
.flatMap(groupedFlux ->
groupedFlux
.map(Order::getAmount)
.reduce(0, Integer::sum)
.map(total -> new CustomerTotal(groupedFlux.key(), total))
)
.subscribe(System.out::println);
// 输出:
// CustomerTotal{customerId='A', total=250}
// CustomerTotal{customerId='B', total=200}
// CustomerTotal{customerId='C', total=50}
window操作符
将流划分为多个窗口(子流):支持按数量分窗口、按时间分窗口、动态窗口(基于另一个Publisher)等
java
Flux<Flux<Integer>> windowFLux = Flux.range(1, 10)
.window(3);// 每3个元素一个窗口
windowFLux.flatMap(window -> window.reduce(Integer::sum))
.subscribe(System.out::println);
// 输出:6, 15, 24, 10
java
Flux.interval(Duration.ofMillis(100)) // 每100ms生成一个数字: 0,1,2...
.window(Duration.ofSeconds(1)) // 每1秒分一个窗口
.flatMap(window ->
window.count() // 统计每秒内的元素数量
)
.subscribe(count ->
System.out.println("1秒内元素数: " + count)
);
java
// 边界控制器:每500ms发出信号
Flux<Long> boundary = Flux.interval(Duration.ofMillis(500));
Flux.interval(Duration.ofMillis(100)) // 数据流: 0,1,2,3...
.window(boundary) // 按boundary信号分窗口
.flatMap(window -> window.collectList())
.subscribe(System.out::println);
buffer操作符
和window操作符基本一致,也支持各种窗口划分方式。将元素收集到集合中:
java
Flux<List<Integer>> listFlux = Flux.range(1, 10)
.buffer(3);
listFlux.subscribe(System.out::println);
// 输出:[1,2,3], [4,5,6], [7,8,9], [10]
5. 调度器(Schedulers)和线程模型
在 Reactor 中,调度器(Schedulers) 和 线程模型 是非常重要的部分,它们决定了数据流的执行线程和调度策略。下面从几个方面为你介绍:
5.1 调度器
Reactor 提供了几种常见的调度器(Scheduler
),用于控制操作符执行的线程。
1. Schedulers.immediate()
- 执行策略:在当前调用线程上立即执行任务。
- 使用场景:调试或不需要线程切换的简单场景。
- 线程开销:没有额外线程开销。
2. Schedulers.single()
- 执行策略:使用一个单独的线程串行执行任务。
- 线程数:只有一个线程。
- 使用场景:需要顺序执行、避免并发的任务。
4. Schedulers.boundedElastic()
- 执行策略 :类似
elastic()
,但线程数量有上限(默认最大线程数:CPU 核心数 × 10)。 - 使用场景:IO 阻塞任务,数据库、网络、文件等操作。
5. Schedulers.parallel()
- 执行策略:使用固定数量的线程池(默认与 CPU 核心数相等)。
- 使用场景:计算密集型任务,适合并行处理数据。
6. Schedulers.fromExecutor(Executor)
/ Schedulers.fromExecutorService(...)
- 执行策略:自定义线程池。
- 使用场景:适用于需要对线程进行完全控制的情况。
5.2 线程模型
在 Reactor 中,默认所有操作都在当前线程(调用链所在线程)中执行,除非显式指定调度器(Scheduler)。
1. subscribeOn
- 它影响的是源(Publisher)创建与数据发射的线程。
- 只影响调用链中的最上游部分(只生效一次)。
java
Flux<Integer> flux = Flux.range(1, 5)
.map(i -> {
System.out.println("map1 thread: " + Thread.currentThread().getName());
return i;
})
.subscribeOn(Schedulers.boundedElastic())
.map(i -> {
System.out.println("map2 thread: " + Thread.currentThread().getName());
return i;
});
flux.subscribe();
输出中 map1
和 map2
都在 boundedElastic
线程池中执行。
2. publishOn
------ 切换后续操作的执行线程
- 它影响的是
publishOn
之后的操作符所在的线程。 - 可以多次使用,在链中多次切换线程。
java
Flux.range(1, 5)
.map(i -> {
System.out.println("before publishOn: " + Thread.currentThread().getName());
return i;
})
.publishOn(Schedulers.parallel())
.map(i -> {
System.out.println("after publishOn: " + Thread.currentThread().getName());
return i;
})
.subscribe();
before publishOn
在主线程,after publishOn
在 parallel 线程池中。
5.3 并行执行
上述 publishOn
和 subscribeOn
可以指定某一段操作在指定线程池中执行,但它们本质上是串行执行 :一个元素一个元素地顺序处理。在需要对大量元素进行真正并行处理 (多线程同时处理多个元素)时,应使用 parallel()
+ runOn(...)
提供的并发能力。
parallel()
+ runOn(...)
java
Flux.range(1, 10)
.parallel(4) // 拆成 4 条轨道
.runOn(Schedulers.parallel()) // 每条轨道运行在 parallel 调度器的不同线程上
.map(i -> {
System.out.println("Thread: " + Thread.currentThread().getName() + ", value: " + i);
return i;
})
.sequential() // 合并为单一 Flux(可选)
.subscribe();
parallel(n)
:将一个 Flux 拆分成n
个并行轨道(子流),默认数量为 CPU 核心数。runOn(Scheduler)
:将这些轨道的操作符执行绑定到指定的调度器线程池。sequential()
:用于并行处理完成后再合并回一个普通的 Flux(不是必须的,但常见)。
flatMap
也可以通过flatMap将每个元素转为异步执行,之前已介绍过,在此不再赘述。
6. 背压(Backpressure)
背压是一种调节数据生产速率的机制,旨在防止下游消费者因处理不过来而导致**内存溢出(OOM)**或系统性能下降。在响应式流中,背压是保障系统稳定性和弹性的重要手段。
6.1 推模式 vs 拉模式
在响应式编程中,数据流的传递通常有两种方式:
- 推模式(Push):生产者主动将数据推送给消费者。这种方式在消费者处理能力不足时,可能造成内存堆积或应用崩溃。
- 拉模式(Pull):消费者根据自己的能力请求数据。背压机制本质上就是让"推模式"具备"拉模式"的能力,由消费者控制节奏。
因此,对于拉模式,是自带背压的;对于推模式,需要通过设置一些背压策略避免出现OOM的情况。
属于推模式的几个方法分别为:Flux.interval()、Flux.create()、Flux.push()
值得说明的是,如果发布者和订阅者使用同一个线程,推模式是天然背压的。因为发布者需要等待订阅者处理完之后,才能继续发布下一个数据。
6.2 背压策略
Reactor 提供了多种背压策略,用于在数据下游处理不过来时,决定如何处理溢出的数据。常见策略如下:
策略名称 | 说明 |
---|---|
IGNORE |
忽略背压信号,可能导致 OOM |
ERROR |
抛出异常(IllegalStateException ),终止数据流 |
DROP |
丢弃溢出的数据 |
LATEST |
保留最新数据,丢弃旧数据 |
BUFFER |
缓存所有数据,直到消费者准备好。可设置缓冲区大小和溢出处理策略 |
这些策略通常通过 onBackpressureXXX
系列方法配置,如:
java
Flux.create(sink -> {
// 产生大量数据
}).onBackpressureBuffer(1000, BufferOverflowStrategy.DROP_OLDEST);
6.3 背压操作符
onBackpressureError
java
Flux<Integer> flux = Flux.create(new Consumer<FluxSink<Integer>>() {
@SneakyThrows
@Override
public void accept(FluxSink<Integer> sink) {
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 10; j++) {
log.info("send {}", j);
sink.next(j);
}
TimeUnit.SECONDS.sleep(2);
}
sink.complete();
}
})
.onBackpressureError()
.publishOn(Schedulers.single(), 1);
flux.subscribe(new Consumer<Integer>() {
@SneakyThrows
@Override
public void accept(Integer num) {
log.info("receive {}", num);
TimeUnit.SECONDS.sleep(1);
}
});
onBackpressureDrop
java
Flux<Integer> flux = Flux.create(new Consumer<FluxSink<Integer>>() {
@SneakyThrows
@Override
public void accept(FluxSink<Integer> sink) {
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 5; j++) {
log.info("send {}", j);
sink.next(j);
}
TimeUnit.SECONDS.sleep(2);
}
sink.complete();
}
})
.onBackpressureDrop(integer -> log.warn("drop {}", integer))
.publishOn(Schedulers.single(), 1);
flux.subscribe(new Consumer<Integer>() {
@SneakyThrows
@Override
public void accept(Integer num) {
log.info("receive {}", num);
TimeUnit.SECONDS.sleep(1);
}
});
onBackpressureLatest
java
Flux<Integer> flux = Flux.create(new Consumer<FluxSink<Integer>>() {
@SneakyThrows
@Override
public void accept(FluxSink<Integer> sink) {
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 5; j++) {
log.info("send {}", j);
sink.next(j);
}
TimeUnit.SECONDS.sleep(2);
}
sink.complete();
}
})
.onBackpressureLatest()
.publishOn(Schedulers.single(), 1);
flux.subscribe(new Consumer<Integer>() {
@SneakyThrows
@Override
public void accept(Integer num) {
log.info("receive {}", num);
TimeUnit.SECONDS.sleep(1);
}
});
onBackpressureBuffer
java
Flux<Integer> flux = Flux.create(new Consumer<FluxSink<Integer>>() {
@SneakyThrows
@Override
public void accept(FluxSink<Integer> sink) {
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 5; j++) {
log.info("send {}", j);
sink.next(j);
}
TimeUnit.SECONDS.sleep(1);
}
sink.complete();
}
})
.onBackpressureBuffer(3, integer -> log.warn("drop {}", integer), BufferOverflowStrategy.DROP_OLDEST)
.publishOn(Schedulers.single(), 1);
flux.subscribe(new Consumer<Integer>() {
@SneakyThrows
@Override
public void accept(Integer num) {
log.info("receive {}", num);
TimeUnit.SECONDS.sleep(1);
}
});
6.4 哪些场景需要特别关注背压
- 自定义发布者:如使用
Flux.create()
、Flux.push()
等手动发射数据的方法。 - 高速生产、慢速消费:如读取高速数据源(消息队列、数据库流)时未加以限制。
- 多线程环境:生产者在独立线程中快速产生数据,容易压垮单线程消费者。
- 网络 IO、文件流处理等慢速资源场景。
相比之下,一些操作符(如 Flux.range()
、Flux.just()
)天然是背压感知的,无需额外干预。
7. 冷流 vs 热流
- 冷流(Cold Sequence):每个订阅者独立消费完整数据(如HTTP请求)。
java
Flux<String> cold = Flux.fromStream(() -> Stream.of("A", "B"));
cold.subscribe(); // 触发数据生成
cold.subscribe(); // 再次触发新数据流
- 热流(Hot Sequence):多个订阅者共享实时数据(如消息队列)。
java
ConnectableFlux<String> hot = Flux.just("X", "Y", "Z").publish();
hot.connect(); // 手动启动发布
hot.subscribe(System.out::println); // 所有订阅者接收相同数据
9. 上下文(Context)
- 作用:在响应式链中传递状态(类似ThreadLocal)。
- 示例:
java
Mono<String> result = Mono.deferContextual(ctx ->
Mono.just("User: " + ctx.get("user"))
).contextWrite(ctx -> ctx.put("user", "Alice"));
总结
本文介绍了响应式编程、Reactive Streams规范、Project Reactor的核心概念及使用。后续将基于本文的介绍的内容,介绍Spring WebClient。