跳到主要内容

以编程方式创建序列

Doubao 中英对照 Programmatically creating a sequence

目录

本节介绍通过以编程方式定义 Flux 或 Mono 相关的事件(onNext、onError 和 onComplete)来创建它们。所有这些方法的共同点是,它们都暴露一个 API 来触发事件,我们称之为 sink。实际上有几种 sink 变体,我们稍后就会介绍。

1. 同步 generate​

以编程方式创建 Flux 最简单的形式是通过 generate 方法,它接受一个生成器函数。

这适用于同步且逐个的发射,也就是说 sink 是一个 SynchronousSink,其 next() 方法在每次回调调用中至多只能调用一次。之后您还可以额外调用 error(Throwable) 或 complete(),但这是可选的。

最有用的变体大概是那个还让您维护一个状态的版本,您可以在使用 sink 时引用该状态来决定下一步发射什么。此时生成器函数变为 BiFunction<S, SynchronousSink<T>, S>,其中 <S> 是状态对象的类型。您必须为初始状态提供一个 Supplier<S>,而您的生成器函数现在每一轮都返回一个新状态。

例如,您可以用一个 int 作为状态:

Flux<String> flux = Flux.generate(
() -> 0, 1
(state, sink) -> {
sink.next("3 x " + state + " = " + 3*state); 2
if (state == 10) sink.complete(); 3
return state + 1; 4
});
  • 我们提供初始状态值 0。

  • 我们用状态来选择发射什么(3 的乘法表中的一行)。

  • 我们还用它来选择何时停止。

  • 我们返回一个新状态,供下一次调用使用(除非序列在本次调用中已终止)。

前述代码生成 3 的乘法表,得到如下序列:

3 x 0 = 0
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
3 x 4 = 12
3 x 5 = 15
3 x 6 = 18
3 x 7 = 21
3 x 8 = 24
3 x 9 = 27
3 x 10 = 30

您也可以使用一个可变的 <S>。例如,上面的例子可以改写成用单个 AtomicLong 作为状态,并在每一轮修改它:

Flux<String> flux = Flux.generate(
AtomicLong::new, 1
(state, sink) -> {
long i = state.getAndIncrement(); 2
sink.next("3 x " + i + " = " + 3*i);
if (i == 10) sink.complete();
return state; 3
});
  • 这一次,我们生成一个可变对象作为状态。

  • 我们在这里修改状态。

  • 我们返回同一个实例作为新状态。

提示

如果您的状态对象需要清理一些资源,请使用 generate(Supplier<S>, BiFunction, Consumer<S>) 变体来清理最后一个状态实例。

以下示例使用包含 Consumer 的 generate 方法:

Flux<String> flux = Flux.generate(
AtomicLong::new,
(state, sink) -> { 1
long i = state.getAndIncrement(); 2
sink.next("3 x " + i + " = " + 3*i);
if (i == 10) sink.complete();
return state; 3
}, (state) -> System.out.println("state: " + state)); 4
  • 同样,我们生成一个可变对象作为状态。

  • 我们在这里修改状态。

  • 我们返回同一个实例作为新状态。

  • 我们在这个 Consumer lambda 的输出中看到最后一个状态值(11)。

当状态包含一个数据库连接或其他需要在过程结束时处理的资源时,该 Consumer lambda 可以关闭连接,或以其他方式处理任何应在过程结束时完成的任务。

2. 异步与多线程:create​

create 是一种更高级的以编程方式创建 Flux 的形式,它适合每一轮多次发射,甚至可以来自多个线程。

它暴露一个 FluxSink,带有其 next、error 和 complete 方法。与 generate 不同,它没有基于状态的变体。另一方面,它可以在回调中触发多线程事件。

提示

create 在把已有 API 与响应式世界桥接时非常有用——例如基于监听器的异步 API。

注意

create 不会并行化您的代码,也不会使代码变为异步,尽管它可以与异步 API 配合使用。如果您在 create lambda 内部阻塞,就会让自己面临死锁及类似副作用的风险。即使使用 subscribeOn,也要注意:一个长时间阻塞的 create lambda(例如调用 sink.next(t) 的无限循环)可能会锁死整条管线:由于循环耗尽了本应运行这些请求的同一线程,请求将永远无法被执行。请使用 subscribeOn(Scheduler, false) 变体:requestOnSeparateThread = false 会用 Scheduler 线程来运行 create,同时仍通过在原始线程中执行 request 来让数据流动。

设想您使用一个基于监听器的 API。它按数据块处理数据,并有两个事件:(1) 一个数据块已就绪;(2) 处理完成(终止事件),如 MyEventListener 接口所表示的那样:

interface MyEventListener<T> {
void onDataChunk(List<T> chunk);
void processComplete();
}

您可以使用 create 将其桥接为一个 Flux<T>:

Flux<String> bridge = Flux.create(sink -> {
myEventProcessor.register( 4
new MyEventListener<String>() { 1

public void onDataChunk(List<String> chunk) {
for(String s : chunk) {
sink.next(s); 2
}
}

public void processComplete() {
sink.complete(); 3
}
});
});
  • 桥接到 MyEventListener API

  • 一个数据块中的每个元素都成为 Flux 中的一个元素。

  • processComplete 事件被转换为 onComplete。

  • 所有这些都在 myEventProcessor 执行时异步完成。

