Spring 5.0系列:Spring Reactor

1.响应式编程

响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。 - 维基百科

响应式编程来源于数据流和变化的传播,意味着由底层的执行模型负责通过数据流来自动传播变化。在2009年的时候,微软提出了一个更优雅地实现异步编程的方式 - Reactive Programming,中文称反应式编程或响应式编程。最早由.NET平台上的Reactive Extensions(Rx)库来实现.Java平台上,更早采用反应式编程技术的是Netflix公司开源的RxJava框架。现在大家比较熟知的Hystrix就是以RxJava为基础开发的。在传统的编程范式中,我们一般通过迭代器(Iterator)模式来遍历一个序列。这种遍历方式是由调用者来控制节奏的,采用的是拉的方式。每次 调用者通过next()方法来获取序列中的下一个值。使用反应式流时采用的则是推的方式,即常见的发布者 - 订阅者模式。当发布者有新的数据产生时,这些数据会被推送到订阅者来进行处理。在反应式流上可以添加各种不同的操作来对数据进行处理,形成数据处理链。这个以声明式的方式添加的处理链只在订阅者进行订阅操作时才会真正执行。

如果 Publisher 发布消息太快了,超过了 Subscriber 的处理速度,那怎么办。这就是 Backpressure 的由来,Reactive Programming 框架需要提供机制,使得 Subscriber 能够控制消费消息的速度。订阅者可以通过 request()方法来声明其一次所能处理的消息数量,而生产者就只会产生相应数量的消息,直到下一次 request()方法调用。这实际上变成了推拉结合的模式。

Reactive Streams定义的API主要的接口有这三个:

  • Publisher:发布者是有序元素的提供者,根据从其订阅者收到的需求发布它们
public interface Publisher {
    public void subscribe(Subscriber s);
}
  • Subscriber:在将订阅者实例传递给Publisher.subscribe(订阅者)之后,将接收对Subscriber.onSubscribe(订阅)的调用。
public interface Subscriber {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}
  • Subcription:订阅表示订阅发布者的订阅者的一对一生命周期。
public interface Subscription {
    public void request(long n);
    public void cancel();
}
  • Processor:处理器代表一个处理阶段 - 既是订阅者又是发布者,并遵守两者的合同。

Reactive Streams、Reactor 和 Web Flux之间的关系
Reactive Streams 是规范,Reactor 实现了 Reactive Streams。Web Flux 以 Reactor 为基础,实现 Web 领域的反应式编程框架。

2.Reactor

Reactor 框架主要有两个主要的模块:reactor-core 和 reactor-ipc。前者主要负责 Reactive Programming 相关的核心 API 的实现,后者负责高性能网络通信的实现,目前是基于 Netty 实现的。

在 Reactor 中,经常使用的类并不是很多,主要有以下两个:

  • Mono 实现了 org.reactivestreams.Publisher 接口,代表0到1个元素的发布者。
  • Flux 同样实现了 org.reactivestreams.Publisher 接口,代表0到N个元素的发表者。

可能会使用到的类

  • Scheduler 表示背后驱动反应式流的调度器,通常由各种线程池实现。

解释
Flux 和 Mono 是 Reactor 中的两个基本概念。Flux 表示的是包含 0 到 N 个元素的异步序列。在该序列中可以包含三种不同类型的消息通知:正常的包含元素的消息、序列结束的消息和序列出错的消息。当消息通知产生时,订阅者中对应的方法 onNext(), onComplete()和 onError()会被调用。Mono 表示的是包含 0 或者 1 个元素的异步序列。该序列中同样可以包含与 Flux 相同的三种类型的消息通知。Flux 和 Mono 之间可以进行转换。对一个 Flux 序列进行计数操作,得到的结果是一个 Mono对象。把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。

3.Flux和Mono

