快速掌握Reactor Core实现响应式编程

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()

首先,所有程序都在主线程中运行。这里不深入讨论这方面细节,因为后面会进一步阐述并发性。不过它确实让事情变得简单,我们可以有条不紊地处理所有事情。

现在我们基于日志信息描述下过程:

  1. onSubscribe() – 当订阅流时调用
  2. request(unbounded) – 当调用subscribe()方法, 意味着我们正创建订阅从流中请求元素。这种缺省情况为无边界的,即请求所有单个有效元素、
  3. onNext() – 对每个单个元素都调用
  4. 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概述,并解释了如何发布和订阅流、应用反压、对流进行操作以及异步处理流数据。学习这些内容希望能对你有帮助,为编写响应式应用程序打下基础。

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值