此外,由于 create 可以桥接异步 API 并管理背压,您还可以通过指定一个 OverflowStrategy 来细化背压方面的行为:

  • IGNORE,完全忽略下游的背压请求。当下游队列满时,这可能产生 IllegalStateException。

  • ERROR,当下游跟不上时发出一个 IllegalStateException 信号。

  • DROP,当下游尚未准备好接收时丢弃传入信号。

  • LATEST,让下游只获取上游最新的信号。

  • BUFFER(默认),当下游跟不上时缓冲所有信号。(这会进行无界缓冲,并可能导致 OutOfMemoryError)。

备注

Mono 也有一个 create 生成器。Mono 的 create 所用的 MonoSink 不允许多次发射。它会丢弃第一个信号之后的所有信号。

3. 异步但单线程:push​

push 是 generate 与 create 之间的折中,适合处理来自单个生产者的事件。它与 create 相似之处在于:它也可以是异步的,并且可以使用 create 支持的任何一种溢出策略来管理背压。然而,一次只能有一个生产线程调用 next、complete 或 error。

Flux<String> bridge = Flux.push(sink -> {
myEventProcessor.register(
new SingleThreadEventListener<String>() { 1

public void onDataChunk(List<String> chunk) {
for(String s : chunk) {
sink.next(s); 2
}
}

public void processComplete() {
sink.complete(); 3
}

public void processError(Throwable e) {
sink.error(e); 4
}
});
});
  • 桥接到 SingleThreadEventListener API。

  • 事件通过单个监听器线程用 next 推入 sink。

  • complete 事件也由同一个监听器线程产生。

  • error 事件同样由同一个监听器线程产生。

3.1. 混合 push/pull 模型​

大多数 Reactor 操作符(如 create)遵循一种混合的**推/拉(push/pull)**模型。我们的意思是:尽管大部分处理是异步的(暗示一种推的方式),其中仍有一个小小的拉的成分:请求(request)。

消费者从源拉取数据,意思是它在首次被请求之前不会发射任何东西。源则在数据可用时随时向消费者推送数据,但不会超出其请求的数量。

请注意,push() 和 create() 都允许设置一个 onRequest 处理器,以便管理请求量,并确保只有在有待处理请求时才通过 sink 推送数据。

Flux<String> bridge = Flux.create(sink -> {
myMessageProcessor.register(
new MyMessageListener<String>() {

public void onMessage(List<String> messages) {
for(String s : messages) {
sink.next(s); 3
}
}
});
sink.onRequest(n -> {
List<String> messages = myMessageProcessor.getHistory(n); 1
for(String s : messages) {
sink.next(s); 2
}
});
});
  • 在收到请求时轮询消息。

  • 如果消息立即可用,就把它们推送到 sink。

  • 之后异步到达的其余消息也会被投递。

3.2. 在 push() 或 create() 之后清理​

两个回调 onDispose 和 onCancel 用于在取消或终止时执行清理。onDispose 可用于在 Flux 完成、出错或被取消时执行清理。onCancel 可用于在 onDispose 清理之前执行任何专门针对取消的操作。

Flux<String> bridge = Flux.create(sink -> {
sink.onRequest(n -> channel.poll(n))
.onCancel(() -> channel.cancel()) 1
.onDispose(() -> channel.close()) 2
});
  • onCancel 最先被调用,仅针对取消信号。

  • onDispose 针对完成、错误或取消信号被调用。

4. handle​

handle 方法略有不同:它是一个实例方法,意味着它被链接到一个已有源之上(与常见操作符一样)。它在 Mono 和 Flux 中都存在。

它与 generate 相近,意思是它使用一个 SynchronousSink,并且只允许逐个发射。然而,handle 可以用来从每个源元素派生出一个任意值,甚至可能跳过某些元素。这样一来,它可以充当 map 与 filter 的组合。handle 的签名如下:

Flux<R> handle(BiConsumer<T, SynchronousSink<R>>);

让我们看一个例子。响应式流规范不允许序列中出现 null 值。如果您想执行一个 map,但又想把一个已有的方法用作 map 函数,而该方法有时会返回 null,该怎么办?

例如,下面这个方法可以安全地应用于一个整数源:

public String alphabet(int letterNumber) {
if (letterNumber < 1 || letterNumber > 26) {
return null;
}
int letterIndexAscii = 'A' + letterNumber - 1;
return "" + (char) letterIndexAscii;
}

然后我们可以用 handle 来移除任何 null:

Flux<String> alphabet = Flux.just(-1, 30, 13, 9, 20)
.handle((i, sink) -> {
String letter = alphabet(i); 1
if (letter != null) 2
sink.next(letter); 3
});

alphabet.subscribe(System.out::println);
  • 映射为字母。

  • 如果这个“map 函数”返回 null……

  • 通过不调用 sink.next 将其过滤掉。

这会打印出:

M
I
T