3.1 创建
  • Flux 类的静态方法
    • just():可以指定序列中包含的全部元素。创建出来的 Flux 序列在发布这些元素之后会自动结束。
    • fromArray(),fromIterable()和 fromStream():可以从一个数组、Iterable 对象或 Stream 对象中创建 Flux 对象。
    • empty():创建一个不包含任何元素,只发布结束消息的序列。
    • error(Throwable error):创建一个只包含错误消息的序列。
    • never():创建一个不包含任何消息通知的序列。
    • range(int start, int count):创建包含从 start 起始的 count 个数量的 Integer 对象的序列。
    • interval(Duration period)和 interval(Duration delay, Duration period):创建一个包含了从 0 开始递增的 Long 对象的序列。其中包含的元素按照指定的间隔来发布。除了间隔时间之外,还可以指定起始元素发布之前的延迟时间。
Flux.just("Hello", "World").subscribe(System.out::println);
Flux.fromArray(new Integer[] {1, 2, 3}).subscribe(System.out::println);
Flux.empty().subscribe(System.out::println);
Flux.range(1, 10).subscribe(System.out::println);
Flux.interval(Duration.of(10, ChronoUnit.SECONDS)).subscribe(System.out::println);
Flux.intervalMillis(1000).subscribe(System.out::println);

当序列的生成需要复杂的逻辑时,则应该使用 generate() 或 create() 方法。

  • generate()方法
    generate()方法通过同步和逐一的方式来产生 Flux 序列。序列的产生是通过调用所提供的 SynchronousSink 对象的 next(),complete()和 error(Throwable)方法来完成的。逐一生成的含义是在具体的生成逻辑中,next()方法只能最多被调用一次。通过 complete()方法来结束该序列。如果不调用 complete()方法,所产生的是一个无限序列。
Flux.generate(sink -> {
    sink.next("Hello");
    sink.complete();
}).subscribe(System.out::println);
  • create()方法
    create()方法与 generate()方法的不同之处在于所使用的是 FluxSink 对象。FluxSink 支持同步和异步的消息产生,并且可以在一次调用中产生多个元素。在代码清单 3 中,在一次调用中就产生了全部的 10 个元素。
Flux.create(sink -> {
    for (int i = 0; i < 10; i++) {
        sink.next(i);
    }
    sink.complete();
}).subscribe(System.out::println);

Mono 的创建方式与之前介绍的 Flux 比较相似。

Mono.fromSupplier(() -> "Hello").subscribe(System.out::println);
Mono.justOrEmpty(Optional.of("Hello")).subscribe(System.out::println);
Mono.create(sink -> sink.success("Hello")).subscribe(System.out::println);

如果有哪些不不清楚可以去查官方文档>>地址

3.2操作符

buffer 和 bufferTimeout
这两个操作符的作用是把当前流中的元素收集到集合中,并把集合对象作为流中的新元素。在进行收集时可以指定不同的条件:所包含的元素的最大数量或收集的时间间隔。方法 buffer()仅使用一个条件,而 bufferTimeout()可以同时指定两个条件。指定时间间隔时可以使用 Duration 对象或毫秒数。
除了元素数量和时间间隔之外,还可以通过 bufferUntil 和 bufferWhile 操作符来进行收集。这两个操作符的参数是表示每个集合中的元素所要满足的条件的 Predicate 对象。bufferUntil 会一直收集直到 Predicate 返回为 true。使得 Predicate 返回 true 的那个元素可以选择添加到当前集合或下一个集合中;bufferWhile 则只有当 Predicate 返回 true 时才会收集。一旦值为 false,会立即开始下一次收集。

Flux.range(1, 100).buffer(20).subscribe(System.out::println);
Flux.interval(Duration.ofMillis(100)).bufferTimeout(5,Duration.ofMillis(400)).take(5).toStream().forEach(System.out::println);
Flux.range(1, 10).bufferUntil(i -> i % 2 == 0).subscribe(System.out::println);
Flux.range(1, 10).bufferWhile(i -> i % 2 == 0).subscribe(System.out::println);

