About the Documentation
本节简要概述了Reactor参考文档。不需要一行一行地阅读本指南,每一节都是独立的,尽管他们经常互相引用。
Getting Started
Introducing Reactor
Reactor是JVM上的完全非阻塞的响应式编程框架,支持有效的需求管理(通过背压的方式)。它直接与Java 8的函数式API集成,特别是CompletableFuture、Stream和Duration。它提供了可组合的异步序列(asynchronous sequence)API:Flux(用于[N]个元素)和Mono(用于 [0|1]个元素)。广泛实现了Reactive Streams。
使用reactor-netty,Reactor也支持非阻塞的进程间通信。配合微服务架构,Reactor Netty为HTTP(包括Websockets)、TCP和UDP提供了背压的网络引擎。完全支持响应式的Encoding和Decoding。
Prerequisites
Reactor Core最低支持Java 8。
它依赖org.reactivestreams:reactive-streams:1.0.2。
Android支持:
- Reactor 3没有正式支持Android(RxJava 2支持)
- 在Android SDK 26以上可以工作得很好
Introduction to Reactive Programming
Reactor是响应式编程范式的一个实现,可以概括为:
响应式编程是关于数据流和变化传播(the propagation of change)的异步编程范式。这意味着可以通过采用的编程语言轻松地表达静态(比如数组)或者动态(比如事件发送器)的数据流。
见Reactive_programming
Microsoft最先在.NET ecosystem增加了Reactive Extensions(Rx)库。然后,RxJava在JVM上实现了响应式编程。后来,通过Reactive Streams实现了Java上的标准化-Flow类定义的接口集和交互规则集成到了Java 9内。
响应式编程范式通常在面向对象的语言中出现,是Observer设计模式的扩展。比较一下响应式流和Iterator设计模式,一个主要不同点是,Iterator是pull-based的,而响应式流是push-based。
使用一个迭代器是一种命令式的编程模式,尽管访问值的方法完全是Iterable的责任。开发人员可以选择何时访问序列中的next()项。
而响应式流,是发布-订阅模式的。发布者提醒订阅者来了新值,这种push是响应式的关键。除了推送值,还以明确定义的方式,涵盖错误处理和完成。通过调用onNext,发布者将新值推送给订阅者。也可以通过调用onError发送一个错误信号,或者通过onComplete完成。错误和完成都会终止序列,可以概括为:
onNext x 0..N [onError | onComplete]
这种方法非常灵活,它支持没有值、一个值和n个值(包括无限的值序列,比如时钟的连续滴答)。
但是,为什么需要一个异步的响应式库呢?
Blocking Can Be Wasteful
现代程序可以覆盖大量并发用户,虽然硬件性能在持续提升,可软件性能还是一个关键问题。
有两种办法提高程序性能:
- parallelize:采用更多线程和更多硬件资源
- seek more efficiency:寻求当前资源的更高效率
一般来说,Java程序都是阻塞式的。容易达到性能瓶颈,就需要更多线程,运行类似的阻塞代码。这样做,很快就会出现争用和并发问题。
更糟糕的是,阻塞浪费资源。如果仔细观察,一旦程序涉及延迟(特别是I/O,比如数据库操作和网络调用),资源就会被浪费-因为线程处于空闲或者等待数据的状态。
Asynchronicity to the Rescue?
也可以编写异步的、非阻塞的代码,使用相同的底层资源切换到其他活动任务,等异步处理执行完成再切换回来。
在JVM内,怎么写异步代码呢?
- Callbacks:异步方法没有返回值,但是需要额外的回调参数(lambda或者匿名类),结果有效时就调用该参数。比如Swing中的EventListener
- Futures:异步方法立即返回Future。异步过程计算T的值,Future对象包装了对值的访问。该值不会立即有效,值有效以后才可以拉取(poll)。比如运行Callable任务的ExecutorService使用Future对象
这两种方法都有局限性。
Callbacks很难组合到一起。阅读和维护起来也很困难。
From Imperative to Reactive Programming
诸如Reactor这样的响应式库不但解决上述缺点,还关注其他方面:
- 组合性和可读性
- 数据作为流(flow),有丰富的operators
- subscribe前什么都没发生
- Backpressure,消费者向生产者发送明确的信号,表明生产得太快了
- 与并发无关的高级抽象
Composability and Readability
composability是指有能力编排多个异步任务,前一个任务的结果就是后续任务的输入,或者使用fork-join执行几个任务,也可以重用任务,把它作为更高级系统的组件。
编排任务的能力与代码的可读性和可维护性紧密相关。随着异步过程层级的数量和复杂性的增加,编写和阅读代码都变得越来越难。正如我们所看到的,回调很简单,但它的一个主要缺点是,对于复杂的过程,你需要在回调中执行回调,他们嵌套在一起(Callback Hell)。
Reactor提供了丰富的组合选项,代码反应了抽象过程的组织,全都位于同一级(嵌套最小化)。
The Assembly Line Analogy
你可以想象为,响应式程序里的数据在装配线上移动。Reactor既是传送带,又是工作站。原材料从源注入(Publisher),最终的成品推送给消费者(Subscriber)。
原材料经历各种转换和其他中间步骤,或者是大型装配线上的一部分和其他部件聚合到一起。如果在某一点出现了故障或者堵塞(也许花费了太长时间),受影响的工作站可以向上游信号以限制原材料的流动。
Operators
Reactor中,operators就是装配线类比中的工作站。每个operator都会向Publisher添加行为,并把前一步的Publisher包装成新的实例。整个链就这样形成了,数据从第一个Publisher沿着链向后移动,由每个link转发。最终,Subscriber完成了该过程。记住,在Subscriber订阅Publisher前,什么都没发生。
Reactive Streams规范根本没有指定任何operators,Reactor的最佳附加值就是添加了丰富的operators。他们涉及很多方面,从简单的转换、过滤到复杂的编排和错误处理。
Nothing Happens Until You subscribe()
Reactor中,当你写一个Publisher链,默认情况下,数据不会启动。你要增加一个异步处理的抽象描述(帮助重用和组合)。
通过订阅,把Publisher绑定到Subscriber,从而出发整个链中的数据流。在内部,Subscriber发送一个request信号,向上游传递,直到Publisher。
Backpressure
backpressure也是通过向上游传递信号实现的,还是用装配线做类比,如果工作站处理得比上游慢就发送一个反馈信号。
Reactive Streams规范的定义非常接近类比:subscriber可以在unbounded模式工作,让源以最快的速度推送数据;或者使用request机制,向源发信号,它现在可以处理最多n条数据。
中间operators也可以改变request。比如buffer operator,可以把数据分组。还有些operators实现了prefetching策略,这就避免了request(1)往返,如果在请求之前就生成元素不太昂贵,这样处理是划算的。
这样,把push模式变成了push-pull,如果有数据,下游可以从上游pull数据。如果没有数据,就等数据准备好以后push给下游。
Hot vs Cold
有两大类反应式序列hot和cold。主要区别是响应式流如何应答订阅:
- Cold:为每个Subscriber都生成新的序列,包括数据源。比如,如果源包装了一个HTTP调用,就为每个订阅生成一个新的HTTP请求
- Hot:对于每个Subscriber,不会重新开始。相反,迟到的订阅者只能接收到订阅之后发射的数据。注意,一些hot响应式流可以缓存或者重放历史(甚至全部历史)。hot的序列甚至可以在没有订阅者时也发射数据
Reactor Core Features
Reactor提供了可组合的响应式类型,他们(Flux和Mono)实现了Publisher,还提供了丰富的operators。Flux代表0…N个元素,Mono代表(0…1)。
比如,HTTP请求只有一个响应,所以应该不会做count运算。所以,使用Mono代表一次HTTP请求的结果会更好。
改变最大基数的Operators会切换相关类型。比如,Flux才有count运算,但是它返回Mono。
Flux, an Asynchronous Sequence of 0-N Items
Flux是一个标准的Publisher,可以由一个completion信号或者error终止。三种类型的信号转换为对下游Subscriber的onNext、onComplete或者onError方法的调用。
所有的事件,包括terminating,都是可选的。如果没有onNext事件但是有onComplete代表一个empty有限序列;而删除了onComplete,就变成一个无限的空序列(没什么用,除非要测试cancellation)。
无限序列不一定是空的,比如,Flux.interval(Duration)生产的Flux就是无限的,根据时钟发出滴答声。
Mono, an Asynchronous 0-1 Result
Mono 是一个专用的Publisher,最多发送一条数据,然后可以由onComplete或者onError信号终止。
只包含Flux的operators的子集,一些operators可以切换到Flux。
比如,Mono#concatWith(Publisher)返回一个Flux,Mono#then(Mono) 返回另一个Mono。
Mono可以代表一个无值的异步处理,它仅有completion概念(类似Runnable)。想增加这样一个,请使用Mono。
Simple Ways to Create a Flux or Mono and Subscribe to It
可以使用工厂方法开始使用Flux和Mono。
比如,要增加一个String序列,可以枚举他们,可以放进集合,然后增加Flux:
Flux<String> seq1 = Flux.just("foo", "bar", "foobar");
List<String> iterable = Arrays.asList("foo", "bar", "foobar");
Flux<String> seq2 = Flux.fromIterable(iterable);
其他例子:
//没有值,也可以使用泛型
Mono<String> noData = Mono.empty();
Mono<String> data = Mono.just("foo");
//第一个参数是范围的开始,第二个参数是数量
Flux<Integer> numbersFromFiveToSeven = Flux.range(5, 3);
订阅的时候,Flux和Mono支持Java 8 lambdas。可以选择.subscribe()的变种,将lambdas用于不同的回调组合:
//订阅,触发一个序列
subscribe();
//使用每个产生的值做点啥
subscribe(Consumer<? super T> consumer);
//处理值,也响应错误
subscribe(Consumer<? super T> consumer,
Consumer<? super Throwable> errorConsumer);
//处理值和错误。当序列successfully完成时,做点啥
subscribe(Consumer<? super T> consumer,
Consumer<? super Throwable> errorConsumer,
Runnable completeConsumer);
//处理值、错误和successful完成。再使用Subscription做点什么
subscribe(Consumer<? super T> consumer,
Consumer<? super Throwable> errorConsumer,
Runnable completeConsumer,
Consumer<? super Subscription> subscriptionConsumer);
这些变种返回对subscription的引用,当你不再需要数据时,可以cancel该subscription。通过cancellation,源会停止生产值,清除它增加的资源。这种cancel和清理由Disposable接口代表。
subscribe Method Examples
本节包含subscribe方法的每个签名的最小示例。
先是无参方法:
//生产三个值
Flux<Integer> ints = Flux.range(1, 3);
//订阅
ints.subscribe();
前面的代码没有产生可见的输出,但是它能工作。该Flux产生了三个值。如果我们提供一个lambda,可以让值可视:
Flux<Integer> ints = Flux.range(1, 3);
//订阅,打印值
ints.subscribe(i -> System.out.println(i));
输出是:
1
2
3
为了演示下一个方法签名,我们故意引入错误:
//产生四个值
Flux<Integer> ints = Flux.range(1, 4)
//需要map处理不同的值
.map(i -> {
//对于大多数值,返回该值
if (i <= 3) return i;
//强制产生错误
throw new RuntimeException("Got to 4");
});
//订阅包含了错误处理
ints.subscribe(i -> System.out.println(i),
error -> System.err.println("Error: " + error));
我们有两个lambda表达式,一个为期望的内容,一个为错误。输出是:
1
2
3
Error: java.lang.RuntimeException: Got to 4
subscribe的下一个签名包含completion事件的处理:
Flux<Integer> ints = Flux.range(1, 4);
ints.subscribe(i -> System.out.println(i),
error -> System.err.println("Error " + error),
() -> System.out.println("Done"));
error信号和completion信号都是终止事件,彼此排斥(不可能同时得到)。要使completion消费者工作,就不能触发错误。
completion回调没有输入,由一对空括号表示:它匹配Runnable接口的run方法。前面代码的输出是:
1
2
3
4
Done
subscribe方法的最后一个签名包含一个Consumer。可以使用Subscription做些事情:执行request(long),或者cancel()。否则Flux会被挂起:
Flux<Integer> ints = Flux.range(1, 4);
ints.subscribe(i -> System.out.println(i),
error -> System.err.println("Error " + error),
() -> System.out.println("Done"),
//源最多发射10个元素,实际上,只发射了四个
sub -> sub.request(10));
Cancelling a subscribe() with its Disposable
subscribe()的这些变种都有一个Disposable返回类型。在这里,Disposable代表订阅能被取消(通过调用dispose() 方法)。
对于Flux和Mono,cancellation是一个信号,源会停止产生数据。但是,不能保证是立竿见影的。有些源生产数据的速度太快,在收到cancel指令前可能已经完成了。
Disposables类中有一些实用的工具。Disposables.swap()增加一个Disposable包装器,允许你原子地cancel或者替换一个具体的Disposable。比如在一个UI场景中,每当用户按下一个button,你就可以cancel一个请求,替换成一个新的。
Alternative to lambdas: BaseSubscriber
也可以扩展BaseSubscriber,实现订阅功能。
比如这样调用一个SampleSubscriber:
SampleSubscriber<Integer> ss = new SampleSubscriber<Integer>();
Flux<Integer> ints = Flux.range(1, 4);
ints.subscribe(i -> System.out.println(i),
error -> System.err.println("Error " + error),
() -> {System.out.println("Done");},
s -> s.request(10));
ints.subscribe(ss);
SampleSubscriber是这样实现的:
public class SampleSubscriber<T> extends BaseSubscriber<T> {
public void hookOnSubscribe(Subscription subscription) {
System.out.println("Subscribed");
request(1);
}
public void hookOnNext(T value) {
System.out.println(value);
request(1);
}
}
SampleSubscriber类扩展了BaseSubscriber,这是自定义Subscribers时,Reactor推荐扩展的抽象类。它提供了可以被覆盖的钩子,以调整subscriber的行为。默认会触发一个unbounded的请求,表现得很像subscribe()。如果你想自定义请求总量,扩展BaseSubscriber就很好。
要自定义请求量,最低限度要实现hookOnSubscribe(Subscription subscription) 和hookOnNext(T value)。前面的例子,hookOnSubscribe打印到标准输出,然后发送第一次请求。hookOnNext打印值,执行附加的请求,每次一条。
上面SampleSubscriber类的输出是
Subscribed
1
2
3
4
BaseSubscriber类还包含requestUnbounded()方法,可以切换到unbounded模式(相当于request(Long.MAX_VALUE))。此外还有cancel()方法。
它还有这些钩子:hookOnComplete、hookOnError、hookOnCancel和hookFinally(总是在序列终止时被调用,终止类型见SignalType参数)。
On Backpressure, and ways to reshape requests
Reactor实现背压的时候,消费者向上游operator发送request。当前请求的和有时候被称为当前demand或者是pending request。上限是Long.MAX_VALUE,代表无限的请求(没有背压)。
第一个请求来自最终的subscriber。在订阅的时候,最直接的办法是触发无限的请求:
- subscribe()和大多数变种
- block()、blockFirst()和blockLast()
- 使用toIterable()/toStream()迭代
自定义原始请求的最简单的办法是覆盖BaseSubscriber的hookOnSubscribe方法:
Flux.range(1, 10)
.doOnRequest(r -> System.out.println("request of " + r))
.subscribe(new BaseSubscriber<Integer>() {
@Override
public void hookOnSubscribe(Subscription subscription) {
request(1);
}
@Override
public void hookOnNext(Integer integer) {
System.out.println("Cancelling after having received " + integer);
cancel();
}
});
输出是
request of 1
Cancelling after having received 1
操纵request的时候,你必须小心地产生足够的序列要求,否则你的Flux会被卡住。所以,BaseSubscriber的hookOnSubscribe默认是无限的request。如果你覆盖这个钩子,最少要调用一次request。
Operators changing the demand from downstream
要记住,subscribe级别的要求,能被上游的每个operator重新整形。比如buffer(N) operator:如果它收到request(2),它解释成需要two full buffers。因为buffers认为有N个元素就是满的,该buffer operator使得request成了2 x N。
Prefetch是在内部序列调整初始request的方法,一般来说,默认是32。
这些operators一般也实现了补充(replenishing)优化:一旦operator看到25%的prefetch已经完成,就再向上游请求25%。这是一种启发式的优化,让这些operators主动预测即将到来的请求。
也可以直接调整request:limitRate和limitRequest。
limitRate(N)拆分下游请求,让他们以较小的批量传播到上游。比如一个100的request,通过limitRate(10)会导致最多10次10个的requests,传播到上游。limitRate实现了上面讨论的补充优化。
该operator有个变种,可以调整补充总量:limitRate(highTide, lowTide),lowTide为0就是严格的highTide批量的请求,而没有补充优化。
limitRequest(N)定义下游request的最大总需求。如果单个request不会让总需求超过N,整个request就会传给上游。达到总量,limitRequest认为序列完成,向下游发射onComplete并cancel资源。