Reactor Core是Java8提供的实现响应式编程模型,它基于Reactive Stream规范之上(构建响应式应用的标准)。
从非响应式Java开发的背景来看,响应式开发需经过陡峭的学习曲线,相比于Java8 Stream API更具有挑战性,因为被误认为有相同的高级别抽象。本文尝试理清响应式编程范式,我们会从如何组合响应式代码构建应用开始,逐步打好基础为后续更高级主题应用做准备。
响应式流规范
在进入Reactor主题之前,首先讨论响应式流规范,它就是Reactor实现的,是Reactor Core库的基础功能。响应式流规范本质就是异步流程处理规范。也就是说,系统产生大量事件需要异步消费。如每秒有数千个股票更新流的金融应用,必须要快速响应这些更新请求。
响应式编程一个主要目标就是要解决反压问题。如果生产者发射事件速度超过消费者处理速度,最终消费者会因为过量事件而耗尽资源。反压意味着消费者为了防止过量事件,告诉生产者能够发送多少数据,这就是规范中陈述的内容。
maven 依赖
首先需要增加maven依赖:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.4.16</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
这里增加logback,是为了更好理解数据流而输出Reactor的处理日志。
生产数据流
为了使应用程序具有响应性,它必须能够做的第一件事是生成数据流。类似于我们之前给出的股票更新例子。没有这些数据就没有任何可响应需求,这就是为什么这是合乎逻辑的第一步。Reactive Core提供了两种数据类型产生数据流。
Flux
第一种类型是Flux,能够产生0~N个元素的数据流,示例代码如下:
Flux<Integer> just = Flux.just(1, 2, 3, 4);
上述代码产生了4个元素的静态数据流。
Mono
第二种方式是使用Mono类,能够产生0~1个元素,示例代码:
Mono<Integer> just = Mono.just(1);
上述代码行为似乎与上节Flux一致,只是限制元素数量不超过1。
为什么需要两种类型
在进一步实验之前,有必要强调一下为什么要使用这两种数据类型。首先应该了解的是Flux和Mono都为响应式流publisher接口的实现。这两个类都遵照规范,我们可以在下面代码中使用该接口:
Publisher<String> just = Mono.just("foo");
但实际上明确了解具体那个类是有用的,这是因为一些操作仅对两者之一有意义、更易理解,就像repository中findOne()接口。
订阅流
现在我们已经初步了解如何生成数据流,现在需要订阅并处理元素。
收集元素
下面代码使用subsribe()方法收集流中的元素:
import org.junit.jupiter.api.Test;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.ConnectableFlux;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import static java.time.Duration.ofSeconds;
import static org.assertj.core.api.Assertions.assertThat;
public class FooTest {
@Test
public void test1(){
List<Integer> elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.subscribe(elements::add);
assertThat(elements).containsExactly(1, 2, 3, 4);
}
}
只有当我们订阅了数据才会开始流动。另外还添加了日志记录,这有助于我们调试程序、查看后台发生了什么。
元素流
通过日志,可以看到完整数据流过程:
09:08:47.222 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
09:08:47.234 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
09:08:47.236 [main] INFO reactor.Flux.Array.1 - | request(unbounded)
09:08:47.236 [main] INFO reactor.Flux.Array.1 - | onNext(1)
09:08:47.236 [main] INFO reactor.Flux.Array.1 - | onNext(2)
09:08:47.236 [main] INFO reactor.Flux.Array.1 - | onNext(3)
09:08:47.236 [main] INFO reactor.Flux.Array.1 - | onNext(4)
09:08:47.237 [main] INFO reactor.Flux.Array.1 - | onComplete()
首先,所有程序都在主线程中运行。这里不深入讨论这方面细节,因为后面会进一步阐述并发性。不过它确实让事情变得简单,我们可以有条不紊地处理所有事情。
现在我们基于日志信息描述下过程:
- onSubscribe() – 当订阅流时调用
- request(unbounded) – 当调用subscribe()方法, 意味着我们正创建订阅从流中请求元素。这种缺省情况为无边界的,即请求所有单个有效元素、
- onNext() – 对每个单个元素都调用
- onComplete() – 接收到最后一个元素后调用。实际上还有一个onError(),如果有异常就会调用它,但在本例中没有
这是在Subscriber接口定义的,作为响应式流规范的一部分实现。实际上在调用onSubscribe()时在幕后实例化Subscriber,该方法很有用,但为了更好地理解其原理,让我们直接提供一个Subscriber接口:
@Test
public void test2(){
List<Integer> elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.subscribe(new Subscriber<Integer>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(Integer integer) {
elements.add(integer);
}
@Override
public void onError(Throwable t) {}
@Override
public void onComplete() {}
});
}
我们能看到Subscriber 实现种每个方面对应可能的处理阶段。Flux提供的助手方法subscribe()避免了大量冗余工作。
与Java8流对比
看起来我们仍然有一些类似于Java 8流的东西在做收集:
List<Integer> collected = Stream.of(1, 2, 3, 4)
.collect(toList());
不完全一致,核心区别在于,反应式是一个推模型,而Java 8流是一个拉模型。在响应式方法中,事件在进入时被推送给订阅者。
接下来要注意的是Streams终端操作提取所有数据并返回结果。而响应式可以从外部资源获得无限信息流,连接附加多个订阅者或临时删除订阅者。还可以做一些事情,如合并流、节流、应用反压,这些将在下文中讨论。
反压
本节讨论反压,在示例中订阅者告诉生产者每次推送单个元素,最终导致订阅者过载,消耗了所有资源。反压是指下游可以告诉上游发送更少的数据以防止超载。我们可以修改订阅者实现来应用反压力。让我们使用request()方法告诉上游每次只发送两个元素:
@Test
public void test3(){
List<Integer> elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.subscribe(new Subscriber<Integer>() {
private Subscription s;
int onNextAmount;
@Override
public void onSubscribe(Subscription s) {
this.s = s;
s.request(2);
}
@Override
public void onNext(Integer integer) {
elements.add(integer);
onNextAmount++;
if (onNextAmount % 2 == 0) {
s.request(2);
}
}
@Override
public void onError(Throwable t) {}
@Override
public void onComplete() {}
});
}
现在运行测试代码,可以看到request(2)被调用,接着是调用两次onNext(1),然后再次调用request(2)。
10:02:13.832 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | request(2)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | onNext(1)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | onNext(2)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | request(2)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | onNext(3)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | onNext(4)
10:02:13.834 [main] INFO reactor.Flux.Array.1 - | request(2)
10:02:13.835 [main] INFO reactor.Flux.Array.1 - | onComplete()
本质上这是响应式编程的拉反压,要求上游只推送一定数量的元素,并且只有当我们准备好了。
假设我们正在接收来自Twitter的消息流,那么将由上游决定如何做。如果有tweet消息传入,但没有来自下游的请求,那么上游可以丢弃消息、或将它们存储在缓冲区中、亦或采取其他策略。
流操作
我们还可以根据合适情况对响应事件流的数据执行相应操作。
映射数据
下面代码执行转换,对流元素乘以2:
@Test
public void test4(){
List<Integer> elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.subscribe(elements::add);
}
当onNext被调用时会执行map().
合并两个流
更有趣的是,与其他流合并为单个流,可以使用zip()方法:
@Test
public void test5(){
List<String> elements = new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.zipWith(Flux.range(0, Integer.MAX_VALUE),
(one, two) -> String.format("First Flux: %d, Second Flux: %d", one, two))
.subscribe(elements::add);
assertThat(elements).containsExactly(
"First Flux: 2, Second Flux: 0",
"First Flux: 4, Second Flux: 1",
"First Flux: 6, Second Flux: 2",
"First Flux: 8, Second Flux: 3");
}
这里创建了另一个Flux流,让其每次增长1,让其与当前流合并,通过日志可以看到整个过程:
10:17:08.007 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
10:17:08.023 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
10:17:08.027 [main] INFO reactor.Flux.Array.1 - | request(32)
10:17:08.028 [main] INFO reactor.Flux.Array.1 - | onNext(1)
10:17:08.028 [main] INFO reactor.Flux.Array.1 - | onNext(2)
10:17:08.028 [main] INFO reactor.Flux.Array.1 - | onNext(3)
10:17:08.028 [main] INFO reactor.Flux.Array.1 - | onNext(4)
10:17:08.028 [main] INFO reactor.Flux.Array.1 - | onComplete()
10:17:08.029 [main] INFO reactor.Flux.Range.2 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription)
10:17:08.029 [main] INFO reactor.Flux.Range.2 - | request(32)
10:17:08.029 [main] INFO reactor.Flux.Range.2 - | onNext(0)
10:17:08.029 [main] INFO reactor.Flux.Range.2 - | onNext(1)
10:17:08.030 [main] INFO reactor.Flux.Range.2 - | onNext(2)
10:17:08.030 [main] INFO reactor.Flux.Range.2 - | onNext(3)
10:17:08.030 [main] INFO reactor.Flux.Array.1 - | cancel()
10:17:08.030 [main] INFO reactor.Flux.Range.2 - | cancel()
我们看到每个Flux有一个订阅者,onNext()调用也交替进行,因此当应用zip()函数时,流中每个元素的索引将匹配。
热流
前面主要聚集冷流,它们是静态的、固定长度流,相对比较容易处理。现实中响应式场景更可能面对是无限流。举例,持续不断鼠标运动事件流需要响应。这种类型流称为热流,因为它们总是在运行,可以在任何时间点订阅,忽略开始数据。
创建ConnectableFlux
创建热流的一种方式为转换冷流。下面创建Flux一致持续往控制台输出结果,模拟从外部资源来的无线数据流:
@Test
public void test6() throws InterruptedException {
ConnectableFlux<Object> publish = Flux.create(fluxSink -> {
while(true) {
fluxSink.next(System.currentTimeMillis());
}
})
.publish();
publish.subscribe(System.out::println);
publish.connect();
}
通过调用publish()产生ConnectableFlux,这意味着调用subscribe()不会导致它开始触发,且支持增加多个订阅:
publish.subscribe(System.out::println);
publish.subscribe(System.out::println);
如果运行代码,什么都不会发生。直到调用connect()方法,Flux才开始触发:
publish.connect();
节流
运行上节代码,控制台产生大量日志,这模拟大量数据传入消费者的场景。下面代码演示如何节流:
@Test
public void test7(){
ConnectableFlux<Object> publish = Flux.create(fluxSink -> {
while(true) {
fluxSink.next(System.currentTimeMillis());
}
})
.sample(ofSeconds(2))
.publish();
publish.subscribe(System.out::println);
publish.connect();
}
这里引入sample方法,并指定参数:间隔时间。现在每两秒推动一次值给订阅者,意味着控制台数据量大大减少。当然有很多策略可以减少数据推送给下游,比如窗口和缓存,这些超出本文的范围。
并发
上面示例都在主线程中执行。但你可以控制代码在不同线程上执行。Scheduler接口提供了异步执行的抽象,为我们提供一些实现。可以尝试订阅在非主线程中执行:
@Test
public void test8(){
List<Integer> elements = new CopyOnWriteArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.subscribeOn(Schedulers.parallel())
.subscribe(elements::add);
}
并行调度让订阅运行在不同的线程,通过日志可以看到。第一个入口从主线程开始,然后Flux运行在另一个线程。
13:48:09.733 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
13:48:09.801 [parallel-1] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
13:48:09.803 [parallel-1] INFO reactor.Flux.Array.1 - | request(unbounded)
13:48:09.803 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(1)
13:48:09.803 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(2)
13:48:09.803 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(3)
13:48:09.803 [parallel-1] INFO reactor.Flux.Array.1 - | onNext(4)
13:48:09.803 [parallel-1] INFO reactor.Flux.Array.1 - | onComplete()
并发比这个示例更有趣,它值得你进一步学习。
总结
在本文我们给出Reactive Core概述,并解释了如何发布和订阅流、应用反压、对流进行操作以及异步处理流数据。学习这些内容希望能对你有帮助,为编写响应式应用程序打下基础。