注意在第二行代码中通过 toStream()方法把 Flux 序列转换成 Java 8 中的 Stream 对象,再通过 forEach()方法来进行输出。这是因为序列的生成是异步的,而转换成 Stream 对象可以保证主线程在序列生成完成之前不会退出,从而可以正确地输出序列中的所有元素。
filter
对流中包含的元素进行过滤,只留下满足 Predicate 指定条件的元素。

Flux.range(1, 10).filter(i -> i % 2 == 0).subscribe(System.out::println);

语句输出的是 1 到 10 中的所有偶数。
take
take 系列操作符用来从当前流中提取元素。提取的方式可以有很多种。

  • take(long n),take(Duration timespan)和 takeMillis(long timespan):按照指定的数量或时间间隔来提取。
  • takeLast(long n):提取流中的最后 N 个元素。
  • takeUntil(Predicate
Flux.range(1, 1000).take(10).subscribe(System.out::println);
Flux.range(1, 1000).takeLast(10).subscribe(System.out::println);
Flux.range(1, 1000).takeWhile(i -> i < 10).subscribe(System.out::println);
Flux.range(1, 1000).takeUntil(i -> i == 10).subscribe(System.out::println)

reduce 和 reduceWith
reduce 和 reduceWith 操作符对流中包含的所有元素进行累积操作,得到一个包含计算结果的 Mono 序列。累积操作是通过一个 BiFunction 来表示的。在操作时可以指定一个初始值。如果没有初始值,则序列的第一个元素作为初始值。

Flux.range(1, 100).reduce((x, y) -> x + y).subscribe(System.out::println);
Flux.range(1, 100).reduceWith(() -> 100, (x, y) -> x + y).subscribe(System.out::println);

merge 和 mergeSequential
merge 和 mergeSequential 操作符用来把多个流合并成一个 Flux 序列。不同之处在于 merge 按照所有流中元素的实际产生顺序来合并,而 mergeSequential 则按照所有流被订阅的顺序,以流为单位进行合并。

 Flux.merge(Flux.interval(Duration.ofMillis(100)).take(5), Flux.interval(Duration.ofMillis(50), Duration.ofMillis(100)).take(5))
                .toStream()
               .forEach(System.out::println);
        Flux.mergeSequential(Flux.interval(Duration.ofMillis(100)).take(5), Flux.interval(Duration.ofMillis(50), Duration.ofMillis(100)).take(5))
                .toStream()
                .forEach(System.out::println);

合并的流都是每隔 100 毫秒产生一个元素,不过第二个流中的每个元素的产生都比第一个流要延迟 50 毫秒。在使用 merge 的结果流中,来自两个流的元素是按照时间顺序交织在一起;而使用 mergeSequential 的结果流则是首先产生第一个流中的全部元素,再产生第二个流中的全部元素。
flatMap 、concatMap和 flatMapSequential
flatMap 和 flatMapSequential 操作符把流中的每个元素转换成一个流,再把所有流中的元素进行合并。flatMapSequential 和 flatMap 之间的区别与 mergeSequential 和 merge 之间的区别是一样的。

Flux.just(5, 10)
    .flatMap( x-> Flux.just(x+x))
        .subscribe(System.out::println);
Flux.just(5, 10)
    .flatMap(x -> Flux.interval(Duration.ofMillis(100)).take(x))
    .toStream()
    .forEach(System.out::println);
Flux.just(5, 10)
    .flatMapSequential(x -> Flux.interval(Duration.ofMillis(100)).take(x))
    .toStream()
    .forEach(System.out::println);

concatMap 操作符的作用也是把流中的每个元素转换成一个流,再把所有流进行合并。与 flatMap 不同的是,concatMap 会根据原始流中的元素顺序依次把转换之后的流进行合并;与 flatMapSequential 不同的是,concatMap 对转换之后的流的订阅是动态进行的,而 flatMapSequential 在合并之前就已经订阅了所有的流。

Flux.just(5, 10)
    .concatMap(x -> Flux.interval(Duration.ofMillis(100)).take(x))
    .toStream()
    .forEach(System.out::println);

3.3.消费消息和异常处理

直接消费的 Mono 或 Flux 的方式就是调用 subscribe 方法。调用 subscribe 方法时可以指定需要处理的消息类型。可以只处理其中包含的正常消息,也可以同时处理错误消息和完成消息。

 Mono.error(new IllegalStateException())
                .subscribe(System.out::println, System.err::println);
        Flux.just(1, 2)
                .subscribe(System.out::println, System.err::println);

正常的消息处理相对简单。当出现错误时,有多种不同的处理策略。第一种策略是通过 onErrorReturn()方法返回一个默认值。在代码清单 16 中,当出现错误时,流会产生默认值 0.

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .onErrorReturn(0)
        .subscribe(System.out::println);

第二种策略是通过 switchOnError()方法来使用另外的流来产生元素。当出现错误时,将产生 Mono.just(0)对应的流,也就是数字 0。

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .switchOnError(Mono.just(0))
        .subscribe(System.out::println);

第三种策略是通过 onErrorResumeWith()方法来根据不同的异常类型来选择要使用的产生元素的流。在代码清单 18 中,根据异常类型来返回不同的流作为出现错误时的数据来源。因为异常的类型为 IllegalArgumentException,所产生的元素为-1。

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalArgumentException()))
        .onErrorResumeWith(e -> {
            if (e instanceof IllegalStateException) {
                return Mono.just(0);
            } else if (e instanceof IllegalArgumentException) {
                return Mono.just(-1);
            }
            return Mono.empty();
        })
        .subscribe(System.out::println);

可以通过 retry 操作符来进行重试。重试的动作是通过重新订阅序列来实现的。在使用 retry 操作符时可以指定重试的次数。

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .retry(1)
        .subscribe(System.out::println);

输出的结果是 1,2,1,2 和错误信息。

4.调度器

前面介绍了反应式流和在其上可以进行的各种操作,通过调度器(Scheduler)可以指定这些操作执行的方式和所在的线程。
有下面几种不同的调度器实现。

  • 当前线程,通过 Schedulers.immediate()方法来创建。
  • 单一的可复用的线程,通过 Schedulers.single()方法来创建。
  • 使用弹性的线程池,通过 Schedulers.elastic()方法来创建。线程池中的线程是可以复用的。当所需要时,新的线程会被创建。如果一个线程闲置太长时间,则会被销毁。该调度器适用于 I/O 操作相关的流的处理。
  • 使用对并行操作优化的线程池,通过 Schedulers.parallel()方法来创建。其中的线程数量取决于 CPU 的核的数量。该调度器适用于计算密集型的流的处理。
  • 使用支持任务调度的调度器,通过 Schedulers.timer()方法来创建。
  • 从已有的 ExecutorService 对象中创建调度器,通过 Schedulers.fromExecutorService()方法来创建

某些操作符默认就已经使用了特定类型的调度器。比如interval方法创建的流就使用了由 Schedulers.parallel()创建的调度器。

通过 publishOn()和 subscribeOn()方法可以切换执行操作的调度器。其中 publishOn()方法切换的是操作符的执行方式,而 subscribeOn()方法切换的是产生流中元素时的执行方式。

Flux.create(sink -> {
    sink.next("sink:"+Thread.currentThread().getName());
    sink.complete();
    })
    .publishOn(Schedulers.single())
    .map(x -> x+"   map:"+Thread.currentThread().getName())
    .subscribeOn(Schedulers.parallel())
    .toStream()
    .forEach(System.out::println);

5.总结

还有测试等API由于篇幅有限,没有展开详细说明。感兴趣的小伙伴可以去官方文档查阅。Reactor 作为一个基于反应式流规范的新的 Java 库,可以作为反应式应用的基础。本文对 Reactor 库做了详细的介绍,包括 Flux 和 Mono 序列的创建、常用操作符的使用、调度器等。在后面的文章会讲到webFlux的应用。

你可能感兴趣的:(Spring系列)