Reactor-反应式编程实用指南-全-

Reactor 反应式编程实用指南(全)

原文:zh.annas-archive.org/md5/483c7d8caeecdab1dabbbc736910bbe2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Reactor 是 Java 9 反应式流规范的实现,这是一个用于异步数据处理的应用程序接口。该规范基于反应式编程范式,使开发者能够以降低复杂性和缩短时间的方式构建企业级、健壮的应用程序。《Reactors 实践反应式编程》向您展示了 Reactor 的工作原理,以及如何使用它来开发 Java 中的反应式应用程序。

本书从 Reactor 的基础知识及其在构建有效应用程序中的作用开始。您将学习如何构建完全非阻塞的应用程序,并随后将指导您了解发布者和订阅者 API。前四章将帮助您理解反应式流和 Reactor 框架。接下来的四章将使用 Reactor 构建一个基于 REST 的微服务 SpringWebFlux 扩展来构建 Web 服务。它们演示了流、背压和执行模型的概念。您将了解如何使用两个反应式可组合 API,Flux 和 Mono,这些 API 被广泛用于实现反应式扩展。在最后两章中,您将了解反应式流和 Reactor 框架。

各章节解释了最重要的部分,并构建简单的程序来建立基础。到本书结束时,您将获得足够的信心来构建反应式和可扩展的微服务。

本书面向的对象

对于任何对 Java 的基本概念有基本了解,并且能够轻松使用 Reactor 开发事件驱动和数据驱动应用程序的人来说,这本书是为您准备的——这是一本逐步指南,帮助您开始使用反应式流和 Reactor 框架。

本书涵盖的内容

第一章,开始使用反应式流,解释了反应式流 API,并介绍了反应式范式及其优势。本章还介绍了 Reactor 作为反应式流的实现。

第二章,Reactors 中的发布者和订阅者 API,解释了生产者和订阅者 API 以及 Reactor 的相应 Flux 和 Mono 影响。它还讨论了 Flux 和 Mono 的用例以及相应的接收器。我们还将探讨组件的热和冷变体。

第三章,数据和流处理,探讨了在发布者生成数据并被订阅者消费之前,我们如何处理这些数据,可用的可能操作,以及如何将它们组合起来构建一个健壮的流处理管道。流处理还涉及转换、旋转和聚合数据,然后生成新的数据。

第四章,处理器,介绍了 Reactor 中可用的开箱即用的处理器。处理器是特殊的发布者,也是订阅者,在实践之前理解为什么我们需要它们是非常重要的。

第五章,SpringWebFlux for Microservices,介绍了 SpringWebFlux,这是 Reactor 的 Web 扩展。它解释了 RouterFunction、HandlerFunction 和 FilterFunction 的概念。然后我们将使用 Mongo 作为存储来构建基于 REST 的微服务。

第六章,动态渲染,将模板引擎集成到我们在上一章中介绍的基于 REST 的微服务中,以渲染动态内容。它还演示了请求过滤器。

第七章,流控制和背压,讨论了流控制,这是反应式编程的一个重要方面,对于控制快速发布者的溢出是基本必需的。它还讨论了控制整个管道处理的各种方法。

第八章,处理错误,正如其标题所暗示的,解释了如何处理错误。所有 Java 开发者都习惯于错误处理的 try-catch-finally 块。本章将其转换为流处理。它还涵盖了如何从错误中恢复以及如何生成错误。这对于所有企业应用都是基本要求。

第九章,执行控制,探讨了 Reactor 中用于处理构建流的多种策略。它可以按某个间隔调度,或分组批量处理,或者所有操作都可以并行执行。

第十章,测试和调试,列出了我们可以测试流的方法,因为没有测试的开发是不完整的。我们将构建 JUnit 测试,这些测试将使用 Reactor 提供的某些测试工具来创建健壮的测试。本章还列出了调试在 Reactor 之上构建的异步流的方法。

为了充分利用这本书

  • 理解基本 Java 概念是至关重要的

  • 需要 Java 标准版,JDK 8,以及 IntelliJ IDEA IDE 或更高版本

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误表”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本解压缩或提取文件夹:

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“所有Subscribe方法返回Disposable类型。此类型也可以用于取消订阅。”

代码块设置为如下所示:

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

任何命令行输入或输出都写成如下所示:

gradlew bootrun

粗体:表示新术语、重要单词或您在屏幕上看到的单词。

警告或重要提示看起来是这样的。

小贴士和技巧看起来是这样的。

联系我们

我们读者的反馈总是受欢迎的。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送邮件至customercare@packtpub.com

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上遇到我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用过本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问packt.com

第一章:开始使用反应式流

随着时间的推移,应用架构已经发展。企业越来越需要构建在需要时可以保持响应性和可扩展性的系统。系统还应易于维护和快速发布。根据这些需求,我们已经开始构建松散耦合的服务应用。我们不再将系统构建为一个庞大的应用程序。相反,我们将系统拆分为多个独立、自主的服务。这些服务的目标是做一件事,并且做好。

在本章中,我们将讨论构建此类服务时相关的担忧。我们还将探讨如何解决这些担忧。

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章节的 GitHub 链接为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter01

反应式架构

当我们开始构建微服务架构时,我们试图涉及不同的服务以提供业务解决方案。我们通常将服务构建为传统的 API 模型,其中每个服务都可以与其他服务交互。这被称为分布式架构。如果分布式架构设计不当,性能问题会很快显现出来。要使众多分布式服务同时工作以提供预期的性能可能很困难。因此,提供需要大量数据交换服务(如 Netflix、Amazon 或 Lightbend)的公司已经看到了对替代范例的需求,这些范例可以用于具有以下特性的系统:

  • 由非常松散耦合的组件组成

  • 对用户输入做出响应

  • 能够抵御变化的负载条件

  • 总是可用

为了实现上述特性,我们需要构建事件驱动的、模块化的服务,这些服务通过使用通知相互通信。反过来,我们可以对系统的事件流做出响应。模块化服务更易于扩展,因为我们可以在不停止整个应用程序的情况下添加或删除服务实例。如果我们能够隔离错误并采取纠正措施,完整的架构将具有容错性。上述四个特性是反应式宣言的基本原则。反应式宣言指出,每个反应式系统应由松散耦合的组件组成,这些组件依赖于异步、消息驱动的架构。它们必须对用户输入保持响应,并将故障隔离到单个组件。为了应对不同的负载条件,必须进行复制。以下是反应式宣言的示意图:

图片

响应式宣言描述了一个响应式系统。它并不要求系统基于响应式编程或任何其他响应式库。我们可以构建一个基于消息驱动、弹性、可扩展和响应的应用程序,而不使用响应式库,但基于响应式库构建应用程序会更简单。

响应式编程

我们大多数人编写的是命令式应用程序,其中需要语句来改变应用程序状态。代码被执行,最终达到一个状态。在状态计算之后,当底层因素发生变化时,状态不会改变。以下代码作为例子:

int value1 = 5;
int value2 = 10;
int sum = val1 + val2;
System.out.println(sum); // 15
value1 = 15;
System.out.println(sum); // 15

总和仍然是15,尽管value1已经改变。

另一方面,响应式编程是关于变化的传播。它也被称为声明式编程,其中我们表达我们的意图和应用程序状态,这些状态由底层因素的变化动态确定。在响应式范式下,前面的总和程序示例将如下行为:

int value1 = 5;
int value2 = 10;
int sum = val1 + val2;
System.out.println(sum); // 15
value1 = 15;
System.out.println(sum); // 25

因此,如果一个程序对底层因素的变化做出反应,它可以被称为响应式。响应式程序可以使用命令式技术,如回调来构建。对于只有一个事件的程序来说,这可能没问题。然而,对于有数百个事件发生的应用程序,这很容易导致回调地狱;我们可能有多个相互依赖的回调,很难确定哪些正在执行。因此,我们需要一套新的抽象,使我们能够无缝地在网络边界上构建异步、事件驱动的交互。在 Java 等不同的命令式语言中,有提供这些抽象的库。这些库被称为响应式扩展

ReactiveX

响应式扩展,也称为 ReactiveX,使我们能够将应用程序中的异步事件表示为一组可观察序列。其他应用程序可以订阅这些可观察序列,以便接收正在发生的事件的通知。生产者可以随后将这些通知事件推送到消费者。或者,如果消费者速度较慢,它可以根据自己的消费速率拉取通知事件。生产者和其消费者之间的端到端系统被称为管道。需要注意的是,管道默认是懒加载的,直到被消费者订阅才会实际化。这与代表主动工作的急切 Java 类型(如 Future)非常不同。ReactiveX API 由以下组件组成:

  1. 可观察序列:可观察序列代表了 ReactiveX 的核心概念。它们代表发出项的序列,并生成传播到预期订阅者的事件。

  2. 观察者:任何应用程序都可以通过创建观察者并订阅相应的可观察对象来表达其对由可观察对象发布的事件的意图。意图通过OnNextOnCompletedOnError方法来表示。每个可观察对象发送一系列事件,然后是一个完成事件,这些事件执行这些方法。

  3. 操作符:操作符使我们能够转换、组合和处理由可观察对象发出的项目序列。可观察对象上的操作符提供一个新的可观察对象,因此,它们可以相互连接。它们不会独立于原始可观察对象工作;相反,它们作用于由前一个操作符生成的可观察对象以生成一个新的可观察对象。完整的操作符链是惰性的。它不会在观察者订阅之前进行评估。完整的链如下所示:

图片

ReactiveX 提供了构建反应式应用程序的架构设计。不同的命令式语言围绕它构建了各个库,以使其可用。这些抽象使我们能够构建异步、非阻塞的应用程序,并提供以下章节中列出的额外好处。

组合流

在软件设计中,组合指的是将不同的实体分组,并将每个组视为单个实体。此外,单个实体表现出与其所引用的类型相同的行为。ReactiveX 流本质上是组合的。它们使得将现有数据流组合起来、添加转换以及生成新的数据流成为可能。此外,所有这些都可以以声明性方式完成,从而使整体解决方案在长期内可维护。

灵活操作符

这些库提供了一系列适用于各种函数的操作符。每个操作符都像装配线上的工作站一样完成任务。它从前一个工作站接收输入,并向下一个工作站提供输入。这些操作符提供各种数据转换、流编排和错误处理。

ReactiveX 使构建基于事件的程序变得更加容易。然而,该框架没有展示不同事件驱动应用程序之间应该如何相互交互。在一个由众多事件驱动服务组成的微服务架构中,所获得的收益往往被所需的进程间通信的解决方案所抵消。

Reactive Streams

Reactive Streams 是一个规范,它确定了构建大量无界数据异步处理所需的最小接口集。它是一个针对 JVM 和 JavaScript 运行时的规范。Reactive Streams 规范的主要目标是标准化应用程序异步边界之间的流数据交换。API 由以下四个接口组成:

  1. 发布者:发布者负责生成无限数量的异步事件并将这些事件推送到相关的订阅者。

  2. 订阅者:订阅者是发布者发布事件的消费者。订阅者可以获得订阅、数据、完成和错误事件。它可以选择对其中任何一个执行操作。

  3. 订阅:订阅是发布者和订阅者之间用于调解两者之间数据交换的共享上下文。订阅仅在订阅者处可用,并使其能够控制来自发布者的事件流。如果发生错误或完成,订阅将无效。订阅者还可以取消订阅,以关闭其流。

  4. 处理器:处理器代表订阅者和发布者之间数据处理的一个阶段。因此,它被两者所约束。处理器必须遵守发布者和订阅者之间的合同。如果有错误,它必须将其传播回订阅者。

Reactive Streams 规范是来自 Kaazing、Netflix、Pivotal、Red Hat、Twitter、Typesafe 和其他许多公司的工程师共同努力的结果。

尽管只有四个接口,但大约有 30 条规则来规范发布者和订阅者之间的数据交换。这些规则基于以下章节中提到的两个原则。

异步处理

异步执行指的是执行任务时无需等待之前执行的任务完成。执行模型解耦任务,使得每个任务都可以同时执行,利用可用的硬件。

Reactive Streams API 以异步方式传递事件。发布者可以以同步阻塞方式生成事件数据。另一方面,每个事件处理程序都可以以同步阻塞方式处理事件。然而,事件发布必须异步进行。在处理事件时,它不能被订阅者阻塞。

订阅者背压

订阅者可以控制其队列中的事件以避免任何溢出。如果还有额外容量,它还可以请求更多事件。背压强制发布者根据订阅者限制事件队列。此外,订阅者可以要求一次接收一个元素,构建停止等待协议。它也可以请求多个元素。另一方面,发布者可以应用适当的缓冲区来保存未发送的事件,或者如果生产率超过消费率,它可以直接开始丢弃事件。

重要的是要注意,Reactive Streams API 旨在不同系统之间的事件流。与 ReactiveX 不同,它不提供任何执行转换的算子。该 API 已被采纳为 JDK 9 中java.util.concurrent.Flow包的一部分。

David Karnok 的分类

David Karnok,Rxjava 和 Reactor 等各种反应式项目的资深人士,将反应式库的演变划分为以下几代。

零代

零代围绕着 java.util.observable 接口和相关回调。它本质上使用可观察设计模式进行反应式开发。它缺乏必要的组合、操作符和背压支持。

第一代

第一代代表了 Erik Mejer 通过构建 Rx.NET 来解决反应式问题的尝试。这指的是以 IObserverIObservable 接口形式实现的实现。整体设计是同步的,缺乏背压。

第二代

第二代 API 解决了第一代中背压和同步处理的问题。这一代指的是反应式扩展的第一批实现,如 RxJava 1.X 和 Akka。

第三代

第三代指的是 Reactive Streams 规范,它使库实现者之间能够相互兼容,并在边界之间组合序列、取消和背压。它还允许最终用户根据自己的意愿在实现之间切换。

第四代

第四代指的是反应式操作符可以以外部或内部方式组合,从而实现性能优化。第四代反应式 API 看起来与第三代相似,但内部操作符发生了显著变化,以产生预期的效益。Reactor 3.0 和 RxJava 2.x 属于这一代。

第五代

第五代指的是未来的工作,届时将需要在流上进行双向反应式 I/O 操作。

Reactor

Reactor 是由 Pivotal 开源团队完成的一个实现,符合 Reactive Streams API。该框架使我们能够构建响应式应用程序,负责处理背压和请求处理。该库提供了以下功能。

无限数据流

Reactor 提供了生成无限数据序列的实现。同时,它提供了一个用于发布单个数据条目的 API。这适用于请求-响应模型。每个 API 都提供了旨在处理特定数据基数的方法。

而不是等待整个数据集到达,每个数据流的订阅者可以按数据到达的顺序处理项目。这从空间和时间上优化了数据处理。内存需求限制在同时到达的项目的子集,而不是整个集合。在时间上,结果一旦收到第一个元素就开始到达,而不是等待整个数据集。

推拉模型

Reactor 是一个推拉系统。快速的生产者引发事件并等待较慢的订阅者拉取它们。在慢速发布者和快速订阅者的情况下,订阅者等待从生产者那里推送事件。Reactive Streams API 允许这种数据流在本质上具有动态性。它只依赖于生产的实时速率和消费速率。

并发无关

Reactor 执行模型是并发无关的。它不涉及不同流应该如何处理。库简化了不同的执行模型,开发者可以根据自己的意愿使用。所有转换都是线程安全的。有各种算子可以通过组合不同的同步流来影响执行模型。

算子词汇表

Reactor 提供了一系列算子。这些算子允许我们选择、过滤、转换和组合流。操作作为管道中的工作站执行。它们可以组合在一起来构建高级、易于推理的数据管道。

Reactor 已被 Spring Framework 5.0 采用,以提供反应式功能。完整项目包括以下子项目:

  • Reactor-Core:该项目为 Reactive Streams API 提供实现。该项目也是 Spring Framework 5.0 反应式扩展的基础。

  • Reactor-Extra:该项目补充了 Reactor-Core 项目。它提供了在 Reactive Streams API 之上工作的必要算子。

  • Reactor-Tests:该项目包含测试验证的实用工具。

  • Reactor-IPC:该项目提供非阻塞的进程间通信。它还提供了为 HTTP(包括 WebSocket)、TCP 和 UDP 准备的背压就绪网络引擎。该模块也可以用于构建微服务。

项目设置

本书采用动手实践的方法;您将通过与示例一起工作来学习 Reactor。本章将设置我们将在这本书中使用的项目。在我们继续之前,我们必须做一些设置。请在您的机器上安装以下项目:

$ java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)
  • IntelliJ IDEA 2018.1 或更高版本:我们将使用 IntelliJ 的最新社区版。您可以从 JetBrains 网站下载最新版本,网址为www.jetbrains.com/idea/download/。我们将使用版本 2018.1.1。

  • Gradle:Gradle 是 JVM 生态系统中最受欢迎的构建工具之一。它用于依赖项管理和运行自动化任务。您不需要在本地机器上安装 Gradle;我们将使用 Gradle 包装器,它会为您的项目下载和安装 Gradle。要了解更多关于 Gradle 的信息,您可以参考 Gradle 文档docs.gradle.org/current/userguide/userguide.html

现在我们已经拥有了所有先决条件,让我们通过 IntelliJ IDEA 本身来创建一个 Gradle 项目:

  1. 启动 IntelliJ IDEA,您将看到以下屏幕,您可以在其中开始创建项目:

图片

  1. 点击“创建新项目”以开始创建 Java Gradle 项目的流程。您将看到一个创建新项目的屏幕。在此,选择 Gradle 和 Java,如以下截图所示。您还必须指定项目 SDK。点击“新建”按钮以选择 JDK 8。然后,点击“下一步”以进入下一屏幕:

图片

  1. 现在您将被要求输入 GroupId 和 ArtifactId。点击“下一步”以进入下一屏幕:

图片

  1. 下一屏幕将要求您指定一些 Gradle 设置。我们将选择“使用自动导入”,这样 Gradle 将在我们将新依赖项添加到构建文件时自动添加它们。点击“下一步”以进入最终屏幕:

图片

  1. 在此屏幕上,您将被要求指定您想要创建项目的位置。选择一个方便的应用程序目录路径。最后,点击“完成”以完成项目创建过程:

图片

现在 Java Gradle 项目已经创建,我们不得不在 Gradle 构建文件build.gradle中进行一些更改,即更改以下内容:

plugins {
    id "io.spring.dependency-management" version "1.0.5.RELEASE"
}
group 'com.reactor'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}
dependencyManagement {
    imports {
        mavenBom "io.projectreactor:reactor-bom:Bismuth-RELEASE"
    }
}
dependencies {
    compile 'io.projectreactor:reactor-core'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

在前面的build.gradle文件中,我们做了以下操作:

  1. 添加了io.spring.dependency-management插件。此插件允许我们有一个dependency-management部分,用于配置依赖项版本。

  2. 配置了dependency-management插件以下载 Reactor 的最新版本。我们使用了 Reactor 项目发布的 Maven BOM。

  3. reactor-core依赖项添加到项目依赖项列表中。

这就是我们开始使用 Reactor 所需要做的全部事情。

在撰写本文时,Bismuth-RELEASE 是 Reactor 的最新版本。

现在,让我们构建一个简单的测试用例来查看我们如何使用 Reactor API。我们将构建一个生成斐波那契数的简单测试用例。维基百科将斐波那契数定义为如下:

“在数学中,斐波那契数列是以下整数序列中的数,称为斐波那契序列,其特征是每个数(从第三个数开始)都是前两个数的和:

0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55 , 89 , 144, ...”

让我们构建用于生成斐波那契数的测试。测试用例将从 0 和 1 开始生成一个序列。它将生成前 50 个斐波那契数,并将第 50 个数验证为 7778742049

@Test
public void testFibonacci() {
  Flux<Long> fibonacciGenerator = Flux.generate(
    () -> Tuples.<Long, Long>of(0L, 1L),
     (state, sink) -> {
       sink.next(state.getT1());
       return Tuples.of(state.getT2(), state.getT1() + state.getT2());
     });
     List<Long> fibonacciSeries = new LinkedList<>();
     int size = 50;
     fibonacciGenerator.take(size).subscribe(t -> {
       fibonacciSeries.add(t);
     });
     System.out.println(fibonacciSeries);
     assertEquals( 7778742049L, fibonacciSeries.get(size-1).longValue());
}

完整的代码可以在github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter01找到。

在先前的测试用例中,我们执行了以下操作:

  1. 我们通过使用 Flux.generate() 调用创建斐波那契数作为 Flux<Long>。API 有一个 StateSink。现在,我们将把 Flux API 的细节留给下一章。

  2. API 以 Tuple [0 , 1] 作为种子。然后它通过 Sink.next() 调用发出对的第一个参数。

  3. API 还通过聚合这对数生成下一个斐波那契数。

  4. 接下来,我们使用 take() 操作符选择前 50 个斐波那契数。

  5. 我们订阅发布的数字,然后将接收到的数字追加到 List<Long> 中。

  6. 最后,我们断言发布的数字。

在先前的测试用例中,我们使用了多个 Rector 功能。我们将在后续章节中详细介绍每个功能。现在,让我们执行测试用例并检查我们的项目是否运行正常。

运行我们的单元测试应该会给我们一个绿色的条形,如下所示:

图片

摘要

在本章中,我们讨论了需要反应式范式。我们还探讨了范式的演变,从反应式编程到反应式扩展,再到反应式流。此外,我们讨论了反应式流规范作为针对 JVM 的规范,旨在以下方面:

  • 处理序列中可能无界数量的元素

  • 在组件之间异步传递具有强制非阻塞背压的元素

在本章末尾,我们介绍了 Reactor,这是 Pivotal 团队的一个实现,并使用它构建了一个示例项目。在下一章中,我们将讨论 Reactor 中可用的 API。

问题

  1. 反应式宣言的原则是什么?

  2. 什么是反应式扩展?

  3. 反应式流规范提供了哪些内容?

  4. 反应式流基于哪些原则?

  5. Reactor 框架的主要特性是什么?

进一步阅读

第二章:Reactor 中的发布者和订阅者 API

上一章为您简要介绍了反应范式的发展历程。在那章中,我们讨论了如何通过反应流在命令式语言(如 Java)中执行反应式建模。我们还讨论了反应式中的关键组件——发布者和订阅者。在本章中,我们将详细介绍这两个组件。由于反应流是一个规范,它不提供这两个组件的实现。它只列出了各个组件的责任。具体实现留给实现库,如 Reactor,为接口提供具体实现。Reactor 还提供了不同的方法来实例化发布者和订阅者对象。

本章我们将涵盖以下主题:

  • 将流与现有的 Java API 进行比较

  • 理解 Flux API

  • 理解 Mono API

  • 为 Flux 和 Mono 发布者构建订阅者

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章节的 GitHub 链接为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter02

流发布者

如我们在上一章中讨论的,发布者负责生成无界异步事件,并将它们推送到相关的订阅者。它由以下org.reactivestreams.Publisher接口表示:

public interface Publisher<T> {
    public void subscribe(Subscriber<? super T> s);
}

该接口提供了一个单一的subscribe方法。该方法由任何有兴趣监听发布者发布的事件的任何一方调用。该接口非常简单,可以用来发布任何类型的事件,无论是 UI 事件(如鼠标点击)还是数据事件。

由于接口很简单,让我们为我们的自定义FibonacciPublisher添加一个实现:

public class FibonacciPublisher implements Publisher<Integer> {
    @Override
    public void subscribe(Subscriber<? super Integer> subscriber) {
        int count = 0, a = 0, b = 1;
        while (count < 50) {
            int sum = a + b;
            subscriber.onNext(b);
            a = b;
            b = sum;
            count++;
        }
        subscriber.onComplete();
    }
}

这种实现看起来可能不错,但它是否符合规范中规定的发布者行为?规范规定了描述发布者行为的规则。发布者必须生成以下四种类型的事件:

  • 订阅事件

  • 发布者声明的类型T的数据

  • 完成事件

  • 错误事件

根据规范,发布者可以发出任意数量的数据事件。然而,它必须只发布一个完成、错误和订阅事件。一旦发布完成或错误事件,发布者就不能再向订阅者发送数据事件。

由于背压是规范的重要方面,发布者不能向订阅者推送任意数量的事件。相反,订阅者必须指定它可以接收多少事件,发布者必须发布等于或小于指定数量的事件。

为了验证发布者,反应式流 API 已发布了一个测试兼容性套件。让我们将reactive-streams-tck添加到我们的build.gradle项目中:

dependencies {
  // rest removed for brevity
  testCompile group: 'org.reactivestreams',
  name: 'reactive-streams-tck', version: '1.0.2'
}

技术兼容性套件TCK)提供了一个必须实现的PublisherVerifier接口,以验证发布者。它提供了以下两个方法:

  • createPublisher(long): 此方法必须提供一个发布者实例,该实例可以产生指定的数量的事件

  • createFailedPublisher(): 此方法必须尝试构建一个已引发错误事件的发布者

让我们添加以下实现来测试我们的FibonacciPublisher

public class FibonacciPublisherVerifier extends PublisherVerification<Integer> {
    public FibonacciPublisherVerifier(){
        super(new TestEnvironment());
    }
    @Override
    public Publisher<Integer> createFailedPublisher() {
        return null;
    }
    @Override
    public Publisher<Integer> createPublisher(long elements) {
        return new FibonacciPublisher();
    }
}

现在,让我们运行测试用例以确定我们是否符合反应式流发布者规范:

如前一个屏幕截图所示,大约有 20 个测试失败和 16 个跳过的测试。我们可以修复每一个,但这里的目的是理解即使是一个简单的发布者接口也受到许多行为规范的约束。因此,构建自定义发布者是不必要的。作为服务构建者,我们可以使用 Reactor 框架。它提供了能够发布任何类型数据的发布者实现。

流订阅者

订阅者用于监听由发布者生成的事件。当订阅者向发布者注册时,它按以下顺序接收事件:

因此,订户有以下界面来处理所有这些事件:

public interface Subscriber<T> {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}

让我们详细说明以下每个方法:

  • onSubscribe(Subscription s): 一旦发布者收到订阅者,它就会生成一个订阅事件。然后,生成的订阅事件在指定方法中接收。

  • onNext (T): 所有由发布者生成并由订阅者在指定方法中接收的数据事件。发布者在关闭流之前可能发布也可能不发布数据事件。

  • onCompletion(): 这指的是完成事件,订阅者必须处理此事件。一旦收到完成事件,订阅就视为无效。

  • onError(): 这指的是错误事件,订阅者必须处理此事件。错误可能在任何时候发生——在构建订阅或生成下一个数据事件时。在任何情况下,发布者都必须发送错误事件。一旦收到事件,订阅就视为无效。

订阅

订阅是反应式流中的一个重要组成部分。它提供了必要的控制流,以确保发布者不会使订阅者过载。这被称为背压。

一旦订阅者收到订阅事件,它必须要求发布者在其各自的订阅中发布指定数量的事件。这是通过调用订阅对象的request(long)方法来完成的。

随着数据事件的生成,它们会被订阅者接收。一旦达到限制,发布者必须停止发布更多事件。随着订阅者处理这些事件,它必须从发布者请求更多事件:

public interface Subscription {
    public void request(long n);
    public void cancel();
}

订阅对象允许订阅者控制它想要接收的事件。每当订阅者确定它不再想要接收事件时,它可以调用订阅的cancel()方法。一旦调用,订阅者可能会接收更少的数据事件,这符合取消之前提出的需求。取消后,订阅将变为无效,这意味着它不能用来请求更多数据。

请求方法的Long.MaxValue值会导致发布者产生无限的事件流。

订阅者可以在使用请求方法提出任何需求之前,使用onSubscribe()方法取消一个活跃的订阅。在这种情况下,发布者将取消订阅而不会引发任何事件。

现在我们已经详细了解了订阅者接口,我们可以尝试构建一个FibonacciSubscriber,如下所示:

public class FibonacciSubscriber implements Subscriber<Long> {
    private Subscription sub;
    @Override
    public void onSubscribe(Subscription s) {
        sub = s;
        sub.request(10);
    }
    @Override
    public void onNext(Long fibNumber) {
        System.out.println(fibNumber);
        sub.cancel();
    }
    @Override
    public void onError(Throwable t) {
        t.printStackTrace();
        sub=null;
    }
    @Override
    public void onComplete() {
        System.out.println("Finished");
        sub=null;
    }
}

之前实现的代码执行以下操作:

  1. 在接收到订阅事件后,会发起一个请求来处理10个事件。

  2. 当接收到数据事件时,所有事件都会打印到输出控制台。

  3. 在处理单个事件之后,订阅者取消订阅。

  4. onCompletion方法将订阅设置为null

  5. onError方法将错误信息打印到控制台并将订阅设置为null

现在,让我们使用SubscriberBlackboxVerification<T>抽象类来验证订阅者。我们需要实现createSubscriber()方法,如下面的代码所示:

public class FibonacciSubsciberVerification extends SubscriberBlackboxVerification<Long> {
    public FibonacciSubsciberVerification(){
        super(new TestEnvironment());
    }
    @Override
    public Subscriber<Long> createSubscriber() {
        return new FibonacciSubscriber();
    }
    @Override
    public Long createElement(int element) {
        return new Long(element);
    }
}

让我们运行测试用例以确定我们的订阅者是否符合 Reactive Streams 标准:

图片

在这里,我们也可以找到大量的损坏测试用例。这些损坏的测试用例定义了订阅者的行为。我们可以修复这些用例,但更好的选择是使用 Reactor 来创建我们的服务。在下一节中,我们将描述 Reactor 中可用的发布者和订阅者实现。这些实现符合规范行为。

Reactive Streams 比较

在我们深入研究 Reactor 之前,让我们比较 Streams 模型与一些现有的类似 API,例如java.util.Observable接口和 JMS API。我们将尝试确定 API 之间的相似之处和关键差异。

Observable 接口

java.util.Observable接口实现了观察者模式,这在此处可以相关联。然而,所有相似之处到此为止。如果我们查看Observable接口,我们有以下方法:

public class Observable {
  void addObserver (Observer o);
  void deleteObserver (Observer o);
  void deleteObservers();
  void notifyObservers();
  void notifyObserver(int arg);
  int countObservers();
  boolean hasChanged();
}

在我们确定差异之前,让我们看看Observer接口:

public interface Observer{
  void update(Observable o, Object arg)
}

如果我们查看ObservableObserver接口,我们可以看到它们都是关于单个事件及其状态。Observable API 负责确定变化并将其发布给所有感兴趣的一方。另一方面,Observer只监听变化。这并不是我们用PublisherSubscriber接口建模的内容。Publisher接口负责生成无界事件,与只关注单一实体状态变化的Observable不同。另一方面,Subscriber列出了所有种类的事件,如数据、错误和完成。

此外,Observable维护一个活跃的观察者列表。它有责任移除不再对事件感兴趣的观察者。这不同于PublisherPublisher只负责订阅。Subscriber决定是否关闭订阅,由其自行决定。

Java 消息服务 API

让我们看看响应式流与Java 消息服务(JMS)API的比较。JMS 规范描述了一个Queue和一个Topic,生产者和消费者可以连接到这些队列或主题:

@Resource(lookup = "jms/Queue")
private static Queue queue;

@Resource(lookup = "jms/Topic")
private static Topic topic;
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(queue);
MessageConsumer consumer = session.createConsumer(topic)

在这里,生产者负责在队列或主题上生成无界事件,而消费者积极消费事件。生产者和消费者在各自的速率下独立工作。管理订阅的任务由 JMS 代理负责。这与订阅 API 不同,在事件生成中,背压起着重要作用。也没有像订阅、错误或完成这样的事件建模。JMS 连接就像一个永不结束的数据流。它不能提供完成或错误事件。如果我们需要支持这一点,必须首先建模自定义对象。

了解 Reactor 核心 API

Reactor 项目被划分为不同的模块。reactor-core 模块是核心库,旨在为响应式流提供实现。该库提供了 Flux 和 Mono,它们是Publisher接口的两种不同实现。这两个发布者在其可以发出的事件数量方面有所不同。Flux 可以发出无限序列的元素,但 Mono API 使得最多只能发出一个元素。让我们在接下来的章节中详细介绍这些 API。

Flux API

Flux<T>是一个通用响应式发布者。它表示一个异步事件流,包含零个或多个值,可选地由完成信号或错误终止。重要的是要注意,Flux 发出以下三个事件:

  • 指的是发布者生成的值

  • 完成指的是流的正常终止

  • 错误指的是流的错误终止:

图片

所有的前述事件都是可选的。这可能导致以下类型的流:

  • 无限流:仅生成值事件,没有终端事件(完成和错误)

  • 无限空流:一个不生成值事件和不终止事件的流

  • 有限流:生成 N 个有限值,然后是终端事件

  • 空流:一个不生成值事件,只生成终端事件的发布者

Flux 支持生成所有前面的变体,因此它可以用于大多数通用用例。它还可以为应用程序生成一系列警报。警报是一个无限值的流,没有终端。Flux 还可以用于从订单数据库流式传输订单数据。订单值在最后一个订单值处终止。可能存在没有特定产品类型的订单,使得该类型的流为空。

生成 Flux API

Flux<T> API 支持从各种来源生成流,例如单个值、集合、Java 8 流等。它还可以用于从自定义逻辑或现有的响应式发布者生成流。我们将在接下来的章节中详细讨论所有这些选项。

Flux.just 方法

这是生成 Flux 的最简单方法。它接受一组值,例如 var-args,并使用这些值生成一个有限的 Flux 流。指定的每个 var-args 值形成一个 Flux 的值事件。在发布所有指定的值之后,发布一个完成事件:

Flux.just("Red");
Flux.just("Red", "Blue", "Yellow", "Black");
Flux.just(new Person("Rahul"), new Person("Rudra"));

Flux.from 方法

From 方法可以用于从各种来源生成 Flux,例如数组、集合等。在这种情况下,所有值事先都被识别为多值数据集。生成的 Flux 为原始数据集中的每个值发布值事件,然后是一个完成事件。提供的方法有以下变体:

  • Flux.fromArray:这是用于从一个类型的数组构建流。

  • Flux.fromIterable:这是用于从集合构建流。所有集合都是 Iterable<T> 类型,可以传递给此方法以生成预期的流。

  • Flux.fromStream:这是用于从一个现有的 Java 8 流或 Java 8 流提供者构建 Flux。考虑以下代码:

Flux.fromArray(new Integer[]{1,1,2,3,5,8,13});
Flux.fromIterable(Arrays.asList("Red", "Blue", "Yellow", "Black"));
Flux.fromStream(IntStream.range(1,100).boxed());

工具方法

Flux 提供了生成无限流和空流的方法,或者将现有的响应式流发布者转换为 Flux。这些方法需要生成可以与其他流结合的流,如下所示:

  • Flux.empty:此方法生成一个没有值且只有完成的空流。

  • Flux.error:此方法生成一个没有值且只有指定错误的错误流。

  • Flux.never:此方法生成一个没有任何事件的流。它不生成任何类型的事件。

  • Flux.from:此方法接受一个现有的响应式发布者,并从中生成一个 Flux。

  • Flux.defer:此方法用于构建一个懒加载的响应式发布者。该方法接受一个 Java 8 供应商来实例化一个特定订阅的响应式流发布者。发布者实例仅在订阅者订阅 Flux 时生成。

Flux.generate 方法

Flux 支持程序化事件生成。在上一章中,我们使用了该 API 生成斐波那契事件。这是 API 的高级使用方法,涉及更多组件。我们将在以下部分中详细介绍这些内容。

SynchronousSink

该汇(sink)被绑定到发布者的订阅者。当订阅者请求数据时,通过消费者函数调用它。对于每次调用,汇(sink)可以一次生成最多一个值事件。在调用过程中,汇(sink)可以引发额外的 onCompletion 或错误事件。

重要的是要注意,由汇(sink)生成的事件在订阅者端是同步消费的。让我们回顾一下我们在上一章中编写的斐波那契(Fibonacci)测试:

Flux<Long> fibonacciGenerator = Flux.generate(
        () -> Tuples.<Long, Long>of(0L, 1L),
        (state, sink) -> {
            sink.next(state.getT1());
            System.out.println("generated "+state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
fibonacciGenerator.take(size).subscribe(t -> {
    System.out.println("consuming "+t);
    fibonacciSeries.add(t);
});

在汇(sink)中生成多于一个事件会导致 java.lang.IllegalStateException: More than one call to onNext

在生成和消费数字时,我们添加了额外的打印语句。让我们运行我们的测试以查看输出,如下所示:

图片

消费者和生产者语句以不同的方式生成。我们可以很容易地推断出每个数字在生成下一个数字之前被消费。Generate API 提供了多种变体,汇(sink)可以带或不带初始状态使用。在我们的 FibonacciGenerator 中,我们使用的是基于每个订阅者的初始化状态。可选地,我们还可以提供一个终端函数,该函数在事件流终止时被调用。这意味着它将在汇(sink)调用错误或完成事件之后发生。终端函数可以用于执行与状态相关的任何清理操作。

Flux.create

Flux.create 是另一种用于程序化生成事件的机制。它接受一个 FluxSink,该 FluxSink 能够生成任意数量的事件。与上一节中讨论的 Generate 方法相比,该 API 更加通用。FluxSink 能够异步生成事件。此外,它不考虑订阅取消或背压。这意味着即使订阅者已经取消了订阅,创建 API 也会继续生成事件。所有实现都必须监听 cancel 事件并显式启动流关闭。

关于背压,生产者持续生成事件,而不考虑订阅者的任何需求。如果订阅丢失,这些事件将被缓冲并默认丢弃。

要了解这两个的不同之处,让我们修改我们的 FibonacciGenerator 以使用 FluxSink。以下是一些关键差异的突出显示:

  • API 中没有初始种子状态

  • FluxSink 不论订阅状态如何,都会持续生成事件

  • 我们可以在汇集中生成任意数量的事件

  • 可以监听 OnDispose 事件来执行任何清理操作,或者停止发布事件

  • 所有生成的事件都会被缓冲,一旦取消订阅就会丢弃

需要注意的是,FluxSink 提供了生命周期回调方法,可以用来执行额外的清理操作,或者执行任何其他操作,如下所示:

  • OnCancel:当订阅被取消时,此方法被调用。

  • OnDispose:当由于取消、关闭或错误事件关闭订阅时,此方法被调用。

  • OnRequest:此方法使用订阅者指定的值调用。它可以用来构建拉数据模型。当方法被调用时,可以调用下一个方法来指定值的数量:

@Test
public void testFibonacciFluxSink() {
    Flux<Long> fibonacciGenerator = Flux.create(e -> {
        long current = 1, prev = 0;
        AtomicBoolean stop = new AtomicBoolean(false);
        e.onDispose(()->{
            stop.set(true);
            System.out.println("******* Stop Received ****** ");
        });
        while (current > 0) {
            e.next(current);
            System.out.println("generated " + current);
            long next = current + prev;
            prev = current;
            current = next;
        }
        e.complete();
    });
    List<Long> fibonacciSeries = new LinkedList<>();
    fibonacciGenerator.take(50).subscribe(t -> {
        System.out.println("consuming " + t);
        fibonacciSeries.add(t);
    });
    System.out.println(fibonacciSeries);
}

让我们检查生成的输出,如下所示:

Flux 还提供了一个 Push 方法。这与 create 方法类似,但错误和完成事件的调用过程不同。这些事件必须以同步方式,从单个线程生产者调用。

Mono API

现在我们已经介绍了 Flux API,让我们看看 Mono。它能够生成最多一个事件。这是 Flux 的一个特定用例,能够处理一个响应模型,例如数据聚合、HTTP 请求-响应、服务调用响应等。需要注意的是,Mono 会发出以下三个事件:

  • 指的是发布者生成的单个值

  • 完成指的是流的正常终止

  • 错误指的是流的错误终止

由于 Mono 是 Flux 的子集,它支持 Flux 操作符的子集。让我们看看如何构建一个 Mono。

生成一个 Mono

Mono<T> API 支持从各种单值源生成流,如单个值、方法调用、Java 8 供应函数等。它还可以用于从自定义逻辑或现有响应式发布者生成流。我们现在将详细讨论这些。

Mono.just 方法

Mono.just 方法是生成 Mono 最简单的方法。它接受一个值,并从中生成一个有限的 Mono 流。在发布指定的值之后,会发布一个完成事件:

Mono.just("Red");
Mono.justOrEmpty(value);
Mono.justOrEmpty(Optional.empty());

Mono.from 方法

当值可以从现有源确定时,使用 From 方法来构建 Flux。与 Flux 方法不同,其中源是多值的,Mono 的源是单值的。这些方法提供了以下变体:

  • fromCallable: 此方法生成一个包含一个值,随后是完成事件的 Mono。如果 Callable 返回多值数据集,如数组或集合,则完整的数据集将作为单个事件中的对象推送。

  • fromFuture: 此方法生成一个包含一个值,随后是完成事件的 Mono。

  • fromSupplier: 此方法生成一个包含一个值,随后是完成事件的 Mono。

  • fromRunnable: 此方法生成一个没有值,只包含完成事件的 Mono。这可以通过以下代码解释:

Mono.fromSupplier(() -> 1);
Mono.fromCallable(() -> new String[]{"color"}).subscribe(t -> System.out.println("received " + t));
Mono.fromRunnable(() -> System.out.println(" ")).subscribe(t -> System.out.println("received " + t), null, () -> System.out.println("Finished"));

工具方法

Mono 提供了生成空/错误流或将现有的响应式流发布者转换为 Mono 的方法。这些方法需要生成可以与其他流通过可用运算符组合的流,如下所示:

  • Mono.empty:生成一个没有值,只包含完成事件的流。

  • Mono.error:生成一个没有值,只包含指定错误的流。

  • Mono.never:生成一个没有任何事件的流。它不会生成任何类型的事件。

  • Mono.from: 从现有的流发布者生成 Mono 流。

  • Mono.defer: 此方法用于构建一个懒加载的响应式发布者。它还接受一个 Java 8 供应商来实例化特定订阅的响应式流发布者。发布者实例仅在订阅者对 Mono 进行订阅时生成。

需要注意的是,可以使用 Flux 源生成 Mono。在这种情况下,Mono 使用 Flux 发布的第一个事件,如下所示:

Mono.from(Flux.just("Red", "Blue", "Yellow", "Black")).subscribe(t -> System.out.println("received " + t))

**** Output ******
received Red

Process finished with exit code 0

Mono.create

除了 Flux.create 方法外,还有一个 Mono.create 方法。此方法提供了一个 MonoSink,可以用来生成值、完成或错误事件。与 Flux 方法不同,我们在生成 N 个事件,如果我们在 Mono 中生成更多事件,它们将被丢弃。也没有处理背压,因为只有一个事件。

API 不考虑订阅取消。这意味着即使订阅者已取消其订阅,创建方法仍然生成其事件。实现者必须注册自定义钩子以处理生命周期事件并执行流关闭。

构建 Flux 和 Mono 的订阅者

Reactor Flux 和 Mono 提供了广泛的订阅方法。响应式发布者引发四种类型的事件,即订阅、值、完成和错误。可以为每个事件注册单独的函数。我们还可以注册一个订阅者,而不监听任何类型的事件。让我们看看所有可能提供的变体,如下所示:

fibonacciGenerator.subscribe(); (1)

fibonacciGenerator.subscribe(t -> System.out.println("consuming " + t));   (2)

fibonacciGenerator.subscribe(t -> System.out.println("consuming " + t),
                e -> e.printStackTrace() ); (3)

fibonacciGenerator.subscribe(t -> System.out.println("consuming " + t),
                e -> e.printStackTrace(),
                ()-> System.out.println("Finished")); (4)

fibonacciGenerator.subscribe(t -> System.out.println("consuming " + t),
                e -> e.printStackTrace(),
                ()-> System.out.println("Finished"),
                s -> System.out.println("Subscribed :"+ s)); (5)

以下代码显示了所有的 Subscribe 方法:

  1. 如第 1 行所示,没有事件被消费。

  2. 只消费值事件,如第 2 行所示。

  3. 除了值事件外,我们还打印错误堆栈跟踪,如第 3 行所示。

  4. 我们可以监听值、错误和完成事件,如第 4 行所示。

  5. 我们可以监听值、错误、完成和订阅事件,如第 5 行所示。

所有 Subscribe 方法都返回 Disposable 类型。此类型也可以用于取消订阅。

有时,我们可能会认为 Subscribe 方法不够好。我们必须创建一个具有自己处理的自定义订阅者。Reactor 为这些情况提供了 reactor.core.publisher.BaseSubscriber<T>。而不是实现响应式流的 Subscriber,Reactor 建议实现 BaseSubscriber 抽象类:

BaseSubscriber<Long> fibonacciSubsciber= new BaseSubscriber<Long>() {
            @Override
            protected void hookOnSubscribe(Subscription subscription) { }

            @Override
            protected void hookOnNext(Long value) {}

            @Override
            protected void hookOnComplete() { }

            @Override
            protected void hookOnError(Throwable throwable) {}

            @Override
            protected void hookOnCancel() {}

        };

如果我们查看 BaseSubscriber 的实现,我们将看到以下内容:

  • 每个单独的事件都可以通过单独的钩子方法来处理。

  • 它捕获订阅并使其通过上游方法可访问。此方法可以在任何生命周期方法中调用。

  • 它还通过提供 request(long) 方法来处理背压。默认方法是逐个请求值。然而,订阅者可以通过使用 request 方法提出额外的需求。

  • 它还提供了 requestUnbound() 方法,该方法禁用背压。

一旦我们有了自定义订阅者,就可以使用 Flux 和 Mono 中的 subscribe() 方法来调用它。

生命周期钩子

发布者-订阅者通信在整个响应式流的生命周期中生成事件。Reactor 提供了相应的生命周期方法,可以用来将自定义逻辑钩接到每个事件上,如下表所示:

事件 方法
订阅事件 doOnSubscribe
请求事件,从订阅者请求 N 个项目 doOnRequest
值事件,对于所有生成的值 doOnNext
错误事件,由发布者产生的任何错误 doOnError
完成事件 doOnCompletion
取消事件,由订阅者取消 doOnCancel

除了前面提到的方法之外,还有以下方法:

  • doOnEach:此方法在流处理中引发的所有发布者事件上执行。

  • doOnTerminate:此方法在由于错误或完成而关闭流时执行。它不考虑取消。

  • doFinally:此方法在由于错误、完成或取消而关闭流时执行。

尝试一个动手项目

现在我们已经详细讨论了 Reactor 接口,让我们尝试使用 Reactor 生成一个阶乘序列。给定一个数字,我们希望生成小于或等于提供的数字的所有数字的阶乘。在数论中,阶乘被描述如下:

"正数 'n' 的阶乘定义为 n! = n(n-1)(n-2)...2.1 例如,5! = 5 × 4 × 3 × 2 × 1 = 120。"

现在,让我们尝试构建一个阶乘流函数,它接受一个数字并尝试为从 0 到 N 的每个数字生成阶乘:

public class FactorialService {

    Flux<Double> generateFactorial(long number) {
        Flux<Double> factorialStream = Flux.generate(
                () -> Tuples.<Long, Double>of(0L, 1.0d),
                (state, sink) -> {
                    Long factNumber = state.getT1();
                    Double factValue = state.getT2();
                    if (factNumber <= number)
                        sink.next(factValue);
                    else
                        sink.complete();
                    return Tuples.of(factNumber + 1, (factNumber + 1) * factValue);
                });
        return factorialStream;
    }
}

在前面的代码中,我们执行了以下操作:

  1. 初始的 factorialNumber 被设置为 0,阶乘为 1

  2. 我们检查了 factorialNumber 是否小于或等于传入的数字,并为它发布了阶乘值。

  3. 如果 factorialNumber 大于传入的数字,则发布完成信号。

  4. 我们增加了 factorialNumber 并计算了它的阶乘。

上述流程相当简单,但它使得利用各种 Flux 组件成为可能。由于阶乘服务已经就绪,我们需要通过订阅它来验证它。在下面的测试用例中,我们做以下事情:

  1. 调用生成阶乘数的生成器,直到 10

  2. 使用 doOnNext() 生命周期钩子显示每个生成的数字。

  3. 使用 last() 操作符来获取最后一个值。我们将在下一章介绍操作符。

  4. 在订阅者的值事件函数中比较和断言值:

public class FactorialServiceTest {

    @Test
    public void testFactorial() {
        Flux<Double> factorialGenerator = new FactorialService().generateFactorial(10);
        factorialGenerator
                .doOnNext(t -> System.out.println(t))
                .last()
                .subscribe(t -> assertEquals(3628800.0, t, 0.0));
    }
}

现在,让我们运行测试用例来查看输出:

摘要

在本章中,我们详细讨论了 Reactive Streams 的发布者和订阅者接口。我们尝试实现这些接口来展示它们有许多非显式规则。这些规则已被转换为 Reactive Streams TCK,所有实现都应通过它进行验证。我们还比较了发布者-订阅者模式与 Java 中使用的现有 Observer 和 JMS 模式。接下来,我们详细研究了 Reactor 中可用的 Flux 和 Mono 实现。我们研究了创建它们的方法,然后订阅了生成的流。

在下一章中,我们将探讨可以用来修改生成的流的操作符。

问题

  1. 我们如何验证 Reactive Stream 发布者和订阅者实现?

  2. Reactive Stream 发布者-订阅者模型与 JMS API 有何不同?

  3. Reactive Stream 发布者-订阅者模型与 Observer API 有何不同?

  4. Flux 和 Mono 之间有什么区别?

  5. SynchronousSinkFluxSink 之间有什么区别?

  6. Reactor 中有哪些不同的生命周期钩子可用?

进一步阅读

第三章:数据和流处理

在上一章中,我们通过使用 Reactor Flux 生成数据流,然后在订阅者中消费它。Reactor 还提供了一组多样的操作符,可以用来操作数据。这些操作符接收一个流作为输入,然后生成另一个类型的数据流。简而言之,这些操作符提供了一种强大的方式来组合可读的数据管道。有各种用于过滤、映射和收集数据的操作符。所有这些都会在本章中介绍。

本章将涵盖以下主题:

  • 过滤数据

  • 转换数据

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章的 GitHub 链接是 github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter03

生成数据

在我们深入研究各种操作符之前,让我们首先生成一个数据流。为了做到这一点,让我们回顾一下我们的斐波那契序列,来自 第一章,开始使用响应式流

在数论中,斐波那契数以这样一个事实为特征:从第一个两个数之后的每一个数都是前两个数的和(即,0 , 1 , 1 , 2 , 3 , 5 , 8 , 13 ,21 , 34 , 55 , 89 , 144,等等)。

Flux 生成的 API 使我们能够构建一个生成器。这些生成器从 0 和 1 开始序列。所有数字都由订阅者打印到控制台,该订阅者监听所有生成的事件。这在上面的代码中有显示:

Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long, Long>of(0L, 1L),(state, sink) -> {
  if (state.getT1() < 0)
     sink.complete();
  else  
     sink.next(state.getT1());
  return Tuples.of(state.getT2(), state.getT1() + state.getT2());
});
fibonacciGenerator.subscribe(t -> {
  System.out.println(t);
});

让我们回顾一下这里正在发生的事情,如下所示:

  • 我们通过使用 Flux.generate() 调用创建斐波那契序列作为 Flux<Long>。API 有状态和汇。

  • API 接收一个种子作为 Tuple [0 , 1]。然后通过调用 Sink.next() 来发出对的数据。

  • API 还通过聚合对来生成下一个斐波那契数。

  • 当我们生成负数时,发布者将流标记为完成。这是由于它们超出了 long 数据类型的范围。

  • 我们订阅发布的数字,然后将接收到的数字打印到控制台。这在上面的屏幕截图中有显示:

图片

过滤数据

让我们从选择数据的简单操作符开始。数据过滤有不同类型的类比,如下所示:

  • 根据给定条件选择或拒绝数据

  • 选择或拒绝生成数据的一个子集

以下图显示了前面的信息:

图片

filter() 操作符

filter()操作符允许根据传递的条件选择数据。API 接受一个布尔谓词,该谓词对每个发出的值进行评估,以确定是否被选中。过滤操作相当常见。假设我们想根据月份范围选择日期,或者根据员工 ID 选择员工数据。在这些情况下,传递给filter的布尔谓词包含选择逻辑。这可以非常灵活,并且可以适应不同的需求。

让我们将我们的斐波那契生成器扩展到只选择偶数,如下所示:

fibonacciGenerator.filter(a -> a%2 == 0).subscribe(t -> {
    System.out.println(t);
});

在前面的代码中,谓词执行可除性检查。如果数字能被2整除,操作符将以同步方式执行谓词评估。如果条件得到满足,值将被传递给订阅者。

此外,还有一个FilterWhen,它是一种异步的布尔评估方式。它接受输入值并返回布尔发布者。这可以通过以下代码解释:

fibonacciGenerator.filterWhen(a -> Mono.just(a < 10)).subscribe(t -> {
    System.out.println(t);
});

在前面的代码中,谓词执行小于检查。这是一个延迟评估,结果以Mono<Boolean>的形式返回。

取操作符

之前讨论的过滤方法使我们能够选择数据。如果我们想选择前 10 个元素,例如,我们可以使用filter操作符,其中包含一个计数器谓词。或者,还有一个take操作符用于此目的。该操作符接受一个数字并选择指定数量的元素,如下所示:

fibonacciGenerator.take(10).subscribe(t -> {
    System.out.println(t);
});

前面的代码将选择前10个值来形成斐波那契生成器。

现在,假设我们只想选择最后 10 个元素。takeLast操作符就是为了这个目的而设计的。它也维护一个计数并从序列的末尾选择元素:

fibonacciGenerator.takeLast(10).subscribe(t -> {
    System.out.println(t);
});

如果流确实是无界的,则不会有任何最后一个元素。该操作符仅在流正常关闭时才起作用。

如果我们只想选择最后一个值,我们可以使用takeLast(1)操作符。此操作符将返回一个只包含一个值的 Flux 流。或者,还有一个last()操作符,它返回一个包含最后一个发布元素的 Mono 发布者。last操作符的使用如下所示:

fibonacciGenerator.last().subscribe(t -> {
    System.out.println(t);
});

跳过操作符

现在我们已经找到了选择数据的方法,让我们看看拒绝数据的方法。Reactor API 提供了多种拒绝数据的方法。有一个跳过操作符,有以下类型:

  • Skip(count): 这将从流的开始处拒绝指定数量的元素。

  • Skip(Duration): 这将从流的开始处拒绝指定持续时间的元素。

  • SkipLast(count): 这将从流的末尾拒绝指定数量的元素。

  • SkipUntil(Boolean Predicate): 这将拒绝元素,直到满足所述条件的第一种情况为真。

前面的命令在以下代码中显示:

fibonacciGenerator.skip(10).subscribe(t -> {
    System.out.println(t);
});
fibonacciGenerator.skip(Duration.ofMillis(10)).subscribe(t -> {
    System.out.println(t);
});
fibonacciGenerator.skipUntil(t -> t > 100).subscribe(t -> {
    System.out.println(t);
});

上述代码示例有以下变体:

  • 第一个订阅者拒绝前10个元素,并打印其余元素

  • 第二个订阅者打印拒绝10毫秒内的元素之后的元素

  • 第二个订阅者打印第一个数据元素超过100之后的元素

下面的屏幕截图显示了输出:

图片

到目前为止,我们已经讨论了选择和拒绝数据的通用方法。然而,Flux 接口为特定场景下的数据过滤提供了以下特殊操作符:

  • distinct: 此操作符用于选择传递的数据流中的唯一元素

  • distinctUntilChanged: 此操作符用于选择第一个唯一元素集

  • ignoreElements: 此操作符用于完全忽略数据元素

  • single: 此操作符用于选择单个数据元素

  • elementAt: 此操作符选择流中指定索引处的元素

在上一节中,我们讨论了选择或拒绝数据的方法。Reactor 提供了许多用于此目的的操作符。通常,检查 API 并确定是否存在用于预期目的的操作符,而不是使用过滤和跳过方法自定义谓词,是一个好主意。

转换数据

通常需要将数据从一种格式转换为另一种格式。Reactor 提供了一套庞大的操作符来实现这一点。我们不仅可以转换数据,还可以修改数据元素的数量。

map()操作符

从之前用于解释skip()操作符的斐波那契示例中,假设我们想要将前 10 个元素转换为罗马数字等价物。

罗马数字由七个字母表示:I、V、X、L、C、D 和 M。这些字母分别代表 1、5、10、50、100、500 和 1,000。这七个字母可以组合起来表示成千上万的数字。罗马数字方案使用字母作为计数标记。标记组合起来表示单位值。

我们有一个长数字,我们想要将其转换为罗马数字等价物;这就是map()操作符有价值的地方。它将转换应用于现有流中的每个值,如下所示:

图片

为了实现这种转换,我们需要一个RomanNumberConvertor。在下面的代码中,我们定义了从整数到其罗马数字等价物的转换:

class RomanNumber {
    TreeMap<Integer, String> romanMap= new TreeMap<>();
    RomanNumber(){
        romanMap.put(1000, "M");
        romanMap.put(900, "CM");
        romanMap.put(500, "D");
        romanMap.put(400, "CD");
        romanMap.put(100, "C");
        romanMap.put(90, "XC");
        romanMap.put(50, "L");
        romanMap.put(40, "XL");
        romanMap.put(10, "X");
        romanMap.put(9, "IX");
        romanMap.put(5, "V");
        romanMap.put(4, "IV");
        romanMap.put(1, "I");
    }
    String toRomanNumeral(int number) {
        int l =  romanMap.floorKey(number);
        if ( number == l ) {
            return romanMap.get(number);
        }
        return romanMap.get(l) + toRomanNumeral(number-l);
    }
}

由于我们知道如何转换整数,我们将为我们的流处理器定义Map函数。该操作符将长Value作为输入,然后生成字符串形式的罗马数字等价物:

RomanNumber numberConvertor= new RomanNumber();
fibonacciGenerator.skip(1).take(10).map(t-> numberConvertor.toRomanNumeral(t.intValue())).subscribe(t -> {
    System.out.println(t);
});

在前面的代码中做了几件事情,如下所示:

  • 使用了skip(1)操作符。在上一个章节中,我们提到这将跳过序列的第一个元素。这是因为 0 没有罗马数字等价物。

  • 使用了take(10)操作符。这将只从生成的序列中选择 10 个元素。这样做是为了将数字限制在 1,000 以下。

  • map()操作符定义了longValue到罗马等效字符串的转换。

  • 所有的前面操作符都被链接在一起以生成一个单一的流。输出如下面的截图所示:

如前所述的输出所示,数值从数字到罗马数字的转换应用于流中流动的每个项目。

flatMap操作符

前面的使用map()操作符的转换示例在有一对一值转换时是有效的,但它无法处理一对-n值转换。我们可以通过生成斐波那契数的因子流来展示这个前提。让我们首先回顾一下因式分解是什么。

在数论中,因式分解是将合数分解为较小数字的乘积。例如,对于 6,因数是 1、2、3 和 6。

让我们尝试将斐波那契数转换为它们对应的因子。序列中的每个数字都必须转换为所有可能的因子。首先,让我们构建一个简单的函数来计算因子:

class Factorization {
    Collection<Integer> findfactor(int number) {
        ArrayList<Integer> factors= new ArrayList<>();
        for (int i = 1; i <= number; i++) {
            if (number % i == 0) {
                factors.add(i);
            }
        }
        return factors;
    }
}

在前面的代码中,我们使用了暴力方法,该方法将指定的数字除以所有小于或等于该数字的数字。如果数字可以整除,则除数将被添加到因数列表中。我们可以使用map操作符来实现这一点,如下面的代码所示:

fibonacciGenerator.skip(1).take(10).map(t-> numberConvertor.findfactor(t.intValue())).subscribe(t -> {
    System.out.println(t);
});

生成的输出包括包含斐波那契数因子的单独集合:

为了使生成的因子成为整数因子的流,我们必须使用flatMap操作符。这如下面的代码所示:

Factorization numberConvertor= new Factorization();
fibonacciGenerator.skip(1).take(10).flatMap(t-> Flux.fromIterable(numberConvertor.findfactor(t.intValue()))).subscribe(t -> {
   System.out.println(t);
});

在前面的代码中,以下内容被处理:

  • flatMap接受一个整数并将其传递给因子生成器。它期望一个其他数据类型的发布者。

  • 因子作为整数集合生成。

  • 这些整数被转换为 Flux,使用fromIterable方法来匹配FlatMap方法的期望。

前面的代码生成了以下输出:

当使用flatMap时,了解我们正在生成哪种类型的 Flux 至关重要。将Flux.fromIterable简单地更改为Flux.just会改变前面代码的完整行为。

重放操作符

Reactor 提供了一个操作符来重放数据流。repeat操作符是为了这个目的而设计的。它在接收到完成事件时重放流。假设我们想要输出斐波那契数列两次。我们将使用repeat()操作符,将2作为repeat()操作符的参数:

fibonacciGenerator.take(10).repeat(2).subscribe(t -> {
    System.out.println(t);
}); 

上述代码生成了两次流,如下所示输出。需要注意的是,repeat() 操作符在接收到完成事件后重复流:

图片

Reactor 还使得执行无限重复成为可能。repeat() 操作符在没有任何参数的情况下,无限次数地回放流:

图片

还有一个谓词变体,其中将布尔提供者传递给重复操作符。在完成时,提供者每次都会被评估,以确定流是否需要重复。

collect 操作符

Reactor 还提供了使数据流作为集合累积的算子。其中最基本的是 collectList() 操作符。该操作符将数据累积为列表,如下所示图示:

图片

让我们以斐波那契为例,将数据收集到列表中。收集器方法提供了一个 Mono 发布者,它将发出一个包含所有已发布数据的单个列表:

public void testFibonacciCollect() {
    Flux<Long> fibonacciGenerator = Flux.generate(
            () -> Tuples.<Long, Long>of(0L, 1L),
            (state, sink) -> {
                sink.next(state.getT1());
                return Tuples.of(state.getT2(), state.getT1() + state.getT2());
            });
      fibonacciGenerator.take(10).collectList().subscribe(t -> {
         System.out.println(t);
      });
}

上述代码执行以下操作:

  • take 操作符选择流中的前 10 个元素

  • 然后将它们累积到列表中,返回一个 Mono 发布者

  • 列表提供给订阅者,它将其打印到控制台

如下所示输出确认了该行为:

图片

collectList() 操作符将数据聚合到列表中,但还有一个 CollectSortList 操作符,可以根据数据的自然顺序收集数据到排序列表中。我们还可以向 CollectSortedList 方法提供一个比较器来改变数据的顺序,如下所示代码:

fibonacciGenerator.take(10).
collectSortedList((x,y)-> -1*Long.compare(x,y))
.subscribe(t -> {
   System.out.println(t);
});

上述代码执行以下操作:

  • take 操作符选择流中的前 10 个元素

  • 然后使用传递的比较函数将它们累积到 SortedList 中,返回一个 Mono 发布者

  • 比较函数比较两个长数据类型并反转评估

  • 列表提供给订阅者,它将其打印到控制台

在这里,订阅者以数据的逆序接收列表:

图片

collectMap 操作符

就像 collectlist() 一样,Reactor 还提供了 collectMap() 来将数据累积到 java.util.Map 中;collectMap 接受一个键生成函数来为生成的值元素创建键。如下所示代码:

fibonacciGenerator.take(10)
.collectMap(t -> t%2==0 ? "even": "odd")
.subscribe(t -> {
   System.out.println(t);
});

上述代码生成一个包含两个键的 Map,键表示为 evenodd。它将保留地图中的最后一个偶数/奇数。如下所示:

图片

collectMap 命令不仅接受一个 keyGenerator,还提供了传递一个值生成器的选项。值生成器改变了数据流的原值。

此外,还有一个 CollectMultiMap() 方法,它将数据收集到一个键的映射中,并将列表作为值列出。它不会覆盖原始值,而是将具有相同键的值聚合到一个列表中。如果使用 collectMultiMap 运算符执行,前面的代码将产生以下输出:

图片

除了之前讨论的累加器之外,还有一个通用的 Collect 运算符,它使得将数据累积到任何格式成为可能。此运算符将 Flux 发布者转换回 Mono 发布者,并发出一个累积的单个值。

reduce 运算符

前一节介绍了值累积,而 reduce 操作则围绕值合并。reduce 方法使得将完整的数据流聚合为单个值成为可能。如下所示:

图片

假设我们想要生成斐波那契数的总和,如下所示:

fibonacciGenerator.take(10).reduce((x,y) -> x+y).subscribe(t -> {
    System.out.println(t);
});

在前面的代码中,我们做了以下事情:

  • take 运算符选择了流中的前 10 个元素。

  • reduce 运算符接受一个长类型的 Bifunction。lambda 表达式返回长值的总和以生成回总和。

  • subscribe 操作接收一个 Mono<Long>,并在控制台上打印。如下所示:

图片

此外,还有一个重载的 reduce 方法,它可以将一个初始值作为聚合的起始点。

有一个特殊的 count 运算符,负责返回流的大小。

条件测试

到目前为止,我们讨论了在原始数据上工作的运算符。Reactor 框架提供了布尔运算符,可以测试流中的每个数据元素。有两种类型的运算符,如下所示:

  • all:此运算符接受一个谓词,并确认所有元素是否满足指定的标准。这是所有数据元素的逻辑 AND 运算符。

  • any:此运算符接受一个谓词,并确认是否有任何单个元素满足指定的标准。这是所有数据元素的逻辑 OR

前面方法的输出被合并为一个单个布尔结果,如下所示:

fibonacciGenerator.take(10).all(x -> x > 0).subscribe(t -> {
    System.out.println(t);
});

在前面的代码中,我们做了以下事情:

  • take 运算符选择了流中的前 10 个元素。

  • all 运算符接受一个布尔谓词以确认所有元素都大于 0

  • subscribe 操作接收一个 Mono<Boolean>,并在控制台上打印。

输出如下:

图片

添加数据

到目前为止,我们一直在处理来自单个 Flux 流的生成数据。流处理不仅限于一个发布者。Reactor 提供了操作符,使得将不同的发布者合并成一个单一的数据流成为可能。值可以添加到指定的发布值之前或之后。

concatWith 操作符

concatWith操作符使得在发布值之后附加一个值事件成为可能。它接受一个发布者作为输入,并在第一个发布者完成后,即10个元素之后,附加发布值,如下面的图所示:

图片

假设我们想在 Fibonacci 流末尾附加一些负值:

fibonacciGenerator.take(10)
 .concatWith(Flux.just( new Long[]{-1L,-2L,-3L,-4L}))
 .subscribe(t -> {
    System.out.println(t);
});

在前面的代码中,我们做了以下几件事:

  • take操作符选择了流的前10个元素。

  • concatWith操作符接受一个发布者。它在原始流完成后附加其值,即10个元素之后。

  • subscribe操作接收了一个Flux<Long>,它在控制台上打印出来。

concatWith类似,还有一个startWith操作符,可以用于在原始流值之前添加值。

摘要

在本章中,我们讨论了在 Reactor 中可用的各种操作符。我们首先查看用于选择和拒绝数据的简单操作符。然后,我们查看将数据转换为其他类型的操作符。转换后的数据元素不需要一对一映射。对于每个处理值,可以有多个元素。接下来,我们查看累积数据的操作符。到本章结束时,我们已经涵盖了数据的聚合和条件测试。简而言之,我们已经涵盖了 Reactor 中可用的所有操作符范围。在下一章中,我们将探讨处理器,它们提供了绑定 Reactor 组件所需的粘合剂。

问题

  1. 用于从流中选择数据元素的操作符是什么?

  2. 用于从流中拒绝数据元素的操作符是什么?

  3. Reactor 提供了哪些数据转换操作符?这些操作符彼此之间有何不同?

  4. 我们如何使用 Reactor 操作符执行数据聚合?

  5. Reactor 提供了哪些条件操作符?

第四章:处理器

到目前为止,在这本书中,我们一直在介绍响应式流的构建块。发布者、订阅者和操作员代表数据操作组件。另一方面,处理器代表将这些组件组合成单个数据流所需的管道。在本章中,我们将详细讨论处理器的类型和需求。

本章将涵盖以下主题:

  • 处理器简介

  • 理解处理器类型

  • 热发布者与冷发布者

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章的 GitHub 链接为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter04

处理器简介

处理器代表数据处理的状态。因此,它既被表示为发布者,也被表示为订阅者。由于它是一个发布者,我们可以创建一个处理器并对其Subscribe。大多数发布者的功能都可以通过处理器执行;它可以注入自定义数据,以及生成错误和完成事件。我们还可以与它接口的所有操作符。

考虑以下代码:

DirectProcessor<Long> data = DirectProcessor.create();
data.take(2).subscribe(t -> System.out.println(t));
data.onNext(10L);
data.onNext(11L);
data.onNext(12L);

在前面的代码中,我们做了以下几件事:

  1. 我们添加了一个DirectProcessor实例

  2. 在第二行,我们添加了take操作符,以选择两个元素

  3. 我们还在控制台上订阅并打印了数据

  4. 在最后三行中,我们发布了三个数据元素

让我们看看输出结果,如下面的屏幕截图所示:

在这里,看起来我们可以用处理器替换发布者。但如果是这种情况,为什么响应式流规范要求为同一功能提供两个接口?嗯,发布者和处理器实际上并不相同。处理器是具有有限能力的特殊类发布者。它们代表数据处理的一个阶段。当我们讨论不同类型的可用处理器时,我们将熟悉这些限制。

作为一般规则,我们应该尽量避免直接使用处理器。相反,我们应该尝试寻找以下替代方案:

  • 首先,确定一个可以提供所需功能的现有操作符。操作符应该是执行数据操作的首选。

  • 如果没有可用的操作符,我们应该尝试适配Flux.generate API 并生成一个自定义的数据流。

理解处理器类型

Reactor 中提供了不同类型的处理器。这些处理器在各种特性上有所不同,例如背压能力、它们可以处理的客户端数量、同步调用等。让我们看看 Reactor 中可用的处理器类型。

DirectProcessor 类型

DirectProcessor 是处理器中最简单的一种。此处理器将处理器连接到订阅者,然后直接调用 Subscriber.onNext 方法。处理器不提供任何背压处理。

可以通过调用 create() 方法创建一个 DirectProcessor 实例。任何数量的订阅者都可以订阅处理器。必须注意,一旦处理器发布了完整的事件,它将拒绝后续的数据事件。

考虑以下代码:

DirectProcessor<Long> data = DirectProcessor.create();
data.subscribe(t -> System.out.println(t),
        e -> e.printStackTrace(),
        () -> System.out.println("Finished 1"));
data.onNext(10L);
data.onComplete();
data.subscribe(t -> System.out.println(t),
        e -> e.printStackTrace(),
        () -> System.out.println("Finished 2"));
data.onNext(12L);

上述代码执行以下操作:

  1. 创建了一个 Directprocessor 实例

  2. 添加一个订阅者,该订阅者可以将所有事件(数据/错误/完成)打印到控制台

  3. 发布一个数据事件,随后是一个完成事件

  4. 添加了另一个订阅者,该订阅者可以将所有事件(数据/错误/完成)打印到控制台

  5. 发布一个数据事件。

当我们查看以下输出截图时,我们可以看到所有订阅者都收到了完成事件。值为 12 的数据事件随后被丢弃:

在处理背压方面,DirectProcessor 有另一个重要的限制。我们之前提到,它根本不具备背压能力。这意味着如果我们推送的事件超过了订阅者请求的数量,会导致 Overflow 异常。

让我们看看以下代码:

DirectProcessor<Long> data = DirectProcessor.create();
data.subscribe(t -> System.out.println(t),
        e -> e.printStackTrace(),
        () -> System.out.println("Finished"),
        s -> s.request(1));
data.onNext(10L);
data.onNext(11L);
data.onNext(12L);

上述代码执行以下操作:

  1. 创建了一个 Directprocessor 实例

  2. 添加了一个订阅者,该订阅者可以将所有事件(数据/错误/完成)打印到控制台

  3. 订阅者还监听了订阅事件,并提出了 1 个数据事件的请求

  4. 最后,发布了几个数据事件

上述代码失败,显示以下错误:

UnicastProcessor 类型

UnicastProcessor 类型与 DirectProcessor 类似,在调用 Subscriber.onNext 方法方面。它直接调用订阅者方法。然而,与 DirectProcessor 不同,UnicastProcessor 具备背压能力。内部,它创建一个队列来存储未交付的事件。我们还可以提供一个可选的外部队列来缓冲事件。当缓冲区满时,处理器开始拒绝元素。处理器还使得对每个被拒绝的元素执行清理成为可能。

UnicastProcessor 提供了 create 方法来构建处理器的一个实例。让我们看看以下代码,看看它是如何使用的:

UnicastProcessor<Long> data = UnicastProcessor.create();
data.subscribe(t -> {
    System.out.println(t);
});
data.sink().next(10L);

上述代码执行了以下操作:

  1. 创建了一个 UnicastProcessor 实例

  2. 添加了一个订阅者,该订阅者可以将数据事件打印到控制台

  3. 创建了一个 sink 来推送几个元素

虽然 UnicastProcessor 提供了背压能力,但一个主要的限制是只能与单个订阅者一起工作。如果我们向上述代码添加另一个订阅者,它将失败并显示以下错误:

每个处理器都提供了一个Sink方法。Sink是向订阅者发布事件的推荐方式。它提供了发布下一个、错误和完成事件的机制。Sink提供了一种线程安全的方式来处理这些事件,而不是直接通过调用Subscriber.OnNext方法来发布它们。

EmitterProcessor类型

EmitterProcessor是一个可以与多个订阅者一起使用的处理器。多个订阅者可以根据它们各自的消费速率请求下一个值事件。处理器为每个订阅者提供必要的背压支持。这在上面的图中表示:

处理器还可以从外部发布者发布事件。它从注入的发布者那里消费一个事件,并将其同步传递给订阅者。

让我们看看以下代码:

EmitterProcessor<Long> data = EmitterProcessor.create(1);
data.subscribe(t -> System.out.println(t));
FluxSink<Long> sink = data.sink();
sink.next(10L);
sink.next(11L);
sink.next(12L);
data.subscribe(t -> System.out.println(t));
sink.next(13L);
sink.next(14L);
sink.next(15L);

上述代码执行了以下操作:

  1. 创建了一个EmitterProcessor的实例

  2. 添加了一个订阅者,可以将数据事件打印到控制台

  3. 创建了一个sink来推送一些元素

  4. 添加了另一个订阅者,可以将数据事件打印到控制台

  5. 通过使用 Sink API 推送了更多的事件

上述代码生成了以下输出:

上述代码还清楚地说明了以下内容:

  • 两个订阅者都将项目打印到控制台。

  • 处理器在订阅后将其事件传递给订阅者。这与 Flux 不同,Flux 无论何时订阅都会将所有项目传递给所有订阅者。

ReplayProcessor类型

ReplayProcessor是一个专用处理器,能够缓存并回放事件给其订阅者。处理器还具有从外部发布者发布事件的能力。它从注入的发布者那里消费一个事件,并将其同步传递给订阅者。

ReplayProcessor可以缓存以下场景的事件:

  • 所有事件

  • 有限的事件计数

  • 由指定的时间段限制的事件

  • 由计数和指定的时间段限制的事件

  • 仅最后的事件

一旦缓存,所有事件在添加订阅者时都会回放:

让我们看看以下代码:

ReplayProcessor<Long> data = ReplayProcessor.create(3);
data.subscribe(t -> System.out.println(t));
FluxSink<Long> sink = data.sink();
sink.next(10L);
sink.next(11L);
sink.next(12L);
sink.next(13L);
sink.next(14L);
data.subscribe(t -> System.out.println(t));

上述代码执行了以下操作:

  1. 创建了一个具有三个事件缓存的ReplayProcessor实例

  2. 添加了一个订阅者,可以将数据事件打印到控制台

  3. 创建了一个sink来推送一些元素

  4. 添加了另一个订阅者,可以将数据事件打印到控制台

上述代码生成了以下输出:

  • 处理器缓存了最后三个事件,即121314

  • 当第二个订阅者连接时,它会在控制台上打印缓存的事件

输出的截图如下:

TopicProcessor类型

TopicProcessor是一个能够使用事件循环架构与多个订阅者一起工作的处理器。处理器以异步方式将发布者的事件发送到附加的订阅者,并通过使用RingBuffer数据结构为每个订阅者提供背压。处理器还能够监听来自多个发布者的事件。这在上面的图中有所说明:

图片

与按顺序发送事件的处理器不同,TopicProcessor能够以并发方式向订阅者发送事件。这由处理器中创建的线程数控制。

让我们看看以下代码:

TopicProcessor<Long> data = TopicProcessor.<Long>builder()
        .executor(Executors.newFixedThreadPool(2)).build();
data.subscribe(t -> System.out.println(t));
data.subscribe(t -> System.out.println(t));
FluxSink<Long> sink= data.sink();
sink.next(10L);
sink.next(11L);
sink.next(12L);

上述代码执行了以下操作:

  1. 使用提供的构建器创建了一个TopicProcessor实例。

  2. 提供了一个大小为2ThreadPool,以便将其连接到两个订阅者

  3. 添加了两个订阅者实例,它们可以将数据事件打印到控制台

  4. 创建了一个sink来推送几个元素。

上述代码生成了以下输出,处理器并发地向两个订阅者发送事件:

图片

WorkQueueProcessor类型

WorkQueueProcessor类型与TopicProcessor类似,因为它可以连接到多个订阅者。然而,它不会将所有事件都发送给每个订阅者。每个订阅者的需求被添加到一个队列中,发布者的事件被发送给任何一个订阅者。这种模式更像是 JMS 队列上的监听器;每个监听器在完成时消费一条消息。处理器以轮询的方式将消息发送给每个订阅者。处理器还能够监听来自多个发布者的事件。这在上面的图中有所展示:

图片

在资源需求方面,处理器表现得更好,因为它不会为它的订阅者构建线程池。

让我们看看以下代码:

WorkQueueProcessor<Long> data = WorkQueueProcessor.<Long>builder().build();
data.subscribe(t -> System.out.println("1\. "+t));
data.subscribe(t -> System.out.println("2\. "+t));
FluxSink<Long> sink= data.sink();
sink.next(10L);
sink.next(11L);
sink.next(12L);

上述代码执行了以下操作:

  1. 使用提供的构建器创建了一个WorkQueueProcessor实例。

  2. 添加了两个订阅者实例,它们可以将数据事件打印到控制台。每个订阅者也会打印其 ID。

  3. 创建了一个sink来推送几个元素。

上述代码生成了以下输出。处理器向第一个订阅者发送了一些事件,其余的事件发送给了第二个订阅者:

图片

热发布者与冷发布者

在前面的章节中,我们构建了发布者,它们在订阅后会将数据发布到每个订阅实例。我们在第二章,“Reactors 中的发布者和订阅者 API”中创建的斐波那契发布者会将完整的斐波那契数列发布给每个订阅者。

将以下斐波那契代码视为冷发布者:

Flux<Long> fibonacciGenerator = Flux.generate(
        () -> Tuples.<Long, Long>of(0L, 1L),
        (state, sink) -> {
            sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });

fibonacciGenerator.take(5).subscribe(t -> System.out.println("1\. "+t));
fibonacciGenerator.take(5).subscribe(t -> System.out.println("2\. "+t));

在订阅后开始向订阅者发布数据的发布者被称为冷发布者。重要的是要理解数据应在订阅后生成。如果没有订阅者,则发布者不会生成任何数据。

让我们看看前面提到的冷发布者的输出。在这里,两个订阅者都打印了完整的斐波那契数列集合:

图片

在本章中,我们使用了作为发布者的处理器,这些处理器不依赖于订阅者。这些发布者持续发射数据,并且当新的订阅者到达时,它只会接收新发射的数据。这与冷发布者的行为不同,冷发布者也会为每个新的订阅者发布旧数据。这些发布者被称为热发布者

在以下代码中,我们将斐波那契发布者转换为热发布者:

final UnicastProcessor<Long> hotSource = UnicastProcessor.create();
final Flux<Long> hotFlux = hotSource.publish().autoConnect();
hotFlux.take(5).subscribe(t -> System.out.println("1\. " + t));
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
    int c1 = 0, c2 = 1;
    while (c1 < 1000) {
        hotSource.onNext(Long.valueOf(c1));
        int sum = c1 + c2;
        c1 = c2;
        c2 = sum;
        if(c1 == 144) {
            hotFlux.subscribe(t -> System.out.println("2\. " + t));
        }
    }
    hotSource.onComplete();
    latch.countDown();
}).start();
latch.await();

以下代码说明了以下内容:

  1. 我们构建了一个UnicastProcessor,并通过使用publish方法将其转换为Flux

  2. 然后我们向它添加了一个订阅者

  3. 接下来,我们创建了一个Thread,并使用之前创建的UnicastProcessor实例来生成斐波那契数列

  4. 当数列值为144时,添加了另一个订阅者

让我们看看热发布者的输出:

  • 第一个订阅者打印初始值。

  • 第二个订阅者打印大于143的值。这在下图中显示:

图片

摘要

在本章中,我们探讨了 Reactor 中可用的各种处理器。你了解到DirectProcessor是最简单的处理器,但它不提供背压。然后我们讨论了UnicastProcessorEmitterProcessorReplayProcessorTopicProcessorWorkQueueProcessor的功能和能力。最后,我们熟悉了热发布者和冷发布者,最终使用UnicastProcessor将斐波那契生成器转换为热发布者。

问题

  1. DirectProcessor的限制是什么?

  2. UnicastProcessor的限制是什么?

  3. EmitterProcessor的功能有哪些?

  4. ReplayProcessor的功能有哪些?

  5. TopicProcessor的功能有哪些?

  6. WorkQueueProcessor的功能有哪些?

  7. 热发布者和冷发布者之间的区别是什么?

第五章:SpringWebFlux for Microservices

到目前为止,我们已经讨论了 Reactor 作为独立框架。我们还看到了如何构建发布者并订阅它们。Reactor 非常适合处理大量数据的交换,但需要注意的是,Reactor 不仅限于独立编程;它还可以用于构建 Web 应用程序。

传统上,我们使用 SpringMVC 框架构建企业级 Web 应用程序,SpringMVC 是 Spring 生态系统中的一个同步和阻塞框架。SpringMVC 还可以使用 Servlet 3.1 提供异步非阻塞数据,但那时它就远离了请求映射器和过滤器的概念。这使得框架相当难以使用。此外,在构建高性能的微服务架构时,该框架可能不是最佳选择。在这种架构中,我们希望拥有独立、可扩展和有弹性的服务。SpringMVC 并没有定义这些特性中的任何一项。如 第一章 所述,“开始使用反应式流”,之前讨论的非功能性需求是反应式宣言的特性。

注意到这个差距,Spring 社区提出了 SpringWebFlux 框架。此框架基于 Reactor,并能够创建基于 Web 的微服务。SpringWebFlux 不仅是非阻塞的,它还是一个函数式框架,允许我们使用 Java 8 lambda 函数作为 Web 端点。该框架为非阻塞 Web 栈提供了一套完整的解决方案。

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE 2018.1 或更高版本

本章的 GitHub 链接为 github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter05

SpringWebFlux 简介

为了使我们能够构建基于 Web 的服务,SpringWebFlux 提供以下编程模型:

  • 注解:注解最初是 SpringMVC 栈的一部分。这些注解也由 SpringWebFlux 框架支持。这是开始使用 SpringWebFlux 栈的最简单方法。

  • 功能端点:此模型允许我们构建 Java 8 函数作为 Web 端点。应用程序可以配置为一系列路由、处理器和过滤器。然后它允许将这些全部作为 lambda 函数传递,以便在函数式范式下构建应用程序。

为了使用 SpringWebFlux,我们需要配置一个底层服务器。在编写本书时,Netty、Tomcat、Jetty 和 Undertow 是这里提供的当前选择。Netty 通常被用作标准选择,因为它在异步、非阻塞应用程序中表现良好。它也是一个非 servlet 服务器,与 Tomcat 和 Jetty 不同。以下图表展示了这一点:

配置注解

SpringWebFlux 支持基于注解的控制器。这与 SpringMVC 保持一致。用于创建控制器有两个注解:

  • @Controller: 这个注解定义了一个通用的网络组件。给定一个请求,它创建一个模型对象并为它生成动态视图响应。

  • @RestController: 这个注解定义了一个 RESTful 网络服务。给定一个请求,它返回 JSON 或 XML 格式的响应。这与能够为请求生成动态网页的通用控制器不同。

每个控制器都服务于一个请求模式。以下是可以用来定义控制器所服务请求模式的注解:

  • @RequestMapping: 这个注解用于标记控制器。它定义了一个请求模式前缀。它还可以用来定义请求头、媒体类型、HTTP 方法等。

  • @GetMapping: 这个注解特定于 GET HTTP 方法。它可以用来定义一个 GET HTTP 请求 URL。

  • @PostMapping: 这个注解特定于 POST HTTP 方法。它可以用来定义一个 POST HTTP 请求 URL。

  • @PutMapping: 这个注解特定于 PUT HTTP 方法。它可以用来定义一个 PUT HTTP 请求 URL。

  • @DeleteMapping: 这个注解特定于 DELETE HTTP 方法。它可以用来定义一个 DELETE HTTP 请求 URL。

  • @PatchMapping: 这个注解特定于 PATCH HTTP 方法。它可以用来定义一个 PATCH HTTP 请求 URL。

重要的一点是,@RequestMapping 匹配所有 HTTP 请求方法,与其它特定方法注解不同。

SpringBoot Starter

现在,让我们尝试使用之前讨论的注解支持来定义一个 RESTful 斐波那契网络服务。为此,我们将使用 Spring Boot,因为它提供了一种快速创建企业级 Spring 应用程序的方法。Spring Boot 项目为所有 Spring 模块提供了启动依赖项。每个启动器都假设了默认约定,以确保项目无需麻烦即可启动运行。

为了使用 SpringWebFlux,我们需要将 spring-boot-starter-webflux 依赖项添加到我们的项目中。让我们回顾一下 build.gradle 文件,如下所示:

buildscript {
    repositories {
        maven { url 'https://repo.spring.io/libs-snapshot' }
    }

    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE'
        compile 'org.springframework.boot:spring-boot-starter-webflux'

    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'

在前面的 build.gradle 文件中,我们有以下更改:

  1. 我们已经将 spring-boot 插件添加到我们的 gradle 构建中。

  2. 我们已经将 spring-boot-dependency 插件添加到我们的 gradle 构建中。该插件为我们的 gradle 构建添加了类似 Maven 的依赖项管理功能。

  3. spring-boot-starter-webflux 已作为依赖项添加。此项目引入了其他 webflux 相关项目的传递依赖项,例如 webfluxnetty-core 等。

  4. 在插件配置下已经添加了 spring-boot-gradle 插件。这使我们能够使用 gradlew bootrun 命令从命令行运行 Spring 应用程序。

默认情况下,Spring-boot-start-webflux会引入 Netty 依赖项。如果我们决定使用 Tomcat 或其他服务器,我们将排除spring-boot-starter-reactor-netty并包含该服务器依赖项。

添加控制器

我们需要添加一个可以提供斐波那契数的控制器。如前所述,我们需要添加一个带有@RestController注解的类。让我们看看以下控制器:

@RestController
public class ReactiveController {

 @GetMapping("/fibonacci")
 @ResponseBody
 public Publisher<Long>fibonacciSeries() {
 Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
   Long>of(0L, 1L), (state, sink) -> {
   if (state.getT1() < 0)
   sink.complete();
   else
   sink.next(state.getT1());
   return Tuples.of(state.getT2(), state.getT1() + state.getT2());
 });
 return fibonacciGenerator;
 }

}

在前面的类中,我们做了以下操作:

  1. @RestController添加到ReactiveController类中。这使该类成为 RESTful Web 服务:
public class ReactiveController
  1. @GetMapping添加到fibonacciSeries方法中。这允许我们在接收到对/fibonacci URL 的 HTTP GET请求时调用该方法。

  2. 这里需要注意的是,fibonacciSeries方法返回一个Flux<Long>

现在,我们还需要添加一个Main类,它可以运行SpringApplicationMain类必须注解为@EnableWebFlux,以确保 Spring 上下文实例化和注册与 SpringWebFlux 相关的类。这可以通过以下代码表示:

@SpringBootApplication
@Configuration
@ComponentScan("com.sample.web")
@EnableWebFlux
public class ReactorMain {
 public static void main(String[] args){
  SpringApplication.run(ReactorMain.class, args);
 }
}

使用gradlew bootrun命令运行应用程序。这将启动端口8080上的 Netty 服务器。最后,查找http://localhost:8080/fibonacci以接收以下结果:

图片

方法参数

在前面的代码中,fibonacciSeries请求方法不接受任何参数。这是因为我们没有期望任何输入。如果我们预见任何输入参数,它们可以用以下注解进行绑定:

  • @PathVariable:此注解用于访问 URI 模板变量的值。这些模板将自动转换为适当类型。如果没有找到匹配的类型,将引发TypeMismatchException
@GetMapping("/contact/{deptId}/employee/{empId}")
public Employee findEmployee(@PathVariable Long deptId, @PathVariable Long empId) {
// Find the employee.
}
  • @RequestParam:此注解用于确定作为查询参数传递的值。这里也执行了数据类型的自动转换:
@GetMapping("/contact/employee")
public Employee findEmployee(@RequstParam("deptId")Long deptId, @RequstParam("empId") Long empId) {
// Find the employee.
}
  • @RequestHeader:此注解用于确定请求头中传递的值。数据类型转换到目标类型是自动执行的:
@GetMapping("/fibonacci")
public List<Long> fibonacci(@RequestHeader("Accept-Encoding") String encoding) {
// Determine Series
}
  • @RequestBody:此注解用于确定请求体中传递的值。数据类型转换到目标类型是自动执行的。SpringWebFlux 支持以反应式类型 Flux 和 Mono 读取数据,因此执行非阻塞读取:
@PostMapping("/department")
public void createDept(@RequestBody Mono<Department> dept) {
// Add new department
}
  • @CookieValue:此注解用于确定请求中作为一部分的 HTTP cookie 值。数据类型转换到目标类型是自动执行的。

  • @ModelAttribute:此注解用于确定请求模型中的属性或在没有提供的情况下实例化一个。一旦创建,属性的属性值将使用传递的查询参数和提交的表单字段进行初始化:

@PostMapping("/department")
public void createdept(@ModelAttribute Department dept) {
// Add new department
}
  • @SessionAttribute:此注解用于确定预存在的会话属性。数据类型转换到目标类型是自动执行的。

  • @RequestAttribute: 这个注解用于确定由先前过滤器执行创建的现有请求属性。数据类型自动转换为目标类型。

除了方法参数外,还有@ResponseBody,它用于使用适当的 HTTP 写入器序列化return方法。这可以用于从请求方法返回 JSON 和 XML 类型的响应。

异常处理

应用程序在处理请求时经常会抛出异常。这些异常必须得到适当的处理,否则它们将向请求客户端发送 HTTP 500 错误。SpringWebFlux 通过创建带有@ExceptionHandler注解的方法来支持异常处理。这些异常处理器可以将抛出的异常作为参数:

@RestController
public class ReactiveController {
 @ExceptionHandler
 public String handleError(RuntimeException ex) {
 // ...
 }
}

异常处理器可以具有与请求方法相同的返回类型。可选地,我们希望在异常处理过程中设置 HTTP 状态,但 Spring 不会自动执行此操作。可以通过返回一个包含响应体以及所需的 HTTP 状态码的ResponseEntity来实现。

配置函数

在上一节中,我们使用传统的注解方法配置了 SpringWebFlux。现在,我们将看到如何使用 Java 8 lambda 以函数式方式配置 SpringWebFlux。让我们看看启动所需的关键组件。

处理函数

处理函数负责服务给定的请求。它以ServerRequest类的形式接收请求,并生成ServerResponse响应。ServerRequestServerResponse都是不可变的 Java 8 类。这些类支持用于读取/写入请求/响应体中传递的数据的反应式类型MonoFlux。让我们尝试使用前面的组件构建我们的第一个hello-world示例:

HandlerFunction<ServerResponse> helloHandler = request -> {
            Optional<String>name=request.queryParam("name");
            return ServerResponse.ok().body(fromObject("Hello to " +name.orElse("the world.")));
        };

在前面的代码中,我们正在执行以下操作:

  • Lambda 函数接受ServerRequest输入请求类型

  • 它试图确定是否传递了name查询参数

  • 函数返回 OK(HTTP 200)响应

  • 响应体包含Hello to the world

这只是一个简单的示例,但它清楚地展示了使用 Java 8 lambda 可以实现什么。我们可以向反应式数据库(如 Mongo)或外部调用添加查询,并将响应作为 Mono 或 Flux 返回。如果我们仔细查看ServerRequest,以下方法已被提供以将请求体转换为反应类型:

  • bodyToMono(Class<T> type): 这个方法将指定类型的单个对象作为 Mono 响应读取

  • bodyToFlux(Class<T> type): 这个方法将指定类型的多个对象作为 Flux 响应读取

如果我们查看前面的代码,我们使用了BodyInserters.fromObject()静态方法来写入响应体。这不是唯一的方法来做这件事。有许多方法可以写回响应体,以下是一些方法:

  • fromObject: 此方法将数据写回为对象

  • fromPublisher: 此方法将数据从给定的 Reactive Streams 发布者写回

  • fromFormData: 此方法将给定的键值对和表单数据写回

  • fromMultipartData: 此方法将给定数据写回为多部分数据

以 lambda 表达式编写的处理函数非常方便,但长期来看它们变得难以阅读和维护。通常建议将特定功能的手动函数组合在一个单独的处理类中。

路由函数

路由函数负责将传入的请求路由到正确的处理函数。如果我们将其与注解方法进行比较,那么它类似于@RequestMapping注解。

使用RequestPredicate来匹配请求,它试图验证预期的匹配标准。我们之前创建的helloHandler可以按以下方式配置:

RouterFunction<ServerResponse> route =          RouterFunctions.route(RequestPredicates.path("/hello"),hellowHandler);

上述代码执行以下操作:

  1. 它为/hello路径注册了一个谓词

  2. 如果请求匹配此路径,则路由器调用helloHandler

如果我们查看RequestPredicate,这是一个需要仅实现测试方法的函数式接口:

public interface RequestPredicate {
 boolean test(ServerRequest var1);
 default RequestPredicate and(RequestPredicate other) {..  }
 default RequestPredicate negate() {..}
 default RequestPredicate or(RequestPredicate other) {..}
 default Optional<ServerRequest> nest(ServerRequest request) {..}
}

然而,实现RequestPredicate不是必需的。框架提供了RequestPredicates实用类,其中包含大多数常用谓词。该实用类提供了基于 HTTP 方法、HTTP 头、查询参数、URL 路径等的路由。让我们看看RequestPredicates实用类提供的方法:

方法 匹配
path(String pattern) 谓词匹配传入的 URL
patternDELETE(String pattern) 当 HTTP 方法为DELETE时,谓词匹配传入的 URL 模式
GET(String pattern) 当 HTTP 方法为GET时,谓词匹配传入的 URL 模式
PUT(String pattern) 当 HTTP 方法为PUT时,谓词匹配传入的 URL 模式
POST(String pattern) 当 HTTP 方法为POST时,谓词匹配传入的 URL 模式
PATCH(String pattern) 当 HTTP 方法为PATCH时,谓词匹配传入的 URL 模式
HEAD(String pattern) 当 HTTP 方法为HEAD时,谓词匹配传入的 URL 模式
method(HttpMethod method) 谓词确定请求方法是否与传入的方法相同
oneaccept(MediaType type) 谓词确定请求接受头是否包含给定的MediaType
contentType(mediaType type) 谓词确定请求的contentType头是否包含给定的MediaType
headers(Predicate headerPredicate) 谓词确定请求头是否匹配谓词查询
Param(String name, String value) 谓词确定请求查询参数是否包含键值对
all() 谓词始终匹配请求

我们可以将一个或多个这些谓词组合起来构建复合匹配条件。可以使用以下RequestPredicate方法组合条件:

  • RequestPredicate.and(RequestPredicate): 构建逻辑AND条件,其中两者都必须匹配

  • RequestPredicate.or(RequestPredicate): 构建逻辑OR条件,其中任一可以匹配

  • RequestPredicate.negate(): 构建逻辑NOT条件,它必须不匹配

使用RouterFunctions实用类的Route函数配置RequestPredicates。可以使用以下RouterFunction方法配置额外的路由:

  • RouterFunctions.router(predicate,handler)

  • RouterFunction.andRoute(predicate,handler)

HandlerFilter

HandlerFilter类似于 Servlet 过滤器。它在HandlerFunction处理请求之前执行。在请求被服务之前,可能会有链式过滤器被执行。如果过滤器返回ServerResponse,则请求将按以下方式终止:

helloRoute.filter((request, next) -> {
    if (request.headers().acceptCharset().contains(Charset.forName("UTF-8"))) {
        return next.handle(request);
    }
    else {
        return ServerResponse.status(HttpStatus.BAD_REQUEST).build();
    }
});

上述代码正在执行以下操作:

  1. 使用filter()方法向helloRoute添加过滤器

  2. 过滤器接收一个请求和下一个处理函数

  3. 验证请求头是否在Accept-Language头中包含UTF-8字符集

  4. 如果是这样,将请求转发到下一个函数

  5. 如果不是,则构建状态为BAD_REQUESTServerResponse

图片

HttpHandler

现在我们已经使用处理程序和路由器映射了一个请求,剩下的唯一步骤是启动服务器。SpringWebFlux 使我们能够以编程方式启动服务器。为了做到这一点,我们必须从RouterFunction获取HttpHandler然后启动所需的服务器:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(helloRoute);
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create("127.0.0.1", 8080);
server.newHandler(adapter).block();

上述代码特定于 Netty,因为我们当前示例中使用了reactor-netty。在上述代码中,我们正在执行以下操作:

  • 使用RoterFunctions.toHttpHandlerhelloRoute转换为HttpHandler

  • 实例化 Netty 的ReactorHttpHandlerAdapter并使用它来配置 Netty HttpServer

  • 最后,我们阻塞以监听传入的请求并为其提供服务

上述配置特定于底层服务器。当使用Undertow时,可以使用以下代码构建配置:

HttpHandler httpHandler =  RouterFunctions.toHttpHandler(helloRoute);
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(httpHandler);
Undertow server = Undertow.builder().addHttpListener(8080, "127.0.0.1").setHandler(adapter).build();
server.start();

以下代码适用于 Tomcat:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(helloRoute);
Servlet servlet = new TomcatHttpHandlerAdapter(httpHandler);
Tomcat server = new Tomcat();
File root = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", root.getAbsolutePath());
Tomcat.addServlet(rootContext, "ctx", servlet);
rootContext.addServletMappingDecoded("/", "ctx");
server.setHost(host);
server.setPort(port);
server.start();

或者,我们可以将所有这些配置留给 Spring,并使用基于 Spring DispatcherHandler的配置来启动服务器。该配置基于 Java 注解。配置自动注册以下附加组件以支持功能端点:

  • RouterFunctionMapping: 这确定 Spring 配置中RouterFunction <?> bean 的列表。它将它们组合并将请求路由到正确的RouterFunction

  • HandlerFunctionAdapter: 当收到请求时,调用正确的HandlerFunction

  • ServerResponseResultHandler:这将从HandlerFunction调用中写回ServerResponse

当我们使用@EnableWebFlux注解时,所有前面的组件都是由 Spring 注册的。

斐波那契功能路由器

现在我们已经看到了功能映射的基础,让我们尝试使用它们来渲染斐波那契序列。我们将使用上一节中开发的相同的斐波那契生成器。我们看到了我们可以将 Reactive Stream 发布者写入ServerResponse,如下面的代码片段所示:

@Configuration
class FibonacciConfigurer {

    @Bean
    RouterFunction<ServerResponse> fibonacciEndpoint() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        RouterFunction<ServerResponse> fibonacciRoute =
                RouterFunctions.route(RequestPredicates.path("/fibonacci"),
                        request -> ServerResponse.ok().body(fromPublisher(fibonacciGenerator, Long.class)));
        return fibonacciRoute;
    }
}

在前面的代码中,我们做了以下操作:

  1. 创建了一个FibonacciGenerator来生成序列

  2. /fibonacci配置了一个路由,然后返回了斐波那契数的响应

  3. 方法被注解为@Bean,这将自动将此路由注册到SpringContext

  4. 该类被注解为@Configuration

现在,剩下的只是配置 SpringWebFlux 以选择此配置。这是通过创建一个Main类并使用所需的注解来完成的:

@SpringBootApplication
@Configuration
@ComponentScan("com.sample.web")
@EnableWebFlux
public class ReactorMain {
    public static void main(String[] args) {
        SpringApplication.run(ReactorMain.class, args);
    }
}

前面的类与我们用于在 SpringWebFlux 中处理注解的类完全相同。现在,让我们使用spring-boot插件运行服务器:

gradlew bootrun

这将在端口8080上启动 Netty。让我们提交http://localhost:8080/fibonacci URL 以确定响应:

摘要

在本章中,我们探讨了使用 SpringWebFlux 框架构建基于 Web 的微服务。我们还讨论了项目提供的各种服务器选项,并查看了解决微服务的传统基于注解的方法。我们发现 SpringMVC 项目中的所有注解都由 SpringWebFlux 项目支持。接下来,我们采用了构建微服务的功能方法。我们配置了路由和处理函数来构建斐波那契 Web 服务。

在下一章中,我们将探讨向基于 SpringWebFlux 的 Web 服务添加其他 Web 功能的方法,例如网页模板、安全性和更多内容。

问题

  1. 我们如何配置 SpringWebFlux 项目?

  2. SpringWebFlux 支持哪些MethodParameter注解?

  3. ExceptionHandler的用途是什么?

  4. HandlerFunction的用途是什么?

  5. RouterFunction的用途是什么?

  6. HandlerFilter的用途是什么?

第六章:动态渲染

在上一章中,我们使用 SpringWebFlux 构建了简单的 Web 服务。到目前为止,我们已经构建了返回 JSON 响应的 RESTful Web 服务。然而,SpringWebFlux 不仅限于 RESTful Web 服务;它是一个完整的 Web 框架,提供了构建动态网页的能力。

在本章中,我们将讨论以下主题:

  • 视图模板

  • 静态资源

  • WebClient

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章的 GitHub 链接为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter06

视图模板

SpringWebFlux 提供了多种使用不同技术平台渲染视图的选项。无论我们做出何种选择,框架都会采用相同的视图解析过程,使我们能够得到正确的视图。然后,可以使用任何支持的技术来渲染视图。在本节中,我们将介绍使用 SpringWebFlux 渲染视图的完整过程。

解析视图

视图解析是框架用来确定对于接收到的请求需要渲染哪个视图的过程。完整的视图解析过程使我们能够根据内容参数渲染不同的视图。在我们开始构建不同的视图之前,让我们讨论一下框架是如何确定它需要渲染哪个视图的。

在上一章中,我们为处理请求配置了HandlerFunction。这个函数返回一个HandlerResultHandlerResult不仅包含结果,还包含传递给请求的属性。然后,框架使用HandlerResult调用ViewResolutionResultHandlerViewResolutionResultHandler通过验证以下返回值来确定正确的视图:

  • String:如果返回值是字符串,则框架使用配置的ViewResolvers构建视图。

  • Void:如果没有返回任何内容,它将尝试构建默认视图。

  • Map:框架会查找默认视图,但也会将返回的键值添加到请求模型中。

ViewResolutionResultHandler还会查找请求中传递的内容类型。为了确定应该使用哪个视图,它会将传递给ViewResolver的内容类型与支持的内容类型进行比较。然后,它选择第一个支持请求内容类型的ViewResolver

重要的是要注意,一个请求可以重定向到另一个请求。为了做到这一点,我们在视图名称之前加上redirect:关键字。然后框架使用UrlBasedViewResolver并返回一个用于重定向的 URL。如果返回的 URL 来自同一应用程序,则路径可以以相对方式构建(例如,redirect:/applicationA/locationA)。如果返回的 URL 来自外部位置,则可以使用绝对 URL 构建视图名称(例如,redirect:http://www.google.com/search/)。

现在您已经了解了视图解析过程是如何工作的,让我们尝试使用各种支持的模板框架来构建动态视图。

Freemarker

Freemarker 是一个可以用来生成动态 HTML 输出的模板引擎。它不仅限于 HTML 页面;它可以生成任何类型的文本输出,例如电子邮件和报告。为了使用它,我们必须使用 Freemarker 语法编写一个模板文件。然后 Freemarker 引擎接收该文件以及用于生成结果动态文本的数据。

现在,让我们尝试配置 Freemarker 以渲染我们的斐波那契数列。为了使用 Freemarker 进行视图解析,我们必须首先将所需的依赖项添加到我们的build.gradle中,如下所示:

plugins {
    id "io.spring.dependency-management" version "1.0.1.RELEASE"
    id "org.springframework.boot" version "2.0.3.RELEASE"
}
apply plugin: 'java'
// Rest removed for Brevity

dependencies {
        compile 'org.springframework.boot:spring-boot-starter-webflux'
        compile 'org.springframework:spring-context-support'
        compile group: 'org.freemarker', name: 'freemarker', version: '2.3.28'
}

在前面的代码中,我们添加了以下内容:

  1. org.freemarker:freemarker:Freemarker 模板引擎——在撰写本书时,版本 2.3.28 是最新的版本。

  2. spring-context-support:这提供了 Freemarker 和 Spring 之间所需的集成。由于我们已经配置了spring-boot,因此我们不需要指定spring-context-support依赖项的版本。

现在我们已经添加了 Freemarker,我们必须对其进行配置。Spring 上下文有一个视图解析器注册表,必须更新以包括 Freemarker 解析器,如下所示:

@EnableWebFlux
@Configuration
public class WebfluxConfig implements WebFluxConfigurer {

   @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
   }

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/freemarker/");
        return configurer;
    }
}

在前面的代码中,我们做了以下操作:

  1. 实现了WebFluxConfigurer接口。该接口提供了configureViewResolvers方法。

  2. configureViewResolvers由 Spring 上下文调用,同时提供一个ViewResolverRegistry。该注册表提供了freeMarker()方法以启用基于 Freemarker 的解析。

  3. 接下来,我们必须创建一个FreeMarkerConfigurer,它可以设置 Freemarker 参数。如前所述的代码所示,我们配置了模板路径为classpath:/freemarker/。这将允许我们在src/main/resources/freemarker路径下创建 Freemarker 模板。

现在,让我们添加一个用于显示斐波那契数列的 Freemarker 模板。在这种情况下,我们希望将数字以简单的 HTML 列表形式列出,如下所示:

<!DOCTYPE html>
<html>
    <head>
        <title>Reactor Sample</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>
    <body>
        <h1>Fibonacci Numbers</h1>
        <ul style="list-style-type:circle">
        <#list series as number>
          <li>${number}</li>
        </#list>
        </ul>
    </body>
</html>

在前面的 HTML 模板中,我们做了以下操作:

  1. 我们添加了一个名为series的变量,其中包含一个值列表。

  2. <#list> </#list>语法遍历列表,提供单个值。

  3. 值随后在<li> HTML 标签中呈现。

现在,将文件保存为numbers.ftl,位于src/main/resources/freemarker路径下。

本书的目标不是涵盖 Freemarker 语法。要了解更多信息,请参阅官方 Freemarker 文档。

现在剩下的唯一配置就是使用模板来渲染斐波那契序列。首先,让我们在我们的基于注解的控制器中使用此模板:

@Controller
public class ReactiveController {

// Rest removed for Brevity
@GetMapping("/numbers")
    public String handleSeries(Model model) {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        model.addAttribute("series", fibonacciGenerator);
        return "numbers";
    }
}

在前面的代码中,我们做了以下操作:

  1. 我们添加了 @controller 注解,而不是 @RestControllerRestController 注解仅渲染 JSON 响应。另一方面,@controller 注解允许我们渲染任何类型的响应。

  2. 我们将 fibonacciGenerator(Flux<>) 添加到我们的模型中,作为 series 变量。这将提供系列值给 Freemarker 模板。

  3. 接下来,我们返回一个 numbers 字符串作为返回值。这将解析为选择 number.ftl 模板。

现在,让我们运行 ReactorMain 并访问 http://localhost:8080/numbers。在这个时候,我们将得到一个列出斐波那契序列的 HTML 页面,如下所示:

图片

现在,让我们使用 Freemarker 视图和我们的 HandlerFunction。为了做到这一点,我们必须更改 ServerResponse,如下所示:

@Configuration
class FibonacciConfigurer {

    // Rest removed  For Brevity

     @Bean
     RouterFunction<ServerResponse> fibonacciEndpoint() {
         Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                 Long>of(0L, 1L), (state, sink) -> {
             if (state.getT1() < 0)
                 sink.complete();
             else
                 sink.next(state.getT1());
             return Tuples.of(state.getT2(), state.getT1() + state.getT2());
         });
         Map<String, Flux> model = new HashMap<>();
         model.put("series",fibonacciGenerator);
         RouterFunction<ServerResponse> fibonacciRoute =
                 RouterFunctions.route(RequestPredicates.path("/fibonacci"),
                         request -> ServerResponse.ok().render("numbers",model));
         return fibonacciRoute;
     }

在前面的代码中,我们做了以下操作:

  • 我们现在使用渲染 API 而不是构建 ServerResponse.body。此 API 接受一个视图名称和一个可选的属性映射。

  • 我们通过将系列键映射到 fibonacciGenerator (Flux<>) 来在映射中提供系列值。

现在,让我们运行 ReactorMain 并访问 http://localhost:8080/fibonacci。在这个时候,我们将得到相同的列出斐波那契序列的 HTML 页面。

Thymeleaf

Thymeleaf 是一个基于 Java 和 XML/HTML 的现代模板引擎。它可以用来渲染任何 XML/HTML 内容。使用 Thymeleaf 构建的模板是自然顺序的,这意味着它们将按照设计的方式渲染,与 JSP 不同。这个模板引擎旨在取代 JSP。它与 Spring 有很好的集成。

现在,让我们尝试配置 Thymeleaf 来渲染斐波那契序列。为了使用 Thymeleaf 进行视图解析,我们必须首先在我们的 build.gradle 中添加所需的依赖项,如下所示:

plugins {
    id "io.spring.dependency-management" version "1.0.1.RELEASE"
    id "org.springframework.boot" version "2.0.3.RELEASE"
}
apply plugin: 'java'
// Rest removed for Brevity

dependencies {
        compile 'org.springframework.boot:spring-boot-starter-webflux'
        compile "org.springframework.boot:spring-boot-starter-thymeleaf"
}

在前面的代码中,我们添加了以下内容:

  • spring-boot-starter-thymeleaf: Springboot 启动器导入所需的 Thymeleaf 库。它还使用预定义的默认值配置了 Thymeleaf 引擎。

现在我们已经添加了 Thymeleaf,我们必须启用它。Spring 上下文有一个视图解析器注册表,必须更新以包括 Thymeleaf 解析器,如下所示:

@EnableWebFlux
@Configuration
public class WebfluxConfig implements WebFluxConfigurer {
    private final ISpringWebFluxTemplateEngine templateEngine;

    public WebfluxConfig(ISpringWebFluxTemplateEngine templateEngine) {
        this.templateEngine = templateEngine;
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(thymeleafViewResolver());
   }

    @Bean
    public ThymeleafReactiveViewResolver thymeleafViewResolver() {
        final ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver();
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }

}

在前面的代码中,我们做了以下操作:

  1. 实现了 WebFluxConfigurer 接口。此接口提供了 configureViewResolvers 方法。

  2. configureViewResolvers 方法由 Spring 上下文调用,同时还有一个 ViewResolverRegistry。我们必须使用此方法注册一个 ThymeleafReactiveViewResolver

  3. ThymeleafReactiveViewResolver 使用一个 ISpringWebFluxTemplateEngine 引擎,该引擎在 Spring 上下文中可用。

  4. 模板引擎在 src/main/resources/templates 下查找模板。在查找之前,它还会在模板名称前添加一个 .html 后缀。

现在,让我们添加一个 Thymeleaf 模板来显示斐波那契数列。我们希望将数字作为简单的 HTML 列表列出,如下所示:

<!DOCTYPE html>

<html >
    <head>
        <title>Reactor Sample</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>
    <body>
        <section class="container">
            <ul>
                <li th:each="item : ${series}" th:text="${item}"></li>
            </ul>
        </section>
    </body>
</html>

在前面的 HTML 模板中,我们做了以下操作:

  1. 添加了一个包含值的列表的 series 变量。

  2. 添加了 <li th:each></li>,它遍历系列变量并渲染单个元素。

现在,将文件保存为 numbers.html,在路径 src/main/resources/templates 下。

本书的目标不是涵盖 Thymeleaf 语法。请参阅官方 Thymeleaf 文档。

现在,唯一剩下的配置就是使用模板来渲染斐波那契数列。首先,让我们在我们的基于注解的控制器方法中使用模板:

@Controller
public class ReactiveController {

// Rest removed for Brevity
@GetMapping("/numbers")
    public String handleSeries(Model model) {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        model.addAttribute("series", fibonacciGenerator);
        return "numbers";
    }
}

在前面的代码中,我们做了以下操作:

  1. 我们添加了 @controller 注解,而不是 @RestControllerRestController 注解仅渲染 JSON 响应。另一方面,@controller 注解可以渲染任何类型的响应。

  2. 我们将 fibonacciGenerator(Flux<>) 添加到我们的模型中作为一个系列。这将提供系列值给 Freemarker 模板。

  3. 接下来,我们将 numbers 字符串作为返回值。返回值将映射到 number.html 模板。

现在,让我们运行 ReactorMain 并打开 http://localhost:8080/numbers。在此阶段,我们将得到一个列出斐波那契数列的 HTML 页面,如下所示:

图片

现在,让我们使用 Thymeleaf 视图与我们的 HandlerFunction。为了做到这一点,我们必须更改 ServerResponse,如下所示:

@Configuration
class FibonacciConfigurer {

    // Rest removed  For Brevity

     @Bean
     RouterFunction<ServerResponse> fibonacciEndpoint() {
         Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                 Long>of(0L, 1L), (state, sink) -> {
             if (state.getT1() < 0)
                 sink.complete();
             else
                 sink.next(state.getT1());
             return Tuples.of(state.getT2(), state.getT1() + state.getT2());
         });
         Map<String, Flux> model = new HashMap<>();
         model.put("series",fibonacciGenerator);
         RouterFunction<ServerResponse> fibonacciRoute =
                 RouterFunctions.route(RequestPredicates.path("/fibonacci"),
                         request -> ServerResponse.ok().render("numbers",model));
         return fibonacciRoute;
     }

在前面的代码中,我们做了以下操作:

  • 我们现在使用渲染 API 而不是构建 ServerResponse.body。该 API 接受一个视图名称和一个可选的属性映射。

  • 我们通过将系列键映射到 fibonacciGenerator (Flux<>) 来在映射中提供系列值。

现在,让我们运行 ReactorMain 并打开 http://localhost:8080/fibonacci。在此阶段,我们将得到与列出斐波那契数列相同的 HTML 页面。

脚本

SpringWebFlux 也能够使用各种脚本库进行视图结束。它使用 JSR-223 Java 脚本引擎规范来集成各种脚本引擎。在撰写本书时,以下集成是可用的:

  • Handlebars,使用 Nashrom 引擎

  • Mustache,使用 Nashrom 引擎

  • React,使用 Nashrom 引擎

  • EJS,使用 Nashrom 引擎

  • ERB,使用 JRuby 引擎

  • 字符串,使用 Jython 引擎

  • Kotlin,使用 Kotlin 引擎

在下一节中,我们将介绍与 Mustache 的集成。其他选项的集成类似。

Mustache

Mustache 是一个简单的模板引擎,在各种语言中都有可用。我们现在将使用 JavaScript 中的模板引擎 Mustache.js。Mustache 通常被视为无逻辑的,因为它缺少显式的控制流语句。控制流是通过使用部分标签来实现的。

更多关于 Mustache 的详细信息,请参阅 mustache.github.io/

现在,让我们尝试配置 Mustache 来渲染我们的斐波那契数列。在我们的 build.gradle 文件中不需要任何其他依赖项:

plugins {
    id "io.spring.dependency-management" version "1.0.1.RELEASE"
    id "org.springframework.boot" version "2.0.3.RELEASE"
}
apply plugin: 'java'
// Rest removed for Brevity

dependencies {
        compile 'org.springframework.boot:spring-boot-starter-webflux'
}

Spring 框架提供了开箱即用的集成。Spring 上下文有一个视图解析器注册表,必须更新以包括 ScriptTemplate 解析器,如下所示:

@EnableWebFlux
@Configuration
public class WebfluxConfig implements WebFluxConfigurer {

   @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
   }

    @Bean
    public ScriptTemplateConfigurer scrptTemplateConfigurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setResourceLoaderPath("classpath:/mustache/");
        configurer.setRenderFunction("render");
        return configurer;
    }

}

在先前的代码中,我们做了以下操作:

  • 实现了 WebFluxConfigurer 接口。该接口提供了 configureViewResolvers 方法。

  • configureViewResolvers 方法由 Spring 上下文调用,同时还有 ViewResolverRegistry。该注册表提供了 scriptTemplate() 方法来启用基于脚本的解析器。

  • 接下来,我们必须为 ScriptTempletConfigure 设置参数。配置器需要启用 Mustache.js,并使用 Nashrom 引擎评估它。

  • ScriptTempletConfigure 还指定了模板的位置。在先前的代码中,我们将位置配置为 src/main/resources/mustache

  • 由于我们使用 Mustache.js,我们还需要在 Mustache 模板位置下添加 Mustache.js(来自 github.com/janl/mustache.js)。

现在,让我们添加一个 Mustache 模板来显示斐波那契数列。在这种情况下,将数字列表示为一个简单的 HTML 列表是有益的,如下所示:

<!DOCTYPE html>

<html>
    <head>
        <title>Reactor Sample</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>
    <body>
        <section class="container">
            {{#series}}
            <div class="row">
                {{.}}
            </div>
            {{/series}}
        </section>
    </body>
</html>

在先前的 HTML 模板中,发生了以下情况:

  1. 有一个 series 变量,包含一系列值。

  2. {{#series}} {{/series}} 语法遍历列表,提供单个值。

  3. 然后使用 HTML <div> 标签中的 {{.}} 语法渲染该值。

现在,将文件保存为 numbers.html,位于 src/main/resources/mustache 路径下。剩下的唯一配置就是使用 numbers.html 模板来渲染斐波那契数列。首先,让我们在我们的基于注解的控制器方法中使用 numbers.html 模板:

@Controller
public class ReactiveController {

// Rest removed for Brevity
@GetMapping("/numbers")
    public String handleSeries(Model model) {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        model.addAttribute("series", fibonacciGenerator);
        return "numbers.html";
    }
}

在先前的代码中,我们做了以下操作:

  • 我们添加了 @controller 注解,而不是 @RestControllerRestController 注解仅渲染 JSON 响应。另一方面,@controller 注解允许我们渲染任何类型的响应。

  • 我们在我们的模型中添加了 fibonacciGenerator(Flux<>) 作为 series. 这将为 Mustache 模板提供系列值。

  • 接下来,我们返回了 numbers.html 字符串作为返回值。返回值将映射到 number.html 模板。这与之前的模板引擎不同,后者会自动在返回的字符串值后添加后缀,以确定模板。

现在,让我们运行 ReactorMain 并访问 http://localhost:8080/numbers。此时,我们将得到一个列出斐波那契数列的 HTML 页面,如下所示:

现在,让我们使用 Mustache 视图与我们的 HandlerFunction 结合。为了做到这一点,我们必须更改 ServerResponse,如下所示:

@Configuration
class FibonacciConfigurer {

    // Rest removed  For Brevity

     @Bean
     RouterFunction<ServerResponse> fibonacciEndpoint() {
         Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                 Long>of(0L, 1L), (state, sink) -> {
             if (state.getT1() < 0)
                 sink.complete();
             else
                 sink.next(state.getT1());
             return Tuples.of(state.getT2(), state.getT1() + state.getT2());
         });
         Map<String, Flux> model = new HashMap<>();
         model.put("series",fibonacciGenerator);
         RouterFunction<ServerResponse> fibonacciRoute =
                 RouterFunctions.route(RequestPredicates.path("/fibonacci"),
                         request -> ServerResponse.ok().render("numbers.html",model));
         return fibonacciRoute;
     }

在前面的代码中,我们做了以下操作:

  1. 我们现在不是构建 ServerRespose.body,而是使用渲染 API。该 API 接受一个视图名称和一个可选的属性映射。

  2. 我们通过将序列键映射到 fibonacciGenerator (Flux<>) 来在映射中提供了序列值。

现在,让我们运行 ReactorMain 并访问 http://localhost:8080/fibonacci。作为响应,我们将得到一个列出斐波那契数列的相同 HTML 页面。

学习静态资源

一个动态应用程序通常也有静态部分。SpringWebFlux 也使我们能够配置静态资源。假设我们想在我们的 Thymeleaf 应用程序中使用 bootstrap.css。为了做到这一点,我们必须启用服务器来确定静态内容。这可以配置如下:

public class WebfluxConfig implements WebFluxConfigurer {
    //Rest Removed for Brevity
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("classpath:/static/");
    }
}

在前面的代码中,发生了以下情况:

  1. addResourceHandler 方法接受一个 URL 模式,并将其配置为静态位置,这些位置必须由服务器提供服务。在前面的代码中,我们所有的静态 URL 应该看起来像 like/resources/XXXX

  2. addResourceLocations 方法配置了一个必须从其中提供静态内容的位置。在前面的代码中,我们已经将位置配置为 src/main/resources/static

现在,让我们将 bootstrap.css 下载到 src/main/resources/static。这将服务于 /resources/bootstrap.min.css。剩下要做的就是将 css 包含在我们的 numbers.html Thymeleaf 模板中,如下所示:

<html >
    <head>
        <title>Reactor Sample</title>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <link rel="stylesheet" href="/resources/bootstrap.min.css">
    </head>
    <body>
        <section class="container">
            <ul class="list-group">
                <li th:each="item : ${series}" th:text="${item}" class="list-group-item"></li>
            </ul>
        </section>
    </body>
</html>

在前面的代码中,发生了以下情况:

  1. <link rel="stylesheet"../> 将包含来自我们服务器的 css

  2. 我们使用了 container 以及来自 Bootstrap 的 list-grouplist-group-item 类来为我们的 HTML 元素提供样式。

现在,运行服务器并打开 http://localhost:8080/numbers。页面现在使用 Bootstrap 网格格式化,如下所示:

ResourceHandlerRegistry 还使我们能够配置缓存控制头。它还可以用于构建可以解析 .gz 静态资源和版本化资源的解析器链。

WebClient

SpringWebFlux 框架还提供了一个非阻塞、异步 HTTP 客户端来发送请求。WebClient 提供了可以配置为 Java 8 lambdas 的 API,用于处理数据。在后台,WebClient API 配置 Netty 执行异步、非阻塞通信。现在,让我们看看我们如何在应用程序中使用 WebClient。

WebClient 提供以下两种方法来消费数据:

  • 检索:这是最简单的方法,它将主体解码为 Flux 或 Mono。

  • Exchange: 如果我们对收到的响应感兴趣,exchange 方法适合此目的。它提供了完整的消息,可以将其转换回目标类型。考虑以下代码示例:

public void readFibonacciNumbers() {
  WebClient client = WebClient.create("http://localhost:8080");
  Flux<Long> result = client.get()
          .uri("/fibonacci").accept(MediaType.APPLICATION_JSON)
          .retrieve()
          .bodyToFlux(Long.class);
  result.subscribe( x-> System.out.println(x));
}

在前面的代码中,我们构建了 WebClient 来读取斐波那契数列响应。此代码实现了以下功能:

  1. 它为以下位置创建了一个 WebClient 实例:http://localhost:8080

  2. 客户端向 /fibonacci 发送一个 HTTP GET 请求,并带有所需的 JSON ACCEPT 头部。

  3. 然后它调用 retrieve 方法,并将正文转换为 Flux<Long>

  4. 最后,我们订阅了 Flux 并将数字打印到控制台。

可以使用 exchange 方法处理相同的斐波那契数列,如下所示:

public void readFibonacciNumbersUsingExchange() {
        WebClient client = WebClient.create("http://localhost:8080");
        Flux<Long> result = client.get()
                .uri("/fibonacci").accept(MediaType.APPLICATION_JSON)
                .exchange()
                .flatMapMany(response -> response.bodyToFlux(Long.class));
        result.subscribe( x-> System.out.println(x));
    }

以下是在 exchange 方法与 retrieve 方法之间的关键差异:

  • exchange 方法提供了一个 Mono<ClientResponse>。这必须使用 flatMapMany API 转换为 Flux。

  • 我们处理响应体并将其转换为 Flux<Long>

除了前面提到的差异之外,retrieve 方法还提供了一个方便的 onStatus API。此方法用于在指定的 HTTP 状态码上调用函数。另一方面,在 exchange 方法中,我们获取完整的响应,因此开发者需要读取 HTTP 状态码并调用所需的逻辑。

WebClient 可以用来调用 HTTP GETPUTPOSTDELETEPATCHHEAD 方法。当使用 POST 时,我们通常需要添加一个请求体。这是通过调用 PUT 方法中可用的 body() API 来完成的。该 API 接受指定类型的 Mono 或 Flux。或者,如果有可用的对象,可以使用 syncBody() 方法进行处理。WebClient API 还提供了以下方法来配置请求:

  • accepts: 使用指定的内容类型配置 accepts request

  • acceptCharset: 配置 accepts-charset 请求头

  • header(s): 使用指定的值配置指定的头(s)

  • attributes: 向请求添加指定的属性

  • cookies: 向请求添加 cookies

WebClient 还提供了一个构建器,可以用来根据提供的设置构建 WebClient。这可以用来实例化一个客户端,作为特定的 SSL 上下文,或者使用默认的头。构建器配置应用于创建的 WebClient 实例,因此对于使用该实例进行的每个调用都会调用它。

SpringWebFlux 还提供了 WebTestClient,它是 WebClient 的扩展,并附带断言来验证响应体和响应状态。该类可以以类似于 WebClient 的方式实例化。在通过 exchange 方法发出请求后,可以使用以下方法进行断言:

  • expectStatus(): 此方法可以验证响应状态码,例如 OKNOT_FOUND

  • expectHeader(): 此方法可以验证响应头,例如 MediaType

  • expectBody(class): 此方法可以验证响应体是否可以转换为指定的类。

  • expectBodyList(class): 此方法可以验证响应体是否可以转换为指定类对象的列表。转换后,它可以验证列表大小和列表对象。

WebTestClient 可以用来测试和验证 SpringWebFlux 应用程序。WebTestClient 提供不同的 bindXXX 方法,这些方法可以用来配置 WebTestClient 以适用于 ApplicationContext、URL、控制器、路由函数等。然后它可以对配置的资源进行调用,并验证响应。

摘要

在本章中,我们讨论了如何使用 SpringWebFlux 可用的各种模板引擎来渲染动态内容。我们集成了基于 Java 的模板引擎、Freemarker 和 Thymeleaf。我们还探讨了如何启用基于脚本的引擎,以及如何与 Mustache.js 一起工作。接下来,我们研究了如何使用 SpringWebFlux 来提供静态内容。最后,我们讨论了使用 WebClient 来进行异步、非阻塞的 HTTP 请求。我们现在正在生成事件并处理它们。在下一章中,我们将讨论执行流控制和背压的方法。

问题

  1. SpringWebFlux 框架是如何解析视图的?

  2. 哪些组件被配置为使用 Thymeleaf 模板引擎?

  3. 在 SpringWebFlux 中,使用哪个 API 来配置静态资源?

  4. WebClient 有哪些好处?

  5. 检索和交换 WebClient API 之间的区别是什么?

第七章:流控制和背压

在前面的章节中,我们讨论了 Reactor 如何提供有效的控制来检查生产率。这种机制通常被称为背压。然而,在某些情况下,背压并不是一个有效的策略。在这种情况下,Reactor 提供了一系列无需背压即可使用的流控制优化。

在本章中,我们将介绍有关流控制和背压的以下主题:

  • 分组

  • 缓冲区

  • 窗口

  • 示例

  • 背压

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE 2018.1 或更高版本

本章的 GitHub 链接为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter07

流控制

流控制全部关于管理事件速率,以确保在引发大量事件时,生产者不会压倒其订阅者。快速生产者会将许多事件推送到其订阅者。每个订阅者将按接收顺序逐个处理这些事件。这种顺序处理过程可能相当低效,因为每个事件都是通过电线传输的。

为了提高效率,Reactor 中有一些运算符允许生产者在块中引发事件。每个事件块都发送给订阅者,使他们能够同时处理多个事件。

groupBy运算符

groupBy()运算符将Flux<T>转换为批次。该运算符将每个元素与Flux<T>中的一个键相关联。然后它将具有相同键的元素分组。然后,该运算符发出这些组。这在上面的图中有所描述:

图片

重要的是要注意,一旦元素被分组,它们可能会失去其原始的顺序。顺序是由键生成逻辑强制执行的。由于每个元素只与一个键相关联,因此生成的组不为空。所有生成的组在本质上都是不相交的。让我们尝试为我们的斐波那契数列生成一些组:

    @Test
    public  void testGrouping(){
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator.take(20)
                .groupBy(i -> {
                    List<Integer> divisors= Arrays.asList(2,3,5,7);
                    Optional<Integer> divisor = divisors.stream().filter(d -> i % d == 0).findFirst();
                    return divisor.map(x -> "Divisible by "+x).orElse("Others");

                })
                 .concatMap(x -> {
                     System.out.println("\n"+x.key());
                     return x;
                 })
                .subscribe(x -> System.out.print(" "+x));
    }

在前面的代码中,我们执行了以下步骤:

  1. 我们将原始数据集划分为能被 2 整除能被 3 整除能被 5 整除能被 7 整除等组的集合。

  2. groupBy运算符将这些分区数据集作为键值对发出。键是一个字符串,值是List<Long>

  3. 使用concatMap运算符合并了数据集。我们还使用该运算符打印了键。

  4. 最后,我们在Subscribe函数中打印了List

让我们运行我们的测试用例以确认输出:

图片

缓冲区运算符

buffer() 操作符收集所有 Flux<T> 元素并将它们作为 List<T> 发出。与由 groupBy() 操作符生成的组不同,List<T> 缓冲区中的所有元素都保持其原始顺序。或者,我们也可以向操作符提供一个 batchSize。然后,操作符将生成 N 个列表,每个列表将包含指定数量的元素。让我们尝试在我们的斐波那契数列上使用缓冲操作符:

    @Test
    public  void testBufferWithDefinateSize(){
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator.take(100)
                .buffer(10)
                .subscribe(x -> System.out.println(x));
    }

在前面的代码中,我们做了以下操作:

  1. 我们将原始数据集划分为每个包含 10 个元素的缓冲列表

  2. 我们然后使用 subscribe 函数打印了列表

让我们运行我们的测试用例以确认输出。我们可以看到斐波那契元素以单个 List<Long> 的形式发出:

图片

buffer() 操作符有许多变体。让我们看看其中的一些。这些都会生成多个列表缓冲区。

buffer(maxSize, skipSize) 操作符接受两个参数。第一个参数是每个缓冲区的最大大小。第二个参数是在开始新缓冲区之前必须跳过的元素数量。由操作符生成的缓冲区列表具有以下特征:

  • 如果 maxSize 大于 skipSize,则缓冲区在本质上是有重叠的。下一个缓冲区从上一个缓冲区的 skipSize 位置指定的元素开始。这意味着元素在所有缓冲区中都是重复的。

  • 如果 maxSize 小于 skipSize,则缓冲区在本质上是不相交的。生成的列表会缺少原始 Flux<T> 中的元素。

  • 如果 skipSize0,则所有列表在本质上是不相交的。它们不会缺少原始 Flux<T> 中的任何元素。考虑以下代码:

    @Test
    public  void testBufferSizes(){
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator.take(100)
                .buffer(2,5)
                .subscribe(x -> System.out.println(x));
    }

在前面的代码中,我们做了以下操作:

  1. 我们将原始数据集划分为每个包含两个元素的缓冲区

  2. 每个缓冲列表从第五个元素开始,因此丢弃了三个元素

  3. 我们在 subscribe 函数中打印了列表

让我们运行代码以确认输出。我们可以看到斐波那契元素以单个 List<Long> 的形式发出:

图片

bufferUntilbufferWhile 变体接受一个谓词条件,并聚合元素直到条件为真。bufferWhile 操作符生成一个包含所有匹配条件的元素的单一缓冲区。另一方面,bufferUntil 操作符将不匹配的元素缓冲到一个列表中。当它找到一个匹配的元素时,它将该元素添加到当前缓冲区。然后它开始一个新的缓冲区以添加下一个传入的元素。这个过程在以下图中展示:

图片

另一个重载的 buffer() 方法允许我们根据时间段生成缓冲区列表。操作符接受一个持续时间,并聚合该时间段内的所有元素。因此,它可以收集在第一个 Duration、第二个 Duration 等期间发生的所有事件,如下所示:

    @Test
    public  void testBufferTimePerid(){
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .buffer(Duration.ofNanos(10))
                .subscribe(x -> System.out.println(x));
    }

在前面的代码中,我们做了以下操作:

  1. 我们根据 10 纳秒的时间切片将原始数据划分为分块

  2. 每个缓冲区列表都包含了在该时间段内发出的元素

  3. 最后,我们使用 subscribe 函数打印了列表

让我们运行这段代码以确认输出。我们可以看到斐波那契元素作为多个 List<Long> 发出:

图片

buffer 操作符提供了这里讨论的方法的多种变体。所有 buffer 方法都提供了一个列表,但只有重载方法之一允许我们将缓冲区转换为集合数据集。我们需要向重载的 buffer 操作符提供一个供应商函数。这个函数负责创建集合实例。让我们看看以下代码:

    @Test
    public  void testBufferSupplier(){
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator.take(100)
                .buffer(5,HashSet::new)
                .subscribe(x -> System.out.println(x));
    }

这里,我们做了以下操作:

  • 我们将原始数据集划分为最多包含五个元素的分块

  • 每个缓冲区都作为 HashSet 发出,这意味着它只包含不同的元素

  • 最后,我们使用 subscribe 函数打印了列表

由于我们使用了 HashSet,我们可以看到它不包含斐波那契数列的重复元素:

图片

窗口操作符

window() 操作符与 buffer() 操作符类似。它也会切割原始数据集,但将每个数据集作为处理器发出,而不是作为新的集合。每个处理器在订阅项目后发出项目。window 操作符允许我们有一个固定大小的窗口、基于时间的窗口或基于谓词的窗口。与允许我们为所有发布的元素构建单个缓冲区的 buffer 操作符不同,window 操作符不允许你在单个窗口中发布元素。

window() 操作符提供了更好的内存利用率,因为项目会立即发出,而不是首先被缓存到一个集合中,然后在达到正确的集合大小时再发出。window 操作符也比缓冲操作符提供了更好的内存使用。以下代码展示了这一点:

    @Test
     public  void testWindowsFixedSize(){
         Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                 Long>of(0L, 1L), (state, sink) -> {
             if (state.getT1() < 0)
                 sink.complete();
             else
                 sink.next(state.getT1());
             return Tuples.of(state.getT2(), state.getT1() + state.getT2());
         });
         fibonacciGenerator
                 .window(10)
                 .concatMap(x -> x)
                 .subscribe(x -> System.out.print(x+" "));
     }

在前面的代码中,我们做了以下操作:

  1. 我们将原始数据划分为每个最多包含 10 个元素的分块

  2. 每个窗口都是 UnicastProcesser 类型的一种,因此需要使用 ConcatMapflatMap 将其他生成的窗口与之结合

  3. 最后,我们使用 subscribe 函数打印了列表

让我们运行这段代码以确认输出。我们可以看到斐波那契元素作为多个批次发出,然后合并为一个:

图片

WindowUntilWindowWhile 变体接受一个谓词条件,并构建一个窗口批次,直到条件为真。WindowWhile 操作符生成一个包含所有匹配条件的单个窗口。另一方面,WindowUntil 操作符将不匹配的元素聚合到一个窗口中。当它找到一个匹配的元素时,它将其添加到当前窗口。然后它开始一个新的窗口以添加下一个传入的元素。考虑以下代码:

   @Test
    public  void testWindowsPredicate(){
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .windowWhile(x -> x < 500)
                .concatMap(x -> x)
                .subscribe(x -> System.out.println(x));
    }

在前面的代码中,我们做了以下操作:

  1. 我们根据条件 x < 500 对原始数据进行分区。

  2. 所有符合标准的数据元素都在一个窗口中发布。

  3. 窗口元素作为 WindowFlux 发射。它们使用 concatMapflatMap 进行组合。

  4. 最后,我们使用 subscribe 函数打印元素。

让我们运行代码以确认输出:

图片

样本操作符

groupBy(), buffer(), 和 window() 操作符将输入聚合并基于其大小、时间周期或条件将它们合并成块。它们的目标不是跳过事件。有时,你可能需要跳过事件并监听给定时间间隔内的特定事件。这通常适用于快速、不变化的事件,例如用户点击。在这种情况下,我们需要调节流量并选择性地获取数据。

sample() 操作符允许我们完成这种调节。它接受一个时间周期并监听该时间周期内发布的事件。然后它发布该时间周期内发生的最后一个事件。这在上面的图中有所展示:

图片

让我们尝试给我们的斐波那契数列添加延迟,然后进行调节:

    @Test
    public  void testSample() throws Exception{
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        CountDownLatch latch = new CountDownLatch(1);
        fibonacciGenerator
                .delayElements(Duration.ofMillis(100L))
                .sample(Duration.ofSeconds(1))
                .subscribe(x -> System.out.println(x), e -> latch.countDown() , () -> latch.countDown());
        latch.await();
    }

在前面的代码中,我们做了以下操作:

  1. 我们添加了 delayElements() 操作符。这个操作符负责将每个事件延迟指定的周期。在这种情况下,我们将每个元素延迟了 100 毫秒。

  2. 接下来,我们添加了时间间隔为一秒的 sample() 操作符。

  3. 然后我们使用 Subscribe 函数打印元素。

  4. 我们还添加了一个 CountDownLatch 来等待测试执行的完成/错误事件。

让我们运行代码以确认输出:

图片

samplefirst() 操作符与 sample() 操作符类似。这个操作符在指定的时间周期内发布接收到的第一个元素,而不是选择最后一个元素。

反压

反压是 Reactor 的一个基本组成部分。我们在前面的章节中多次讨论了它,但我们将在这里详细探讨这个主题。让我们回顾 Reactor 提供的开箱即用的反压支持。每个订阅者都使用订阅对象请求它可以处理的事件数量。发布者必须尊重这个限制,并发布小于或等于请求限制的事件。这在上面的图中表示:

图片

使用Long.MAX_VALUE调用请求意味着请求无界数量的事件。发布者可以推送尽可能多的事件。它不再受订阅者限制的约束。

在每个订阅者处理接收到的事件时,它可以使用订阅句柄请求额外的事件。如果发布者快速发布事件,它必须制定一个策略来处理未请求的事件。看看以下测试代码:

    @Test
    public void testBackPressure() throws  Exception{
        Flux<Integer> numberGenerator = Flux.create(x -> {
            System.out.println("Requested Events :"+x.requestedFromDownstream());
            int number = 1;
            while(number < 100) {
                x.next(number);
                number++;
            }
            x.complete();
        });

        CountDownLatch latch = new CountDownLatch(1);
        numberGenerator.subscribe(new BaseSubscriber<Integer>() {
            @Override
            protected void hookOnSubscribe(Subscription subscription) {
                request(1);
            }

            @Override
            protected void hookOnNext(Integer value) {
                System.out.println(value);
            }

            @Override
            protected void hookOnError(Throwable throwable) {
                throwable.printStackTrace();
                latch.countDown();
            }

            @Override
            protected void hookOnComplete() {
                latch.countDown();
            }
        });
        assertTrue(latch.await(1L, TimeUnit.SECONDS));
    }

在前面的代码中,发生了以下情况:

  1. 我们使用Flux.create API 创建了一个发布者

  2. 发布者将请求的数量打印到控制台,并发布了 100 个事件

  3. 订阅者在subscribe钩子中请求了一个事件

  4. 订阅者将接收的事件打印到控制台

  5. 有一个CountDownLatch用于暂停代码 1 秒钟

总结一下,订阅者请求了一个事件,但发布者发布了 100 个。让我们运行测试,看看控制台上的结果:

图片

前面的测试未能成功完成。我们的订阅者请求了一个事件,但它只收到了一个。然而,发布者推送了 100 个数据事件,然后是完整的事件。Reactor 在幕后做了一些工作,将事件保持在队列中。它提供了一些溢出策略来处理快速发布者产生的事件:

策略 描述
IGNORE 此策略忽略订阅者的反压限制,并继续向订阅者发送下一个事件。
BUFFER 此策略将未发送的事件组合在缓冲区中。当订阅者请求下一个事件时,缓冲区中的事件被发送。
DROP 此策略静默丢弃产生的未发送事件。只有当下一次请求被提出时,订阅者才会得到新产生的事件。
LATEST 此策略保留缓冲区中提出的最新事件。只有当下一次请求被提出时,订阅者才会得到最新产生的事件。
ERROR 如果生产者发布的比订阅者请求的事件多,此策略将引发OverFlowException

默认创建的 API 使用Overflow.Buffer策略。我们可以通过在重载的create方法中传递我们想要的策略来覆盖它。让我们用Overflow.Error策略测试前面的代码:

@Test
    public  void testBackPressure() throws  Exception{
        Flux<Integer> numberGenerator = Flux.create(x -> {
            System.out.println("Requested Events :"+x.requestedFromDownstream());
            int number = 1;
            while(number < 100) {
                x.next(number);
                number++;
            }
            x.complete();
        }, OverflowStrategy.ERROR);

      // Removed for Brevity
    }

测试用例现在失败,以下错误:

reactor.core.Exceptions$OverflowException: The receiver is overrun by more signals than expected (bounded queue...)
    at reactor.core.Exceptions.failWithOverflow(Exceptions.java:202)
    at reactor.core.publisher.FluxCreate$ErrorAsyncSink.onOverflow(FluxCreate.java:632)
    at reactor.core.publisher.FluxCreate$NoOverflowBaseAsyncSink.next(FluxCreate.java:603)
    at reactor.core.publisher.FluxCreate$SerializedSink.next(FluxCreate.java:151)

OnBackpressure

Reactor 还提供了操作符来更改与发布者配置的溢出策略。有各种OnBackpressureXXX()操作符,它们针对 Reactor 中可用的每种策略。这些在无法将前面的策略应用于发布者的场景中非常有用。

发布者有时被配置为使用IGNORE策略。在这种情况下,在订阅发布者时使用操作符来配置背压。

让我们使用我们的测试用例并对其应用背压操作符:

@Test
    public  void testBackPressureOps() throws  Exception{
        Flux<Integer> numberGenerator = Flux.create(x -> {
            System.out.println("Requested Events :"+x.requestedFromDownstream());
            int number = 1;
            while(number < 100) {
                x.next(number);
                number++;
            }
            x.complete();
        });

        CountDownLatch latch = new CountDownLatch(1);
        numberGenerator
                .onBackpressureDrop(x -> System.out.println("Dropped :"+x))
                .subscribe(new BaseSubscriber<Integer>() {
            // Removed for Brevity
        });
        assertTrue(latch.await(1L, TimeUnit.SECONDS));
    }

在前面的代码中,我们做了以下操作:

  1. 我们将Flux<Integer>配置为使用默认配置OverflowStrategy.BUFFER

  2. 在订阅Flux<Integer>时,我们更改了策略以使用OverflowStrategy.DROP

  3. 此外,我们还向操作符传递了一个 lambda 表达式来打印丢弃的值

让我们运行代码并验证输出:

此外,还有onBackpressureLatest()onBackpressureError()onBackpressureBuffer()操作符,它们与onBackpressureDrop()操作符类似。onBackpressureBuffer()操作符有几个重载变体。作为一个基本配置,它允许我们指定缓冲区大小。我们还可以指定以下策略之一来处理超出指定缓冲区的溢出:

缓冲区溢出 描述
DROP_LATEST 所有生成的事件首先被缓冲,然后丢弃新的事件。这将保持缓冲区中最老的事件。
DROP_OLDEST 所有生成的事件都被缓冲。然后,当前在缓冲区中的事件被新的事件替换。这将保持缓冲区中最新的事件。
ERROR 这会对超出缓冲区的事件抛出OverFlowException

让我们通过一个示例来看看它是如何工作的。我们还可以传递一个消费者 lambda 表达式来处理溢出事件:

    @Test
    public  void testBackPressureOps() throws  Exception{
        Flux<Integer> numberGenerator = Flux.create(x -> {
            System.out.println("Requested Events :"+x.requestedFromDownstream());
            int number = 1;
            while(number < 100) {
                x.next(number);
                number++;
            }
            x.complete();
        });

        CountDownLatch latch = new CountDownLatch(1);
        numberGenerator
                .onBackpressureBuffer(2,x -> System.out.println("Dropped :"+x),BufferOverflowStrategy.DROP_LATEST)
                .subscribe(new BaseSubscriber<Integer>() {
        // Removed for brevity
        });
        assertTrue(latch.await(1L, TimeUnit.SECONDS));
    }

在前面的代码中,我们做了以下操作:

  1. 我们将Flux<Integer>配置为使用默认配置OverflowStrategy.BUFFER

  2. 在订阅Flux<Integer>时,我们将缓冲区大小更改为两个元素

  3. 我们为超出缓冲区的事件配置了DROP_LATEST策略

  4. 我们还向操作符传递了一个 lambda 表达式来打印丢弃的值

让我们运行代码并验证输出:

摘要

在本章中,我们详细讨论了 Reactor 中可用的流控制操作符。我们探讨了 groupBybufferwindow 操作符中可用的不同重载选项。然后,我们考虑了如何使用样本操作符来节流事件,该操作符允许在指定的时间间隔内只传递一个事件。之后,我们回顾了 Reactor 中可用的背压支持,并研究了它提供的不同溢出策略。我们还了解到,Reactor 默认使用 Overflow.Buffer 策略,这可以作为 Flux.create API 的一部分提供。最后,我们讨论了可以用来改变生产者策略的背压操作符。总之,我们讨论了可用于流控制和背压的完整操作符列表。在下一章中,我们将探讨错误处理和恢复。

问题

  1. 为什么我们需要 groupBy 操作符?

  2. groupBybuffer 操作符之间的区别是什么?

  3. 我们如何在 Reactor 中节流一个事件?

  4. Overflow.IgnoreOverflow.Latest 策略之间的区别是什么?

  5. 哪些操作符可以改变生产者的背压策略?

第八章:处理错误

弹性是响应式系统的一个重要方面。根据响应式宣言,响应式系统必须在失败期间保持响应。系统必须优雅地处理错误,并及时生成用户响应。没有有效的错误处理机制是无法实现的。Reactor 提供了一些操作符来处理错误。在本章中,我们将查看每个操作符。

在本章中,我们将涵盖以下主题:

  • 处理错误

  • 错误操作符

  • 超时和重试

  • WebClient 错误处理

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章的 GitHub 链接为github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter08

生成错误

在我们尝试处理错误之前,让我们先尝试引发一些错误。在 Java 生态系统中,错误条件是通过抛出异常来引发的。以下条件下可以引发异常:

  • 生产者在生成下一个值时可以抛出异常。

  • 订阅者在处理下一个值或订阅事件,或在任何操作符中时可以抛出异常。

在所有上述条件下,必须有一个有效的错误处理程序来处理引发的错误。响应式流规范为同一目的指定了错误事件。规范指出,生产者应引发错误事件,而不是抛出异常。然而,规范没有讨论在处理订阅者中的事件时引发的异常。让我们开始处理我们的斐波那契数列,以了解 Reactor 中的错误处理是如何发生的:

    @Test
    public void testThrownException() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                throw new RuntimeException("Value out of bounds");
            else
                sink.next(state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .subscribe(System.out::println);
    }  

在先前的测试用例中,发生以下情况:

  1. 生成器实现当值变为负数时抛出RuntimeException

  2. 如果我们将此与第二章中创建的原始实现进行比较,《Reactor 中的发布者和订阅者 API》,我们现在不再引发完成事件。

  3. 订阅者中没有配置错误函数。

让我们运行测试用例以查看 Reactor 如何响应,如下所示:

图片

在先前的执行中,你可以看到以下情况:

  1. 所有正数值首先打印到控制台。

  2. 抛出的异常被传播到订阅者。

  3. 由于没有配置错误函数,订阅者引发ErrorCallbackNotImplemented异常。

  4. 异常导致测试用例失败。

在先前的测试执行中,请注意我们没有引发错误事件。然而,当抛出异常时,Reactor 引发了错误事件。然后该事件在订阅者端被处理。

现在,让我们增强我们的测试用例,并在处理订阅者中的事件时引发错误,以下代码:

@Test
    public void testThrownException() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                throw new RuntimeException("Value out of bounds");
            else
                sink.next(state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .subscribe(x -> throw new RuntimeException("Subscriber threw error"));
    }     

之前的代码现在执行以下操作:

  1. 在事件处理器中配置一个 lambda,而不是System.out.println函数。

  2. lambda 抛出RuntimeException,而不是将数字打印到控制台。

如果我们运行前面的测试案例,输出将与我们之前的执行相似。测试案例将失败,以下为堆栈跟踪:

Caused by: java.lang.RuntimeException: Subscriber threw error
    at ErrorHandlingTest.lambda$testThrownException$1(ErrorHandlingTest.java:16)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:202)
    at reactor.core.publisher.LambdaSubscriber.onSubscribe(LambdaSubscriber.java:89)
    at reactor.core.publisher.FluxGenerate.subscribe(FluxGenerate.java:83)

观察前面的两段输出,我们可以说 Reactor 以相同的方式处理生产者和订阅者抛出的异常。订阅者必须提供一个错误函数,以便 Reactive Streams 能够成功完成。

我们还有一个场景尚未处理。在生产者中,我们不是抛出RuntimeException,而是必须引发一个错误事件。这可以通过将throw new RuntimeException替换为sink.error(e)来实现,如下所示:

  @Test
    public void testErrorRaised() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.error(new RuntimeException("Value out of bounds"));
            else           
                // Rest removed for Brevity
        });
    }

我将把确定前一个测试案例输出的任务留给读者。所有讨论过的测试案例都失败了,因为缺少错误回调处理程序。因此,我们必须为订阅者定义一个错误函数。这可以通过在subscriber() API 中传递一个额外的 lambda 函数来实现。为此,请考虑以下代码:

   @Test
    public void testErrorRaised() {

        // Rest Removed for Brevity

        fibonacciGenerator
                .subscribe(System.out::println, System.out::println);
    }

在前面的代码中,我们在消费者和错误消费者事件中传递了println函数。因此,订阅者将同时将这两个事件打印到控制台。现在,运行我们之前失败的测试案例;它们将错误打印到控制台,然后成功完成。这如图所示:

图片

检查异常

我们不能从生产者和订阅者抛出检查异常。每个相应的 Reactor 方法都接受一个没有异常声明的Consumer函数。因此,实现不能抛出它。然而,存在一些场景,其中生产者调用资源,如文件,这些资源可以抛出检查异常。Reactor 提供了Exceptions实用类来处理这些场景。Exceptions类提供了一个propagate方法,可以将任何检查异常包装为非检查异常,如下所示:

      @Test
    public void testCheckedExceptions() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            try {
                raiseCheckedException();
            } catch (IOException e) {
                throw Exceptions.propagate(e);
            }
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .subscribe(System.out::println,
                 e -> Exceptions.unwrap(e));
    }

    void raiseCheckedException() throws IOException {
        throw new IOException("Raising checked Exception");
    }

在前面的代码中,我们做了以下操作:

  1. 检查raiseCheckedException方法是否抛出IOException

  2. 使用Exception.propagate包装异常并将其抛回

  3. 使用Exception.unwrap获取原始的检查异常

接下来,让我们从一些try...catch错误类型开始。

doOnError 钩子

我们在第二章中讨论了生命周期钩子,Reactor 中的发布者和订阅者 API。这些可以用于为每个生命周期事件配置回调。Reactor 提供了生命周期错误回调钩子来配置错误处理器。doOnError钩子允许我们消费错误并执行预期的功能。如果我们已经配置了doOnError钩子和错误回调,那么它们将由 Reactor 同时调用。以下代码展示了这一点:

    @Test
    public void testDoError() {
        // Removed for brevity
        fibonacciGenerator
                .doOnError(System.out::println)
                .subscribe(System.out::println, e -> e.printStackTrace());
    }

上述代码执行以下操作:

  1. doOnError钩子中配置println函数。此函数将错误打印到控制台。

  2. 在订阅者 API 中配置错误 lambda。实现打印抛出的异常的堆栈跟踪。

让我们运行前面的测试用例并验证控制台上打印的输出。两个错误函数同时被调用,如下所示:

图片

doOnTerminate钩子

doOnError生命周期钩子类似,存在doOnTerminate钩子。这是一个通用的钩子,在on completionon error流终止事件时被调用。与提供异常抛出的特定错误钩子不同,此钩子不提供任何类型的输入。它只是执行提供的 lambda 表达式。需要注意的是,doOnTerminate钩子在我们收到终止事件时立即被调用。它不会等待错误回调被处理。考虑以下代码:

    @Test
    public void testDoTerminate() {
       // Removed for brevity
        fibonacciGenerator
                .doOnTerminate(() -> System.out.println("Terminated"))
                .subscribe(System.out::println,e -> e.printStackTrace() );
    }

上述代码执行以下操作:

  1. doOnTerminate钩子中配置println函数。此函数将Terminated打印到控制台。

  2. 在订阅者 API 中配置错误 lambda。此实现打印抛出的异常的堆栈跟踪。

让我们运行前面的测试用例并验证控制台上打印的输出。两个函数同时被调用,如下所示:

图片

doOnTerminate生命周期钩子类似,存在一个doAfterTerminate生命周期钩子。此钩子在将关闭事件传递给订阅者之后被调用。就像doOnTerminate钩子一样,doAfterTerminate是一个通用的钩子,不提供任何事件。由于钩子在事件传递后被调用,它需要错误回调订阅者配置。如果我们不提供它,流将因ErrorCallbackNotImplemented异常而失败。

doFinally钩子

doOnError生命周期钩子类似,存在doFinally钩子。此钩子在流完成之后被调用。钩子执行提供的 lambda 表达式。需要注意的是,doFinally钩子在流关闭回调处理之后被调用,与之前讨论的doOnTerminate钩子不同,后者在收到关闭事件时立即被调用。考虑以下代码:

    @Test
    public void testDoFinally() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.error(new RuntimeException("Value out of bounds"));
            else
                sink.next(state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .doFinally( x -> System.out.println("invoking finally"))
                .subscribe(System.out::println, e -> e.printStackTrace());
    }

上述代码执行以下操作:

  1. fibonacciGenerator在负值上引发错误。

  2. 它在doFinally钩子中配置println函数。该函数将invoking finally打印到控制台。

  3. 它在订阅者 API 中配置错误 lambda。实现打印抛出的异常的堆栈跟踪。

让我们运行前面的测试用例并验证控制台上打印的输出,如下所示:

图片

作为doFinally钩子的替代,有Flux.using API。此 API 为发布者配置一个资源映射。它还配置了一个回调 lambda,当流关闭时,会使用相应的发布者资源调用它。这与 Java 的try-with-resource API 同义:

 @Test
    public void testUsingMethod() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        Closeable closable = () -> System.out.println("closing the stream");
        Flux.using(() -> closable, x -> fibonacciGenerator, e -> {
            try {
                e.close();
            } catch (Exception e1) {
                throw Exceptions.propagate(e1);
            }
        }).subscribe(System.out::println);
    }

前面的代码执行以下操作:

  1. 它通过调用Using API 生成一个Flux<Long>

  2. 它将一个closable实例映射到fibonacciGenerator的一个实例。

  3. 它在流完成时调用close方法。close方法可能会抛出检查型异常,因此使用Exceptions.propagate来包装错误。

让我们运行前面的测试用例并验证控制台上打印的输出,如下所示:

图片

错误恢复

在前面的部分中,我们确定了如何配置错误回调。然而,在执行错误处理时,我们可能会遇到我们想要使用一些替代值继续执行的情况。此类场景有众多用例。例如,报价聚合系统在获取最新 tick 值时可能会抛出错误,但聚合必须使用最后一个值继续。在接下来的章节中,我们将介绍每个提供的算子,以便完成此操作。

onErrorReturn算子

Reactor 提供了OnErrorReturn算子,在发生错误时提供回退值。由于回退,原始错误事件不会被传播到错误回调。事件处理通过使用事件处理器继续,如下所示:

    @Test
    public void testErrorReturn() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.error(new RuntimeException("Value out of bounds"));
            else
                sink.next(state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .onErrorReturn(0L)
                .subscribe(System.out::println);
    }

在前面的代码中,以下适用:

  1. 使用onErrorReturn算子,当订阅者收到错误时提供0

  2. 订阅者 API 中没有配置错误回调

让我们运行我们的测试用例并验证对前面代码的理解:

图片

onErrorReturn算子也提供了针对特定异常的处理。这是一个重载方法,它接受异常类以及一个回退值。Reactor 通过验证抛出的异常是否是配置的异常的实例来选择它找到的第一个匹配项。因此,我们必须首先配置最具体的异常匹配,最后配置最通用的异常匹配。现在,让我们编写一个测试用例来验证异常处理,如下所示:

      @Test
    public void testErrorReturn() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.error(new IllegalStateException("Value out of bounds"));
         // Removed for Brevity
        });
        fibonacciGenerator                
                .onErrorReturn(RuntimeException.class,0L)
                .onErrorReturn(IllegalStateException.class,-1L)
                .subscribe(System.out::println);
    }

在前面的代码中,我们现在抛出的是IllegalStateException,而不是RuntimeExceptionIllegalStateExceptionRuntimeException的一个子类型。订阅者被配置为处理这两种异常。在此处需要注意配置的顺序。RuntimeException被首先配置,默认值为0,而IllegalStateException的值为-1。Reactor 会将抛出的异常与RuntimeException进行匹配。在这里运行测试用例并验证结果。

最后,还有一个onErrorReturn,它通过验证与提供的谓词匹配来匹配异常。配置的谓词接受抛出的异常作为输入,并返回一个布尔结果。在这里,我们也配置了多个谓词。Reactor 将选择第一个匹配的谓词并使用其回退值。

onErrorResume运算符

OnErrorReturn运算符类似,存在OnErrorResume运算符,它提供一个回退值流而不是单个回退值。在发生错误的情况下,返回回退流。原始错误事件不会被传播到错误回调。事件处理通过使用配置的事件处理器继续,如下所示:

    @Test
    public void testErrorResume() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
          // Removed for Brevity
        });
        fibonacciGenerator
                .onErrorResume(x -> Flux.just(0L,-1L,-2L))
                .subscribe(System.out::println);
    }

在前面的代码中,以下适用:

  1. onErrorResume运算符用于在订阅者收到任何错误时提供回Flux<Long>

  2. 订阅者 API 中没有配置错误回调。

让我们运行我们的测试用例并验证我们的理解,如下所示:

onErrorReturn的行类似,onErrorResume运算符被重载以提供基于特定异常的回退值。异常可以直接提供,或者可以使用谓词进行匹配。Reactor 将选择第一个匹配的值。

onErrorMap运算符

Reactor 的onErrorMap运算符允许我们将一种类型的异常转换为另一种类型。与前面的两个运算符不同,onErrorMap运算符需要与订阅者一起配置错误回调。如果没有配置处理程序,则订阅者会抛出ErrorCallbackNotImplemented异常。onErrorMap运算符提供了重载函数,类似于前面的运算符,可以用于根据类型或提供的谓词匹配异常。

现在,让我们构建一个简单的测试用例来验证我们对onErrorMap运算符的理解:

    @Test
    public void testErrorMap() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
           // Removed for brevity
        });
        fibonacciGenerator
                .onErrorMap(x -> new IllegalStateException("Publisher threw error", x))
                .subscribe(System.out::println,System.out::println);
    }

在前面的代码中,以下适用:

  1. onErrorMap运算符配置为在订阅者收到任何错误时抛出IllegalStateException

  2. 错误回调是在订阅者 API 中配置的。

让我们运行它并确认输出,如下所示:

超时

如前几节所述,及时响应是反应式系统的一个重要方面。这个要求意味着反应式系统必须及时提供确定性的响应。然而,所有软件系统本质上都是不可靠的。底层网络本身是不可靠的。所有组件都可能失败而不提供响应。因此,具有流式结果系统的可能会卡在等待响应的状态。

解决这种不可靠性的方法之一是采用快速失败系统设计。这种设计规定系统对正常操作做出一些假设,并且一旦这些假设被打破,系统必须立即失败。这导致可能的问题被提前报告。为了做到这一点,我们必须假设一个可能的响应时间,这是最常见的快速失败指标。如果在规定时间内没有收到响应,那么系统必须触发后备/错误响应。

Reactor 提供了 timeout() 操作符以启用响应时间检查。当在特定时间间隔内没有收到响应时,超时失败。一旦超时到期,它将触发为订阅者配置的错误回调。让我们通过以下代码验证操作符:

    @Test
    public void testTimeout() throws  Exception{
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                throw new RuntimeException("Value out of bounds");
            else
                sink.next(state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        CountDownLatch countDownLatch = new CountDownLatch(1);
        fibonacciGenerator
                .delayElements(Duration.ofSeconds(1))
                .timeout(Duration.ofMillis(500))
                .subscribe(System.out::println, e -> {
                    System.out.println(e);
                    countDownLatch.countDown();
                });
        countDownLatch.await();
    }

在前面的代码中,以下适用:

  1. delayElements 操作符负责通过配置的时间减慢每个元素。在我们的例子中,它在一秒后发送每个元素。

  2. timeout() 操作符被配置为 500 毫秒的间隔。当它首次发现延迟超过 500 毫秒时,此操作符将引发错误。

  3. 为订阅者配置了 onError 回调。我们还添加了一个 CountDownLatch,因为我们想保持测试执行直到接收到错误。

让我们运行它并确认输出,如下所示:

图片

timeout() 操作符还提供在超时触发时提供后备 Flux<> 值的功能。在这种情况下,后备值不会抛出超时错误。因此,它不会触发配置的错误回调。相反,流程作为后续事件执行,如下所示:

    @Test
    public void testTimeoutWithFallback() throws  Exception{
     // Removed for brevity 
    fibonacciGenerator
                .delayElements(Duration.ofSeconds(1))
                .timeout(Duration.ofMillis(500),Flux.just(-1L))
                .subscribe(e -> {
                    System.out.println(e);
                    countDownLatch.countDown();
                });
        countDownLatch.await();
    }

在前面的代码中,以下适用:

  1. delayElements 操作符负责通过配置的时间减慢每个元素。在我们的例子中,它在一秒后发送每个元素。

  2. timeout() 操作符被配置为 500 毫秒的间隔。当它首次发现延迟超过 500 毫秒时,此操作符将引发错误。操作符还有一个后备的 Flux。一旦超时到期,将返回后备值。

  3. onNext 处理器被配置为打印接收到的值。我们添加了一个 CountDownLatch,因为我们想保持测试执行直到接收到值。

  4. 没有配置 onError 回调。

让我们运行它并验证输出,如下所示:

图片

当我们讨论错误和超时时,重要的是要提到 retry 操作符。此操作符允许我们在发现错误时重新订阅发布的流。重试只能执行固定次数。重新订阅的事件由订阅者作为后续事件处理。如果流正常完成,则不会进行后续的重试。错误回调仅在最后重试周期中抛出错误时触发:

    @Test
    public void testRetry() throws  Exception{

      // Removed for brevity

        CountDownLatch countDownLatch = new CountDownLatch(1);
        fibonacciGenerator
                 .retry(1)
                .subscribe(System.out::println, e -> {
                    System.out.println("received :"+e);
                    countDownLatch.countDown();
                },countDownLatch::countDown);
        countDownLatch.await();
    }

在前面的代码中,以下适用:

  1. delayElements运算符负责通过配置的时间减慢每个元素。在我们的例子中,它在一秒延迟后发送每个元素。

  2. timeout()运算符被配置为500毫秒的间隔。当运算符首次发现延迟超过500毫秒时,它将引发一个错误。运算符还有一个回退 Flux。一旦超时到期,将返回回退值。

  3. onNext处理器被配置为打印接收到的值。我们添加了一个CountDownLatch,因为我们想保持测试执行直到接收到值。

  4. 没有配置onError回调。

让我们运行它并验证输出,如下所示:

WebClient

在第六章“动态渲染”中,我们讨论了使用 Spring WebClient 以非阻塞和异步方式执行网络调用。我们迄今为止讨论的运算符适用于反应式流发布者和订阅者。WebClient 还产生一个ServerResponse的单子发布者。那么,我们应该如何处理 WebClient 中生成的错误并生成有效的响应?首先,让我们看看 WebClient 对服务器端错误的默认处理。为此,我们应该首先在我们的 Fibonacci 处理函数中生成错误,如下所示:

   @Bean
    RouterFunction<ServerResponse> fibonacciEndpoint() {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            throw new RuntimeException("Method unsupported");         
        });
      RouterFunction<ServerResponse> fibonacciRoute =
                RouterFunctions.route(RequestPredicates.path("/fibonacci"),
                        request ->  ServerResponse.ok()
                                    .body(fromPublisher(fibonacciGenerator, Long.class)));

        return fibonacciRoute;
    }

在前面的代码中,我们修改了我们的生成器以引发RuntimeException。异常将在服务器构建响应时立即引发。这反过来又发送了带有异常消息主体的 HTTP 500 状态错误:

或者,我们可以使用sink.error()方法引发错误。此方法将接受一个异常实例并将其抛回。它还将引发一个 500 状态码,并带有out of bound错误消息,如下所示:

@Bean
RouterFunction<ServerResponse> fibonacciEndpoint() {
    Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
            Long>of(0L, 1L), (state, sink) -> {
        if (state.getT1() < 0)
            sink.error(new RuntimeException("out of bound"));
        else
            sink.next(state.getT1());
        return Tuples.of(state.getT2(), state.getT1() + state.getT2());
    });

    // Rest removed for Brevity

    return fibonacciRoute;
}

我们将通过使用 WebClient 调用前面的 URL 来理解其默认行为。让我们回顾一下第六章“动态渲染”中的 WebClient 示例:

Flux<Long> result = client.get()
        .uri("/fibonacci")
        .retrieve().bodyToFlux(Long.class)
        .limitRequest(10L);
result.subscribe( x-> System.out.println(x));

在前面的代码中,以下适用:

  1. 我们调用了/fibonacci URL。

  2. 我们使用retrieve方法转换了主体。

  3. 我们使用限制运算符选择了 10 个结果。

  4. 最后,结果被打印到控制台。

注意,没有配置显式的错误处理器;运行代码以查看其响应。当它从服务器接收到错误状态码时,代码不会生成任何输出或转换主体。或者,让我们在订阅者方法中配置一个错误处理器并打印异常,如下所示:

result.subscribe( x-> System.out.println(x), e-> e.printStackTrace);

现在,让我们执行 WebClient 代码以确定输出:

org.springframework.web.reactive.function.client.WebClientResponseException: ClientResponse has erroneous status code: 500 Internal Server Error
    at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.lambda$createResponseException$7(DefaultWebClient.java:464)
    at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:100)
    at 
 ...............

有趣的是,这次我们可以看到一个带有状态码作为错误信息的WebClientResponseException。如果我们查看WebClientResponseException类,该异常允许我们获取响应文本、状态码等。将错误类型转换为字符串并打印响应文本将生成以下输出:

{"timestamp":1533967357605,"path":"/fibonacci","status":500,"error":"Internal Server Error","message":"Unsupported Method"}

注意 WebClient API 的行为。尽管流生成了错误,但我们从未看到ErrorCallbackNotImplemented异常,这与没有配置错误处理器的 Reactive Stream 订阅者的行为不同。

WebClient 与之前章节中讨论的onError运算符配合良好。我们可以配置onErrorReturnonErrorResume运算符。这将提供回退值,在发生错误时返回,如下所示:

Flux<Long> result = client.get()
        .uri("/fibonacci")
        .retrieve()
        .bodyToFlux(Long.class)
        .onErrorResume( x -> Flux.just(-1L, -2L))
        .limitRequest(10L);
result.subscribe( x-> System.out.println(x));

现在,执行前面的代码并确认输出中的回退值。

在这里,检索 API 的 WebClient 还提供了一个onStatus方法来配置响应处理。onStatus方法接受异常映射并对其配置的 HTTP 状态码进行调用。在我们的前一个示例中,让我们尝试为 500 服务器响应抛出一个RuntimeException

Flux<Long> result = client.get()
        .uri("/fibonacci")
        .retrieve()
        .onStatus(HttpStatus::isError, x -> Mono.error(new 
         RuntimeException("Invalid Response ")))
        .bodyToFlux(Long.class)
        .limitRequest(10L);

在前面的代码中,以下适用:

  • RuntimeException作为Mono.error被抛出。

  • Mono 被配置为处理所有 HTTP 错误状态码(4XX5XX)。

当执行前面的代码时,会抛出一个RuntimeException。然而,与之前的操作不同,这次异常导致了一个ErrorCallbackNotImplemented异常,而WebClientResponseException没有要求异常处理器:

reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.RuntimeException: Invalid Response 
Caused by: java.lang.RuntimeException: Invalid Response 

现在,我们可以配置异常映射或回退值提供程序来从抛出的异常中恢复。

摘要

在本章中,我们探讨了为我们的应用程序添加弹性的各种方法。首先,我们涵盖了涉及生产者和订阅者的可能错误场景。接下来,我们研究了 Reactor 在每种条件下如何执行错误处理。这使得我们能够通过使用提供的各种操作来配置 Reactor 中的所需错误处理。Reactor 允许我们通过使用onErrorReturnonErrorResume运算符来配置抛出异常的回退值。我们还使用可用的运算符配置了超时和重试机制,以生成及时响应。最后,我们在 WebClient 中配置了错误处理。总的来说,我们探讨了在 Reactor 中配置错误处理器的可能方法。

问题

  1. 在 Reactor 中如何处理错误?

  2. 哪些运算符允许我们配置错误处理?

  3. onErrorResumeonErrorReturn之间的区别是什么?

  4. 我们如何为 Reactive Stream 生成及时响应?

  5. retry运算符的行为如何?

第九章:执行控制

在整本书中,我们一直在使用 Reactor 操作符。这包括执行各种任务,如过滤、转换和收集。大多数操作符不会创建额外的线程,只是在主线程上工作。然而,我们可以通过使用一组调度器来配置 Reactor 的多线程和并发。

在本章中,我们将涵盖以下主题:

  • 调度器

  • 并行处理

  • 广播

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE,2018.1 或更高版本

本章的 GitHub 链接是github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter09

调度器

Reactor 使用调度器之一来执行所有操作。Reactor 调度器不属于java.util.concurrent API。Java 并发 API 相当底层,我们可以从中启动和控制任务执行。另一方面,Reactor 链中的所有任务都由 Reactor 引擎执行。因此,我们不需要低级 API 来管理任务执行。相反,Reactor 提供了一个声明性模型,我们可以使用它来配置Scheduler并改变链式执行的行怍。

在我们开始配置 Reactor 之前,让我们首先确定默认的执行模型。默认情况下,Reactor 主要是单线程的。发布者和订阅者不会为它们的执行创建额外的线程。所有生命周期钩子和大多数操作符都执行单线程操作。在我们继续之前,让我们编写一些代码来验证这一点,如下所示:

    @Test
    public void testReactorThread() throws Exception{
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
               sink.complete();
            else
                sink.next(state.getT1());
            print("Generating next of "+ state.getT2());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                })
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
                .subscribe(x -> print("Sub received : "+x));
    }

    static void print(String text){
        System.out.println("["+Thread.currentThread().getName()+"] "+text);
    }

在前面的代码中,以下规则适用:

  1. 我们使用filter操作符和生命周期钩子构建了一个简单的斐波那契链。

  2. 每个操作都使用print函数打印到控制台。

  3. print函数打印当前线程名称以及文本。

以下屏幕截图显示了一个简单的调试代码片段,它允许我们看到 Reactor 如何执行流式操作。让我们运行它看看效果:

在前面的屏幕截图中,我们可以看到所有文本都带有前缀 [main]。因此,所有操作都在主线程上执行,Reactor 没有使用额外的线程。这个输出验证了 Reactor 默认是单线程的。由于单线程执行,我们没有使用Thread.sleeplatch.wait来暂停测试执行。

然而,前面的概念只是部分正确;Reactor 操作符确实会改变链式执行的行怍。以前,我们在测试用例中使用了latchThread.sleep来处理延迟和超时操作符。让我们将操作符添加到测试用例中,并分析输出,如下所示:

 @Test
    public void testReactorDelayThread() throws Exception{
        // Removed for brevity

        fibonacciGenerator
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                }).delayElements(Duration.ZERO)
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
                .subscribe(x -> print("Sub received : "+x));
        Thread.sleep(500);
    }

在前面的代码中,以下规则适用:

  1. 我们在filter操作符之后添加了delayElements操作符。

  2. 测试现在迅速终止,因此我们需要添加Thread.sleep来暂停主线程的执行。暂停确保整个链被执行。

让我们运行它并分析输出,如下所示:

截图

通过查看前面的输出,我们可以推断出以下内容:

  • 发布者不会创建线程;它在主线程中执行。

  • filter操作不会创建线程;它在主线程中执行。

  • delayElements操作添加了一个由parallel-1parallel-2表示的两个线程的线程池。

  • 链接现在在线程池中执行,而不是主线程。

现在您已经了解了 Reactor 的线程模型,让我们讨论我们可以如何配置它。

Reactor 调度器

如前节所述,Reactor 操作符配置反应链执行行为。然而,可以通过使用不同的调度器来改变这种行为。大多数操作符都有重载方法,这些方法接受一个调度器作为参数。在本节中,我们将查看 Reactor 中可用的各种调度器。Reactor 还提供了一个schedulers实用类,用于构建可用实现的实例。

立即调度器

Schedulers.immediate调度器在当前执行的线程上执行工作。所有任务都在调用线程上执行,没有任务以并行方式执行。这是大多数 Reactor 任务默认的执行模型。考虑以下代码:

@Test
  public void testImmediateSchedular() throws Exception{

       // Removed for Brevity

        fibonacciGenerator
                .delayElements(Duration.ofNanos(10),Schedulers.immediate())
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
            .subscribe(x -> print("Sub received : "+x));
      Thread.sleep(500);
 }

在前面的代码中,发生了以下情况:

  1. 我们向我们的链中添加了delayElements操作符。

  2. 测试尝试在主线程上调度延迟。

我们可以执行代码,但任务将失败,因为主线程缺乏基于时间的调度能力。以下截图显示了这一点:

截图

单个调度器

Schedulers.single调度器在单个工作线程池上执行工作。由于这是一个单个工作者,所有任务都是逐个执行的,没有任务是以并发方式执行的。调度器对于将非线程安全操作的执行隔离到单个线程中非常有用。考虑以下代码:

@Test
  public void testSingleScheduler() throws Exception{

       // Removed for Brevity

        fibonacciGenerator
                .delayElements(Duration.ofNanos(10),Schedulers.single())
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
           .subscribe(x -> print("Sub received : "+x));
      Thread.sleep(500);
 }

在前面的代码中,发生了以下情况:

  1. 我们向我们的链中添加了delayElements操作符。

  2. 测试尝试在单个线程上调度延迟,而不是在测试执行的主线程上。

从输出中,我们可以验证链中的所有任务都是在single-1线程上执行的。考虑以下截图:

截图

这里,single调度器是用来执行非阻塞、计算密集型操作的。这可以被视为一个事件循环,在其队列中执行非阻塞任务。如果我们调用任何反应式阻塞 API,调度器会抛出以下错误:

 @Test
    public void testSingleSchedulerBlockingOps() throws Exception{
       // Removed for Brevity
        fibonacciGenerator
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                }).delayElements(Duration.ZERO,Schedulers.single())
                .window(10)
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "+x))
                .subscribe(x -> print("Sub received : "+x.blockFirst()));
        Thread.sleep(500);
    }

除了之前讨论的链之外,在之前的代码中还发生了以下情况:

  1. 我们调用了window操作符来生成每个包含10个元素的批次。

  2. 订阅者调用了blockFirst API 来获取第一个元素。

执行前面的代码会导致以下异常:

图片

并行调度器

Schedulers.parallel调度器在多个工作线程池上执行工作。它根据可用的处理器数量创建工作线程。这是在 Reactor 操作符中使用的默认调度器。考虑以下代码:

@Test
    public void testParalleScheduler() throws Exception{

       // Removed for Brevity

        fibonacciGenerator
                .delayElements(Duration.ofNanos(10),Schedulers.parallel())
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
           .subscribe(x -> print("Sub received : "+x));
      Thread.sleep(500);
 }

从输出中,我们可以验证链中的所有任务都是在paralle-1parallel-2线程上执行的。查看以下截图:

图片

single调度器类似,parallel调度器旨在执行非阻塞任务。如果操作调用了任何响应式阻塞 API,调度器将抛出以下异常:

Caused by: java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread parallel-1
    at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:77)
    at reactor.core.publisher.Flux.blockFirst(Flux.java:2013)
    at SchedulerTest.lambda$testSingleSchedulerBlockingOps$27(SchedulerTest.java:116)

弹性调度器

Schedulers.elastic调度器在多个工作线程池上执行工作。每个执行的工作线程都可以执行需要阻塞操作的长任务。任务完成后,每个工作线程返回到池中。与工作线程相关联的还有空闲时间,在此之后,工作线程将被销毁。调度器试图消耗现有的空闲工作线程,如果没有,调度器将动态生成一个,并将任务调度到它上面。以下代码展示了这一点:

@Test
    public void testElasticSchedular() throws Exception{

       // Removed for Brevity

        fibonacciGenerator
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                }).delayElements(Duration.ZERO,Schedulers.elastic())
                .window(10)
                .doOnNext(x -> print("Next value is  "+ x))
                .doFinally(x -> print("Closing "+x))
                .subscribe(x -> print("Sub received : "+x.blockFirst()));
      Thread.sleep(500);
 }

与之前的工人类似,一个阻塞的响应式调用在弹性调度器上成功执行。查看以下截图:

图片

ExecutorService 调度器

Schedulers.fromExecutor使我们能够在 Java ExecutorService上构建一个调度器。调度器不拥有线程生成,而是由底层的ExecutorService控制。不应优先考虑其他调度器,因为ExecutorService的生命周期必须由开发者管理。考虑以下代码:

 @Test
    public void testExecutorScheduler() throws Exception{
        // Removed for Brevity

        ExecutorService executor = Executors.newSingleThreadExecutor();
        fibonacciGenerator
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                }).delayElements(Duration.ZERO,Schedulers.fromExecutor(executor))
                 .doOnNext(x -> print("Next value is  "+ x))
                .doFinally(x -> print("Closing "+executor.isShutdown()))
                .subscribe(x -> print("Sub received : "+x));
        Thread.sleep(5000);
        print("Is shutdown ? "+executor.isShutdown());
    }

在以下输出中,我们可以验证在执行我们的响应式链之后,服务仍在运行:

图片

并行处理

Reactor 发布者和订阅者不会创建线程。然而,如前节所示,有一些操作符可以改变这种行为。在最后一节中,我们看到了delay操作符将 Reactor 链的执行从主线程移动到调度线程。但是,我们不需要延迟/超时操作符来切换执行。Reactor 提供了publishOnsubscribeOn操作符来切换链执行。这两个操作符都将响应式链的执行上下文更改为配置的调度器。

PublishOn 操作符

publishOn操作符在执行链中配置的点拦截发布者的事件,并将它们发送到链的其余部分的另一个调度器。因此,该操作符改变了下游反应链的线程上下文。需要注意的是,该操作符仅影响下游事件链。它不改变上游链,并让上游执行保持默认执行模型。以下代码展示了这一点:

    @Test
    public void testReactorPublishOn() throws Exception{
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            print("Generating next of "+ state.getT2());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator
                .publishOn(Schedulers.single())
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                })
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
                .subscribe(x -> print("Sub received : "+x));
        Thread.sleep(500);
    }

在前面的代码中,以下规则适用:

  1. 我们在filter操作符之前配置了publishOn操作符。这应该会保留主线程上的生成,并在调度器上执行链的其余部分。

  2. 我们为链式执行配置了single调度器。

  3. 由于我们不在主线程上执行链,我们必须暂停测试执行一段时间。这是通过使用Thread.sleep来实现的。

让我们执行测试用例并确定输出。发布者在main线程上生成事件,然后传递到single-1线程,如下面的截图所示:

SubscribeOn 操作符

subscribeOn操作符拦截执行链中的发布者事件,并将它们发送到链的另一个调度器。需要注意的是,该操作符改变了整个链的执行上下文,与仅改变下游链执行的publishOn操作符不同:

@Test
    public void testReactorSubscribeOn() throws Exception{
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            print("Generating next of "+ state.getT2());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator                
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                })
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
                .subscribeOn(Schedulers.single())
                .subscribe(x -> print("Sub received : "+x));
      Thread.sleep(500);
 }

在前面的代码中,我们做了以下操作:

  1. 在订阅之前配置了subscribeOn操作符。

  2. 配置了用于链式执行的single调度器。

  3. 由于我们不在主线程上执行链,我们必须暂停测试执行一段时间。这是通过使用Thread.sleep来实现的。

让我们执行测试用例并验证输出。所有事件都是在由subscribeOn操作符配置的单个线程上生成的:

我们在同一个链中有了subscribeOnpublishOn操作符。subscribeOn操作符将在配置的调度器上执行完整的反应链。然而,publishOn操作符将下游链改为指定的调度器。它将上游链留在由subscribeOn调度器配置的调度器上:

 @Test
    public void testReactorComposite() throws Exception{
      // Removed for Brevity
        fibonacciGenerator
                .publishOn(Schedulers.parallel())
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                })
                .doOnNext(x -> print("Next value is  "+x))
                .doFinally(x -> print("Closing "))
                .subscribeOn(Schedulers.single())
                .subscribe(x -> print("Sub received : "+x));
        Thread.sleep(500);
    }

前面的代码将在由subscribeOn操作符配置的single-1调度器上生成事件。链的其余部分将在由publishOn操作符配置的并行调度器上执行。

下面是运行前面代码后的输出:

ParallelFlux

Reactor 提供了 ParallelFlux,它可以将现有流分割成多个流,采用轮询方式。ParallelFlux 是通过 parallel 操作符从现有 Flux 创建的。默认情况下,它会将流分割成可用的 CPU 核心总数。ParallelFlux 只分割流,不改变执行模型。相反,它在默认线程——主线程上执行流。分割的流可以通过 runOn 操作符进行并行处理配置。类似于 publishOn 操作符,runOn 接收一个调度器,并在指定的调度器上执行下游操作。

需要注意的是,ParallelFlux 不提供 doFinally 生命周期钩子。可以通过使用 sequential 操作符将其转换回 Flux,然后可以使用 doFinally 钩子进行配置:

@Test
    public void testParalleFlux() throws Exception{
      // Removed for Brevity

        fibonacciGenerator
                .parallel()
                .runOn(Schedulers.parallel())
                .filter(x -> {
                    print("Executing Filter");
                    return x < 100;
                })
                .doOnNext(x -> print("Next value is  "+x))
                .sequential()
                .doFinally(x -> print("Closing "))
                .subscribeOn(Schedulers.single())
                .subscribe(x -> print("Sub received : "+x));
        Thread.sleep(500);
    }

在前面的代码中,以下适用:

  1. parallel 操作符配置为从 fibonacciGenerator 生成 ParallelFlux

  2. runOn 操作符用于在并行调度器上配置 ParallelFlux

  3. 使用 sequential 操作符将 ParallelFlux 转换为 Flux。

  4. doFinallysequential Flux 上进行配置。

  5. subscribeOn 配置为在单个线程上执行 Flux 生成。

让我们运行代码并验证输出,如下所示:

到目前为止,我们已经讨论了如何并行执行流操作。在下一节中,我们将同时将事件传递给所有订阅者,并为它们配置并行处理。

广播

在网络中,广播 被定义为向多个接收者同时发布事件。在反应式流中,这意味着向多个订阅者同时发布事件。到目前为止,我们已经订阅了冷发布者,其中每个订阅都会生成一系列新的事件。我们甚至订阅了热发布者,发布者会不断推送事件,而不等待订阅者。每个订阅者都会在事件生成后立即收到相同的事件。热发布者可能看起来像广播事件,但在事件生成流的开始方面有一个关键区别。Reactor 允许我们创建一个 ConnecatableFlux,它能够在开始事件生成之前等待 n 个订阅者。然后它会继续将每个事件发布给所有订阅者。

重放操作符

Reactor 提供了 replay 操作符,用于将 Flux 转换为 ConnectableFlux。生成的 ConnectableFlux 会持续缓冲发送给第一个订阅者的事件。缓冲区可以配置为保留最后 n 个条目,或者可以配置为基于时间长度。只有缓冲的事件会被回放给订阅者。

参考以下图表:

在开始发布事件之前,必须由 n 个订阅者订阅 ConnectableFluxConnectableFlux 提供以下操作符来管理订阅者:

  • Connect:必须在足够的订阅已经完成之后调用 connect 操作符。我们必须自己管理订阅计数。订阅取消也必须由开发者跟踪。

  • Auto-ConnectautoConnect 操作符配置了一个订阅计数。这会动态跟踪对发布者的订阅。最好使用 autoConnect 操作符,并将订阅管理留给 Reactor。

让我们看一下以下代码:

   @Test
    public void testReplayBroadcast() throws Exception{
        // Removed for Brevity
        Flux<Long> broadcastGenerator=fibonacciGenerator.doFinally(x -> {
            System.out.println("Closing ");
        }).replay().autoConnect(2);

        fibonacciGenerator.subscribe(x -> System.out.println("[Fib] 1st : "+x));
        fibonacciGenerator.subscribe(x -> System.out.println("[Fib] 2nd : "+x));

        broadcastGenerator.subscribe(x -> System.out.println("1st : "+x));
        broadcastGenerator.subscribe(x -> System.out.println("2nd : "+x));
      }

在前面的代码中,你可以看到以下:

  1. broadcastGenerator 是使用 replay 操作符从 fibonacciGenerator 生成的。

  2. broadcastGenerator 在开始事件发布之前等待两个订阅者。

  3. fibonacciGenerator 也被订阅了两次。

  4. broadcastGenerator 也被订阅了两次。

在前面的代码中,我们已经对 fibonacciGeneratorbroadcastGenerator 发布者进行了两次订阅。让我们运行测试用例并验证输出,如下所示:

在前面的截图(输出)中,我们可以看到每当相应的发布者请求下一个值时,fibonacciGenerator 发布者就会被调用。然而,broadcastGenerator 发布者只被调用一次,在生成下一个值之前,相同的值会被发布给两个订阅者。

在前面章节中讨论的 connectautoConnect 操作符,只跟踪订阅事件。这些操作符在达到配置的计数时开始处理事件。它们会一直发布事件,直到发布者发送一个终止事件。这些操作符不跟踪订阅者取消订阅;一旦事件生成开始,即使订阅者已经取消了订阅,它也会继续生成事件。

Reactor 为之前讨论的情况提供了一个 refCount 操作符。refCount 操作符也跟踪订阅情况。如果所有订阅者都取消了订阅,它将停止生成新事件,如下所示:

@Test
    public void testBroadcastWithCancel() throws Exception{
        // removed for brevity

       fibonacciGenerator=fibonacciGenerator.doFinally(x ->  System.out.println("Closing "))
       .replay().autoConnect(2);

        fibonacciGenerator.subscribe(new BaseSubscriber<Long>() {
            @Override
            protected void hookOnSubscribe(Subscription subscription) {
                request(1);
            }

            @Override
            protected void hookOnNext(Long value) {
                System.out.println("1st: "+value);
                cancel();
            }
        });

        fibonacciGenerator.subscribe(new BaseSubscriber<Long>() {
            @Override
            protected void hookOnNext(Long value) {
                System.out.println("2nd : "+value);
                cancel();
            }
        });
        Thread.sleep(500);

    }

在前面的代码中,以下适用:

  • 在开始事件发布之前,fibonacciGenerator 被配置为两个订阅者。

  • 每个订阅者请求一个事件。

  • 每个订阅者在处理生成的事件时取消其订阅。

让我们运行以下测试用例以获取输出,如下所示:

在前面的输出中,我们可以看到在流关闭之前生成了完整的斐波那契数列。订阅者没有请求超过一个事件。现在,让我们将 autoConnect 替换为 refCount,并比较输出:

在前面的输出中,你可以看到,一旦所有订阅者取消了订阅,流就会立即关闭。现在,如果新的订阅者到达ConnectedFlux,序列将从第一个事件生成。

发布操作符

Reactor 提供了publish操作符来生成ConnectedFlux。与缓冲第一个订阅者接收到的事件的replay操作符不同,publish操作符从源流获取事件。该操作符跟踪其订阅者提出的请求。如果任何订阅者没有提出请求,它将暂停事件生成,直到所有订阅者都提出新的请求。考虑以下图示:

就像replay操作符一样,发布者生成的ConnectedFlux也需要订阅者管理。在这里,我们可以通过以下三种选项之一来配置它——connectautoConnectrefCount

 @Test
    public void testPublishBroadcast() throws Exception{
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            System.out.println("generating next of "+ state.getT2());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        fibonacciGenerator=fibonacciGenerator.doFinally(x -> {
            System.out.println("Closing ");
        }).publish().autoConnect(2);

        fibonacciGenerator.subscribe(new BaseSubscriber<Long>() {
            @Override
            protected void hookOnSubscribe(Subscription subscription) {
                request(1);
            }

            @Override
            protected void hookOnNext(Long value) {
                System.out.println("1st: "+value);
            }
        });

        fibonacciGenerator.subscribe(x -> System.out.println("2nd : "+x));
        Thread.sleep(500);

    }

在前面的代码中,以下适用:

  1. 在开始事件发布之前,fibonacciGenerator被配置为两个订阅者。

  2. 第一个订阅者只请求一个事件。

  3. 第二个订阅者不对事件数量施加限制。

让我们运行测试用例来分析输出,如下所示:

在前面的输出中,你可以看到只生成了第一个事件。也没有关闭事件,因为流正在等待订阅者一的下一个事件请求。因此,流没有终止。测试在等待了 500 毫秒后完成。

摘要

在本章中,我们探讨了 Reactor 执行模型。我们发现发布者和订阅者 Reactive Streams 是并发无关的。Reactor 中的大多数操作符也是并发无关的。一些操作符,如delayElementstimeout,会改变流执行并发行为。Reactor 提供了各种调度器,可用于控制流的执行行为。我们发现这些调度器可以为各种操作符进行配置,例如publishOnsubscribeOn。接下来,我们讨论了可以配置的ParallelFlux,以及可用的调度器,以执行并行处理。最后,我们讨论了使用ConnectedFlux进行事件广播。Reactor 提供了replaypublishOn操作符,从现有的 Flux 生成ConnectedFlux

问题

  1. Reactor 中可用的不同类型的调度器有哪些?

  2. 应该使用哪个调度器进行阻塞操作?

  3. 应该使用哪个调度器进行计算密集型操作?

  4. PublishOnSubscriberOn之间有什么不同?

  5. ParallelFlux的限制是什么?

  6. 哪些操作符可用于生成ConnectedFlux

第十章:测试和调试

在本书中,我们已经详细介绍了 Reactor,使用其各种操作符并使用它们构建示例。然而,编写代码只是工作的一半。所有生产代码都必须通过足够的单元测试进行验证。这些测试不仅验证我们的代码,而且使我们能够更快地进行更改。如果我们重构代码,测试将确保我们的更改没有破坏任何现有功能。在本章中,我们将介绍 Reactor 提供的测试支持。测试业务代码将捕获大多数问题,但代码将在生产中失败。在这种情况下,需要调试代码以找到失败的根本原因。在本章中,我们还将介绍一些调试 Reactor 管道的基本技术。

在本章的最后,我们将学习如何:

  • 测试 Reactor 管道

  • 调试 Reactor 流

技术要求

  • Java 标准版,JDK 8 或更高版本

  • IntelliJ IDEA IDE 2018.1 或更高版本

本章的 GitHub 链接为 github.com/PacktPublishing/Hands-On-Reactive-Programming-with-Reactor/tree/master/Chapter10

测试 Reactor 管道

单元测试 Reactor 管道相当困难。这是因为 Reactor 声明的是行为而不是可以验证的状态。幸运的是,Reactor 附带了一些辅助类,可以帮助进行单元测试。测试实用工具包含在reactor-test组件中。reactor-test为我们提供了以下三个组件:

  • StepVerifier:允许我们验证管道配置和操作符

  • TestPublisher:允许我们产生测试数据以启用操作符的测试

  • PublisherProbe:使我们能够验证现有的发布者

在我们继续之前,让我们首先将reactor-test添加到我们的build.gradle中。我们不需要指定这个版本的版本,因为这个版本由org.springframework.boot插件定义:

plugins {
    id "io.spring.dependency-management" version "1.0.1.RELEASE"
    id "org.springframework.boot" version "2.0.3.RELEASE"
    id "java"
}

// Removed for brevity

dependencies {
        compile 'org.springframework.boot:spring-boot-starter-webflux'
        compile 'org.springframework:spring-context-support'
        compile group: 'org.freemarker', name: 'freemarker', version: '2.3.28'
        testCompile group: 'junit', name: 'junit', version: '4.12'
        testCompile 'io.projectreactor:reactor-test'
}

现在,让我们运行 ./gradlew clean deploy。完成这个步骤后,我们应该会发现我们有一个成功的构建。

StepVerifier

在此之前,我们已经测试了每个响应式流的最终结果,因为完整的管道是在测试用例中创建的。这种方法不是一个好的单元测试,因为它没有单独验证组件。在 Reactor 中,管道是在代码中声明的。然后这些管道被延迟实例化和验证。由于完整的管道被实例化,因此单独对组件进行单元测试相当困难。对于单元测试,我们必须有能力模拟管道的组件,留下正在测试的组件。但是在这种情况下,我们如何验证操作序列的现有管道?Reactor 提供了StepVerifier组件来单独验证所需操作。此 API 不仅定义了存根,还提供了断言来验证每个步骤。在本节中,我们将使用验证不同 Reactor 场景的各种示例。让我们从一个最简单的用例开始,其中给定一个发布者,我们可能想要断言它发布的nextcompletion事件:

    @Test
    public void testExpectation() throws Exception {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            System.out.println("generating next of " + state.getT1());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });
        StepVerifier.create(fibonacciGenerator.take(10))
                .expectNext(0L, 1L, 1L)
                .expectNextCount(7)
                .expectComplete()
                .verify();
    }

在前面的代码中,我们按照以下方式验证斐波那契数列操作:

  • 我们已将take操作符配置为仅消费 10 个事件。

  • 接下来,我们使用了StepVerifier.Create API 来构建一个验证器的实例。

  • 使用expectNext API 来验证已发布的值,并按照发布顺序进行验证。它接受单个值或值的数组;我们正在验证011值。

  • 使用expectNextCount来验证已发布值的数量。由于我们验证了三个值,所以我们还剩下七个。

  • 使用expectComplete API 来验证完成事件。

  • 最后,使用verify API 来验证行为。

现在,让我们运行测试用例。在这样做的时候,我们应该看到一个绿色的条形图:

如果expectNext不匹配已发布的值,测试将因java.lang.AssertionError和详细错误文本而失败。如果已发布的计数不同,则不会因expectNextCount而失败,而是expectComplete。在所有断言失败中,StepVerifier会抛出一个带有以下详细信息的java.lang.AssertionError

java.lang.AssertionError: expectation "expectComplete" failed (expected: onComplete(); actual: onNext(34))
     at reactor.test.DefaultStepVerifierBuilder.failPrefix(DefaultStepVerifierBuilder.java:2235)
     at reactor.test.DefaultStepVerifierBuilder.fail(DefaultStepVerifierBuilder.java:2231)
     at reactor.test.DefaultStepVerifierBuilder.lambda$expectComplete$4(DefaultStepVerifierBuilder.java:245)

在接下来的章节中,我们将讨论StepVerfier中可用的一些最常用的方法。

expectError

如本书中所述,响应式流以完成或错误事件终止。同样地,对于完成事件,有expectError API 来验证错误事件。expectError API 提供了以下方便的方法来验证错误消息或异常类:

错误名称 描述
expectError() 该 API 仅验证错误事件的 occurrence。它不验证任何关于错误的详细信息。
expectError(exceptionClass) 该 API 验证错误事件中包装的底层异常类。
expectErrorMessage(errorMessage) 此 API 验证错误事件中包装的底层异常消息。
expectError(Predicate) 此 API 使用配置的谓词验证底层异常。

在所有前面的案例中,StepVerifier断言错误事件中包装的异常。如果错误不匹配,StepVerifier会抛出assertionErrorStepVerifier还提供了一个expectErrorSatisfies API,可以用来配置自定义断言。此 API 接受一个Consumer来断言错误事件下的异常:

 @Test
    public void testErrorExpectation() throws Exception {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() > 30)
                sink.error(new IllegalStateException("Value out of bound"));

         // Removed for brevity
        });
        StepVerifier.create(fibonacciGenerator.take(10))
                .expectNextCount(9)
                .expectErroSatisfies(x -> {
                    assert(x instanceof IllegalStateException);
                })
                .verify();
    }

在前面的代码中,当值超过30时,我们会抛出异常。expectErrorSatisfies断言抛出的异常是IllegalStateException类型。让我们执行前面的测试案例以获得成功的绿色条。这在上面的屏幕截图中有展示:

图片

expectNext

Reactor 提供了多个方法来断言下一个值。在前面代码中,我们使用expectNext()重载操作符来匹配值。此操作符提供了以下变体:

| 操作符

| 描述 |

| expectNext(value1,value2.. value6) | 此方法验证发布的值与提供的值是否匹配。值必须按照指定的顺序匹配。|

| expectNext(value[]) | 此方法验证发布的值与提供的值数组是否匹配。所有值必须按照指定的顺序匹配。|

| expectNextSequence(Iterator) | 此方法验证发布的值与配置的迭代器中的值是否匹配。所有值必须按照指定的顺序匹配。|

| expectNextCount(count) | 此方法匹配发布的值的数量。|

| expectNextMatches(predicate) | 此方法验证下一个值是否满足配置的谓词。|

所有的前面方法都验证下一个发布的值与匹配的期望值。这对于小数据集来说很好,但是当我们发布像斐波那契数列这样的大范围时,我们无法匹配所有值。有时,我们只是对消费所有(或某些)下一个值感兴趣。这可以通过使用thenConsumeWhile API 来实现。这些方法接受一个谓词,然后消费所有与谓词匹配的序列值。一旦第一个值不匹配,测试案例将尝试验证以下配置的期望值:

    @Test
    public void testConsumeWith() throws Exception {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            System.out.println("generating next of " + state.getT2());

            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });

        StepVerifier.create(fibonacciGenerator)
                .thenConsumeWhile(x -> x >= 0)
                .expectComplete()
               .verify();
    }

在前面的测试案例中,以下情况发生了:

  • thenConsumeWhile已配置为x >= 0谓词。这应该匹配所有值,除了第一个负值。

  • 接下来,我们期望一个完整的事件,然后使用verify API 进行验证。这在上面的屏幕截图中有展示:

我们已经探讨了expect方法来验证在响应式流中生成的事件。如果期望不匹配,StepVerifier构建一个通用消息来指示失败。StepVerifier还提供了构建特定失败和描述性消息的支持。StepVerifier提供了as方法,该方法可以在expect方法之后调用。as方法接受一个字符串,当异常不匹配时显示:

   @Test
    public void testExpectationWithDescp() throws Exception {
        // removed for brevity
        StepVerifier.create(fibonacciGenerator.take(9))
                .expectNext(0L, 1L, 1L).as("Received 0,1,1 numbers")
                .expectNextCount(7).as("Received 9 numbers")
                .expectComplete()
                .verify();
    }

在此代码中,我们为每个期望提供了描述性消息。如果期望不匹配,测试将因特定错误消息而失败,如以下跟踪所示。这有助于调试测试失败:

java.lang.AssertionError: expectation "Received 9 numbers" failed (expected: count = 7; actual: counted = 6; signal: onComplete())

    at reactor.test.DefaultStepVerifierBuilder.failPrefix(DefaultStepVerifierBuilder.java:2235)
    at reactor.test.DefaultStepVerifierBuilder.fail(DefaultStepVerifierBuilder.java:2231)

捕获值

有时候我们无法直接断言值。在这种情况下,我们通常会捕获调用的值,然后分别断言它们。Reactor 提供了一个recordWith API 来捕获测试中的发布者生成的值。该方法接受一个Supplier函数,该函数被调用以实例化一个用于存储值的集合。然后可以使用expectRecordedMatches方法断言记录集合:

@Test
    public void testRecordWith() throws Exception {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
          //   Removed for Brevity
     });
        StepVerifier.create(fibonacciGenerator, Long.MAX_VALUE)
                .recordWith(() -> new ArrayList<>())
                .thenConsumeWhile(x -> x >= 0)
                .expectRecordedMatches(x -> x.size() > 0)
                .expectComplete()
                .verify();
    }

在前面的代码中,我们做了以下操作:

  1. 配置recordWith使用ArrayList来记录所有值。

  2. 配置了thenConsumeWhile使用谓词x >= 0。这应该匹配所有值,除了第一个负值。所有匹配的值都添加到记录集合中。

  3. 接下来,我们配置了expectRecordedMatches来断言记录集合具有值。

  4. 最后,我们期望一个完成事件,然后使用verify API 进行验证,如下所示:

当运行前面的测试用例时,我们应该得到一个绿色的条形表示通过测试。这在上面的屏幕截图()中显示。同样,除了expectRecordWith方法外,Reactor 还提供了一个consumeRecordWith API,可以用于自定义断言。consumeRecordWith方法接受一个用于记录集合的 Consumer 函数。需要注意的是,记录会话只能与下一个consumeRecordWithexpectRecordWith调用相匹配。

验证

如前所述,verify操作符用于断言配置的行为。在调用verify之前,必须验证发布者的终止事件。或者,Reactor 提供了方便的验证方法来验证终止事件并断言完整的配置链。与expectError类似,该 API 在以下方法中提供:

| 方法名称 | 描述 |

| verifyComplete() | 此方法仅验证完成事件的发生。 |

| verifyError() | 此方法仅验证错误事件的发生。 |

verifyError(exceptionClass);此方法验证错误事件并匹配错误事件中包装的底层异常类。

verifyError(exceptionMsg);此方法验证错误事件并匹配错误事件中包装的底层异常消息。

verifyError(predicate);此方法验证错误事件并将其与提供的谓词匹配。

verfiyErrorSatisfies(assertConsumer);此方法验证错误事件并匹配提供的自定义断言的底层异常。

在前面的测试中,我们可以用以下片段替换expectComplete和验证调用:

StepVerifier.create(fibonacciGenerator.take(10))
        .expectNext(0L, 1L, 1L)
        .expectNextCount(7)
        .verifyComplete();

我们将测试执行留给读者。再次强调,通过测试应显示绿色条。重要的是要注意,verify(及相关方法)返回Duration。该持续时间指定测试实际执行的时间。这也引出了关于验证方法阻塞行为的讨论。默认情况下,验证方法的调用是同步和阻塞的,可能会导致测试无限期等待。可以通过在verify方法调用中指定Duration来更改此行为。或者,我们可以使用StepVerifier.setDefaultTimeout方法设置默认超时:

@Test
    public void testWithTimeout() throws Exception {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
            // removed for brevity
        });
        StepVerifier.create(fibonacciGenerator.take(9).delaySequence(Duration.ofSeconds(1)))
                .expectNext(0L, 1L, 1L)
                .expectNextCount(7)
                .expectComplete()
                .verify(Duration.ofMillis(100));
    }

在此代码中,我们做了以下更改:

  • 使用delaySequence操作符延迟 1 秒生成事件。

  • 删除了verifyComplete调用,因为我们不能指定持续时间。相反,我们添加了expectComplete方法调用。

  • 最后,我们使用带有超时时间的验证调用。超时设置为 100 毫秒。

此测试用例超时并失败,以下为异常信息:

java.lang.AssertionError: VerifySubscriber timed out on reactor.core.publisher.SerializedSubscriber@1a57272

    at reactor.test.DefaultStepVerifierBuilder$DefaultVerifySubscriber.pollTaskEventOrComplete(DefaultStepVerifierBuilder.java:1522)
    at reactor.test.DefaultStepVerifierBuilder$DefaultVerifySubscriber.verify(DefaultStepVerifierBuilder.java:1107)
    at reactor.test.DefaultStepVerifierBuilder$DefaultStepVerifier.verify(DefaultStepVerifierBuilder.java:729)
    at ReactorTest.testWithTimeout(ReactorTest.java:58)

在前面的章节中,我们查看了一些方法,这些方法使我们能够验证大多数响应式流操作符。接下来,我们将讨论一些特定的 Reactor 场景。

验证背压

如前文在第七章中所述,流量控制与背压,背压允许订阅者控制事件流。此机制旨在控制快速生成生产者。背压有不同的配置。这些配置已在第七章中讨论过,流量控制与背压,此处不再赘述。从根本上讲,背压跳过向订阅者传递值。因此,验证它意味着我们必须寻找尚未传递给订阅者的值。Reactor 提供了verifyThenAssertThat API 出于同样的原因。此方法公开了可以验证发布者最终状态的断言。现在让我们来处理一个测试用例:

    @Test
    public void testBackPressure() throws Exception {
        Flux<Integer> numberGenerator = Flux.create(x -> {
            System.out.println("Requested Events :" + x.requestedFromDownstream());
            int number = 1;
            while (number < 100) {
                x.next(number);
                number++;
            }
            x.complete();
        }, FluxSink.OverflowStrategy.ERROR);

        StepVerifier.create(numberGenerator, 1L)
                .thenConsumeWhile(x -> x >= 0)
                .expectError()
                .verifyThenAssertThat()
                .hasDroppedElements();
    }

在前面的代码中,发生了以下情况:

  1. 我们使用Flux.create API 配置了一个带有OverflowStrategy.ERROR的发布者。我们的发布者生成 100 个事件,而不寻找订阅者的更多请求。

  2. 接下来,我们的 StepVerifier 被配置为仅处理一个事件,限制订阅者的请求速率。这是通过使用 StepVerifier.create API 实现的。

  3. 由于订阅者请求一个事件,而发布者引发 100 个事件,这应该导致背压错误。在测试用例中,我们配置了 expectError() 来验证引发的错误。

  4. 最后,我们配置了 verfiyThenAssertThat() 来检查丢失的元素。

前面的测试用例验证了完整的背压场景,如下面的截图所示:在前面的测试用例中,我们验证了元素是否丢失。Reactor 还提供了以下断言来验证各种其他场景:

| 方法名称 | 描述 |

| hasDroppedElements | 此方法验证发布者是否由于溢出而丢失了元素。 |

| hasNotDroppedElements | 此方法验证发布者是否由于溢出而丢失了任何元素。 |

| hasDroppedExactly | 此方法验证丢失的值与在方法调用中提供的值是否一致。 |

| hasDroppedErrors | 此方法验证发布者是否丢失了错误。 |

| hasOperatorErrors | 此方法验证流处理是否引发了操作符错误。 |

验证时间操作

验证基于时间的操作是一项复杂的任务。传统上,我们使用 Thread.sleepwait-notify 块来模拟测试用例中的延迟。Reactor 也提供了丰富的支持来验证此类操作。这允许我们通过使用 Reactive Streams 的 Stepverifier.withVirtualTime 方法来构建一个虚拟时钟。然后可以使用以下任何操作来操作虚拟时钟,以模拟所需操作的时间漂移:

| 操作 | 描述 |

| thenAwait | 这只会暂停执行到配置的时间。 |

| expectNoEvent | 此操作暂停执行,并在配置的延迟期间验证没有发生事件。 |

重要的是要注意,必须在注入虚拟时钟之后调用操作符。此外,expectNoEvent API 将订阅视为一个事件。如果它在注入虚拟时钟后的第一步使用,那么它将由于订阅事件而失败。现在让我们处理以下测试用例:

    @Test
     public void testDelay() {
         StepVerifier.withVirtualTime(() -> Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9)
                 .delaySequence(Duration.ofMillis(100)))
                 .expectSubscription()
                 .thenAwait(Duration.ofSeconds(100))
                 .expectNextCount(9)
                 .verifyComplete();
     }

在前面的代码中,我们实现了以下内容:

  1. 使用 StepVerifier.withVirtualTime 创建了一个带有虚拟时钟的 Flux

  2. 在反应流上配置了 delaySequence 操作

  3. 调用 thenAwait 以保持虚拟时钟在配置的时间内

  4. 预期发布九个值,然后是一个完成事件

现在我们运行测试用例并验证如下!图片

Publisher 探针

在前面的章节中,我们使用了StepVerifier来断言在反应链中执行的步骤。然而,这些通常是简单的链,可以在单个测试用例中端到端验证。可能存在我们需要将 Publisher 注入到服务或方法中并验证发布信号的情景。在这种情况下,我们可以使用PublisherProbe实用工具对现有的 Publisher 进行仪器化。探针跟踪 Publisher 发布的信号。最后,我们可以断言并验证探针的最终状态。该实用工具有助于单元测试执行特定逻辑在反应 Publisher 上的服务或方法。PublisherProbe可以使用以下任一方法构建:

  • PublisherProbe.Of(ExistingPublisher): 对现有的 Publisher 进行仪器化,并从中生成一个探针。探针发送出由原始 Publisher 生成的信号。

  • PublisherProbe.empty(): 创建一个空的序列探针。此探针不会发出任何信号。

我们可以通过调用相应的方法从PublisherProbe获取 Mono 或 Flux。然后,可以将 Flux/Mono 传递给正在测试的方法/服务。调用后,可以使用以下断言验证最终状态:

| **方法名** | **描述** |

| assertWasSubscribed | 这个方法验证在调用中 Publisher 是否被订阅。|

| assertWasRequested | 这个方法验证在调用中是否请求了 Publisher。|

| assertWasCancelled | 这个方法验证在调用中 Publisher 是否被取消。|

以下代码展示了这一点:


    @Test
    public void testPublisherProbe() throws Exception {
        Flux<Long> fibonacciGenerator = Flux.generate(() -> Tuples.<Long,
                Long>of(0L, 1L), (state, sink) -> {
            if (state.getT1() < 0)
                sink.complete();
            else
                sink.next(state.getT1());
            return Tuples.of(state.getT2(), state.getT1() + state.getT2());
        });

        PublisherProbe<Long> publisherProbe = PublisherProbe.of(fibonacciGenerator);
        publisherProbe.flux().subscribe();

        publisherProbe.assertWasSubscribed();
        publisherProbe.assertWasRequested();

    }

在前面的代码中,我们做了以下操作:

  1. 使用fibonacciGenerator创建了一个PublisherProbe

  2. 接下来,我们订阅了探针生成的 Flux

  3. 最后,我们验证 Flux 被订阅,随后是请求事件

让我们运行测试用例并验证如下!图片

Publisher 存根

到目前为止,我们一直在创建操作符的同时创建一个Publisher。因此,我们可以构建端到端的有效性验证。然而,在大多数业务服务中,Publisher将在代码的某个部分创建,而操作将在另一个部分执行。为了单元测试操作服务代码,我们需要生成一个虚拟的Publisher。Reactor 也为此提供了TestPublisher。我们可以使用create factory方法创建一个TestPublisher。生成的TestPublisher可以被转换成 Flux 或 Mono。TestPublisher使得使用以下任何一种方法发射事件成为可能:

| **方法名** | **描述** |

| next(T) / next(T,T...) | 调用 Publisher 的OnNext方法,并使用提供的值。|

| complete() | 使用OnComplete事件终止发布者流。|

| error() | 使用OnError事件终止发布者流。|

| emit(T,T,T .....) | 调用发布者的OnNext方法,使用提供的值,然后是OnComplete终止。|

让我们使用示例代码。我们有以下PrintService,它将偶数打印到控制台,如下所示:

class PrintService{
    public void printEventNumbers(Flux<Long> source, PrintWriter writer) {
        source
                .filter(x -> x % 2 == 0)
                .subscribe(writer::println);
    }
}

现在,让我们构建一个简单的测试用例。在测试用例中,我们将注入一些值和一个StringWriter。最后,我们将验证StringWriter是否包含所有必需的值,如下所示:

 @Test
 public void testPublisherStub() throws Exception {
   TestPublisher<Long> numberGenerator= TestPublisher.<Long>create();
   StringWriter out = new StringWriter();
   new PrintService().printEventNumbers(numberGenerator.flux(),
    new PrintWriter(out));
   numberGenerator.next(1L,2L,3L,4L);
   numberGenerator.complete();
   assertTrue(out.getBuffer().length() >0);
 }

在前面的代码中,我们做了以下操作:

  1. 使用create方法生成了一个TestPublisher

  2. 实例化了一个StringWriter以捕获打印的值

  3. 接下来,我们使用一些值生成了onNext

  4. 最后,我们生成了onComplete并验证了打印的值

现在,运行测试用例。这应该会显示一个绿色的条形图,表示测试已经通过!测试通过TestPublisher还跟踪Publisher存根的最终状态。最终状态可以使用以下断言进行验证:

| assertSubscribers | 此方法验证发布者被提供的调用中指定的订阅者数量订阅。|

| assertCancelled | 此方法验证发布者被取消多次,如调用中提供的数字所指定。|

| assertRequestOverflow | 此方法验证发布者通过生成比订阅者请求的更多事件来引发溢出条件。|

在前面的测试用例中,我们构建了一个表现良好的Publisher存根。它没有发送空事件或发送比请求更多的事件。TestPublisher实用程序还使我们能够实例化违反先前条件的发布者。可以使用createNonCompliant方法生成不一致的发布者。此方法使用违规类型并生成配置的错误:

    @Test
    public void testNonComplientPublisherStub() throws Exception {
        TestPublisher<Long> numberGenerator= TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW);
        StepVerifier.create(numberGenerator, 1L)
                .then(() -> numberGenerator.emit(1L,2L,3L,4L))
                .expectNext(1L)
                .verifyError();

    }

在前面的代码中,我们做了以下操作:

  1. 使用createNonCompliant方法生成了一个TestPublisher。发布者已被配置为生成比请求更多的事件。

  2. 订阅了具有一个初始元素需求的发布者。

  3. 验证了生成的元素后跟错误终止。

调试 Reactor 流

调试 Rector 流不是一件简单的事情。这是因为 Reactor 中的所有流处理都是异步和非阻塞的。在一个同步和阻塞系统中,错误堆栈跟踪指向问题的根本原因。然而,在异步 Reactor 流中,错误被记录在Subscriber中,但在流处理中的操作员中被引发。错误堆栈跟踪没有提到操作员。让我们看一下以下 Reactor 堆栈跟踪:

reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException

Caused by: java.lang.IllegalStateException
    at ReactorDebug.lambda$testPublisherStub$1(ReactorDebug.java:22)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:202)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:170)
    at reactor.core.publisher.LambdaSubscriber.onSubscribe(LambdaSubscriber.java:89)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:79)
    at reactor.core.publisher.FluxGenerate.subscribe(FluxGenerate.java:83)
    at reactor.core.publisher.FluxFilterFuseable.subscribe(FluxFilterFuseable.java:51)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6877)
    at reactor.core.publisher.Flux.subscribeWith(Flux.java:7044)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6870)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6834)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6777)
    at PrintService.printEventNumbers(ReactorProbe.java:57)
    at ReactorDebug.testPublisherStub(ReactorDebug.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)

在前面的堆栈跟踪中,我们可以观察到以下情况:

  • IllegalStateException 已到达我们的订阅者

  • 由于订阅者没有处理错误事件,Reactor 还抛出了 ErrorCallbackNotImpletemented

  • 错误发生在执行 PrintService.printEventNumbers

  • 前面的错误是在我们的 ReactorDebug.testPublisherStub 测试用例中抛出的

这并没有太大帮助,但我们可以通过首先实现一个错误处理器来清理堆栈跟踪。这里最简单的方法是使用可抛出对象的 printstackTrace 方法:

class PrintService{
    public void printEventNumbers(Flux<Long> source, PrintWriter writer) {
        source
                .filter(x -> x % 2 == 0)
                .subscribe(writer::println,Throwable::printStackTrace);
    }
}

subscribe 方法中进行的先前更改清理了抛出错误的堆栈跟踪。然而,错误操作符在跟踪中仍未解释,如下面的代码所示:

java.lang.IllegalStateException
    at ReactorDebug.lambda$testPublisherStub$1(ReactorDebug.java:22)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:202)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:170)
    at reactor.core.publisher.LambdaSubscriber.onSubscribe(LambdaSubscriber.java:89)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:79)
    at reactor.core.publisher.FluxGenerate.subscribe(FluxGenerate.java:83)
    at reactor.core.publisher.FluxFilterFuseable.subscribe(FluxFilterFuseable.java:51)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6877)
    at reactor.core.publisher.Flux.subscribeWith(Flux.java:7044)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6870)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6834)
    at reactor.core.publisher.Flux.subscribe(Flux.java:6804)
    at PrintService.printEventNumbers(ReactorProbe.java:57)
    at ReactorDebug.testPublisherStub(ReactorDebug.java:28)
   ........
   ......
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

调试钩子

Reactor 提供了编译时仪器功能来调试堆栈跟踪。此功能使我们能够拦截所有 Flux/Mono 操作的调用。然后,每次拦截都会记录与操作一起抛出的错误。然后,将此映射附加到堆栈跟踪中。然后,可以使用此记录来找到问题的根本原因。由于这是一个额外的拦截,它保留记录映射,因此它应该仅用于调试错误,并且不应在生产系统中启用。Reactor 提供了一个 Hooks.OnOperatorDebug API,必须在实例化 Flux/Mono 之前调用。让我们在我们的测试用例中调用 Hooks.OnOperatorDebug,如下所示:

      @Test
    public void testPublisherStub() throws Exception {
        Hooks.onOperatorDebug();
        Flux<Long> fibonacciGenerator = getFibonacciGenerator();
        StringWriter out = new StringWriter();
        new PrintService().printEventNumbers(fibonacciGenerator,new PrintWriter(out));
        assertTrue(out.getBuffer().length() >0);
    }
class PrintService {
    public void printEventNumbers(Flux<Long> source, PrintWriter writer) {
        source
                .filter(x -> x % 2 == 0)
                .subscribe(writer::println,Throwable::printStackTrace);
    }
}

让我们运行我们的测试用例并查看生成的堆栈跟踪,如下所示:

java.lang.IllegalStateException
    at ReactorDebug.lambda$getFibonacciGenerator$1(ReactorDebug.java:30)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:202)
  ...........
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.FluxGenerate] :
    reactor.core.publisher.Flux.generate(Flux.java:947)
    ReactorDebug.getFibonacciGenerator(ReactorDebug.java:27)
    ReactorDebug.testPublisherStub(ReactorDebug.java:19)
Error has been observed by the following operator(s):
    &#124;_  Flux.generate(ReactorDebug.java:27)
    &#124;_  Flux.filter(ReactorProbe.java:58)

现在,如果我们查看跟踪的底部,它清楚地表明在 Flux.generate 调用中抛出了一个错误。为了解决这个问题,我们可以修复这个错误并重新运行我们的测试用例。

检查点操作符

上一节中讨论的调试钩子具有全局影响,为所有 Flux/Mono 实例提供仪器。因此,调试钩子的影响是应用范围的。或者,Reactor 还提供了一个 checkpoint 操作符,它只能更改特定的 Flux 流。checkpoint 操作符在操作符调用后为 Reactor Streams 提供仪器。我们可以将我们的先前测试用例修改如下:

@Test
public void testPublisherStub() throws Exception {
  Flux<Long> fibonacciGenerator = 
    getFibonacciGenerator().checkpoint();
    StringWriter out = new StringWriter();
  new PrintService().printEventNumbers(fibonacciGenerator,
   new PrintWriter(out));
  assertTrue(out.getBuffer().length() >0);
}

在前面的代码中,我们在创建 Flux 后调用了 checkpoint() 操作符。修改后的测试用例生成了以下堆栈跟踪。由于 checkpoint 操作符是在 Flux.generate 之后调用的,因此记录映射引用 FluxGenerate 作为错误点。以下代码显示了这一点:

java.lang.IllegalStateException
    at ReactorDebug.lambda$getFibonacciGenerator$1(ReactorDebug.java:29)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:202)   
...........
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.FluxGenerate] :
    reactor.core.publisher.Flux.checkpoint(Flux.java:2690)
    reactor.core.publisher.Flux.checkpoint(Flux.java:2640)
    ReactorDebug.testPublisherStub(ReactorDebug.java:18)
Error has been observed by the following operator(s):
    &#124;_  Flux.checkpoint(ReactorDebug.java:18)

之前讨论的checkpointdebug操作符会影响应用程序的内存占用。这两个操作符都试图保存堆栈跟踪,这导致更高的内存消耗。因此,在没有额外成本的情况下,这些操作符不能在生产应用程序中启用。但checkpoint操作符还提供了一个精简版,它不会保存任何堆栈跟踪。当checkpoint操作符配置了描述信息时,它会禁用堆栈跟踪累积。以下是在我们前面的代码中使用带有描述信息的 checkpoint 时生成的堆栈跟踪:

java.lang.IllegalStateException
    at ReactorDebug.lambda$getFibonacciGenerator$1(ReactorDebug.java:29)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
   .......
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly site of producer [reactor.core.publisher.FluxGenerate] is identified by light checkpoint [generator check]."description" : "generator check"

在前面的堆栈跟踪中,Reactor 使用了描述并将其作为identified by light checkpoint消息的前缀。它不再尝试构建操作符调用的堆栈跟踪。identified by light checkpoint消息可以在应用程序日志中搜索。但如果描述信息不够好,Reactor 允许我们启用堆栈跟踪捕获以构建信息性的失败跟踪。这可以通过使用checkpoint(description,enableStackTrace)操作符来实现。

流日志

记录是了解方法调用下发生情况的最常见方式之一。Reactor 使用 SLF4J 进行日志记录,但它默认不记录流操作。相反,Reactor 提供了log操作符,它可以用来选择性地为特定流启用日志记录。让我们使用以下方式修改我们的测试用例,使用日志操作符:

@Test
public void testPublisherStub() throws Exception {
  Flux<Long> fibonacciGenerator = getFibonacciGenerator().log();
  StringWriter out = new StringWriter();
  new PrintService().printEventNumbers(fibonacciGenerator,
   new PrintWriter(out));
  assertTrue(out.getBuffer().length() >0);
}

log()操作符提供了许多变体。默认情况下,操作符在INFO级别记录。我们可以将其配置为在DEBUG或其他级别记录。此外,我们还可以放置一个logback.xml文件来格式化记录的消息,如下所示:

<configuration>
     <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
             <pattern>
                 %d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n
             </pattern>
         </encoder>
     </appender>
    <root level="DEBUG">
         <appender-ref ref="stdout"/>
     </root>
 </configuration>

在前面的logback.xml文件中,我们配置了一个stdout附加器。附加器将以同步和阻塞的方式被调用。Reactor 还提供了一个reactor-logback库,它可以用来以异步方式记录消息。前面的测试用例现在生成了以下日志消息:

23:07:09.139 [main] [DEBUG] reactor.util.Loggers$LoggerFactory - Using Slf4j logging frameworkthe
23:07:09.419 [main] [INFO ] reactor.Flux.Generate.1 - &#124; onSubscribe([Fuseable] FluxGenerate.GenerateSubscription)
23:07:09.450 [main] [INFO ] reactor.Flux.Generate.1 - &#124; request(unbounded)
23:07:09.462 [main] [INFO ] reactor.Flux.Generate.1 - &#124; onNext(0)
23:07:09.463 [main] [INFO ] reactor.Flux.Generate.1 - &#124; onNext(1)
23:07:09.471 [main] [INFO ] reactor.Flux.Generate.1 - &#124; request(1)
........
23:07:09.958 [main] [INFO ] reactor.Flux.Generate.1 - &#124; request(1)
23:07:10.087 [main] [ERROR] reactor.Flux.Generate.1 - &#124; onError(java.lang.IllegalStateException)
23:07:10.126 [main] [ERROR] reactor.Flux.Generate.1 - 
java.lang.IllegalStateException: null
    at ReactorDebug.lambda$getFibonacciGenerator$1(ReactorDebug.java:29)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.fastPath(FluxGenerate.java:223)
    at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:202)
    at ...........

前面的日志输出清楚地显示了在流处理中发生的事件。我们可以解释日志并构建以下分析:

  1. 每条日志行输出调用的操作符。因此,我们可以看到第一个订阅已被提出。

  2. 接下来,出现了一个无界请求,这开始生成事件。

  3. 之后,订阅者提出了一个元素请求。

  4. 最后,由于IllegalStateException,生成操作符中引发了一个ERROR事件。

因此,我们可以看到日志是调试和了解应用程序流处理的有力机制。

摘要

本章重点介绍了 Reactor Streams 的测试和调试。测试 Reactor Flux/Mono 流是复杂的,因为每个流都是以异步方式懒加载评估的。我们还研究了StepVerifier,它可以单独验证单个步骤。接下来,我们研究了虚拟时钟来验证时间敏感的操作,例如延迟。我们还研究了用于验证发布者最终状态的PublisherProbe实用工具。然后,为了对反应式操作符和流业务逻辑进行单元测试,我们使用了TestPublisher进行存根。下一节是关于调试 Reactor Streams 以获取更多关于底层处理的了解。调试 Reactor 流是复杂的,因为操作符是以异步方式评估的。我们研究了用于生成操作符映射错误堆栈跟踪的 Debug 钩子和检查点操作符。最后,我们研究了用于生成流处理日志的日志操作符。我们的书也即将结束。在这段旅程中,我们学习了 Reactor,这是 Reactive Streams 规范的实现。我们与 Flux 和 Mono 发布者一起工作。我们构建了简单的应用程序来了解更多关于可用操作符的信息。我们得出结论,Reactor 是一个可以用于任何 Java 应用程序的库。在这段旅程中,我们还讨论了 SpringWebFlux,这是一个使用 Reactor 的完整 Web 框架。我们使用它开发了简单的 Web 服务,并探索了 Reactor 提供的背压行为。我们通过查看 Reactor 的各种高级功能来结束这次旅程。

问题

  1. Reactor 中哪个测试实用工具类可用于验证流上调用的操作?

  2. PublisherProbeTestPublisher之间有什么区别?

  3. 应如何配置虚拟时钟以验证时间限制操作?

  4. onOperatorDebug钩子和检查点操作符之间的区别是什么?

  5. 我们如何开启流处理的日志记录?

|

第十一章:评估

第一章:开始使用反应式流

  1. 反应式宣言的原则是什么?

反应式宣言定义了以下原则:

  • 消息驱动:所有应用程序组件都应该是松散耦合的,并使用消息进行通信

  • 响应性:应用程序必须及时响应用户输入

  • 弹性:应用程序必须将故障隔离到单个组件

  • 可伸缩性:应用程序必须对工作负载的变化做出反应

  1. 反应式扩展是什么?

反应式扩展是命令式语言中的库,使我们能够编写异步、事件驱动的反应式应用程序。这些库使我们能够将异步事件表达为一系列可观察对象。这使得我们能够构建可以接收和处理这些异步事件的组件。另一方面,也存在事件生产者,它们推送这些事件。

  1. 反应式流规范旨在满足什么需求?

反应式流是一个规范,它确定了构建大量无界数据异步处理所需的最小接口集。它是一个针对 JVM 和 JavaScript 运行时的规范。反应式流规范的主要目标是标准化应用程序异步边界之间的流数据交换。

  1. 反应式流基于哪些原则?

反应式流基于以下两个原则:

  • 异步执行:这是在不等待先前执行的任务完成的情况下执行任务的能力。执行模型解耦任务,以便每个任务都可以同时执行,利用可用的硬件。

  • 背压:订阅者可以控制其队列中的事件以避免任何溢出。如果还有额外容量,它还可以请求更多事件。

  1. Reactor 框架的主要特点是什么?
  • 无限数据流:这指的是 Reactor 生成无限数据序列的能力。

  • 推拉模型:在 Reactor 中,生产者可以推送事件。另一方面,如果消费者在处理方面较慢,它可以在自己的速率下拉取事件。

  • 无并发性:Reactor 不强制执行任何并发模型。它允许开发者选择最适合的。

  • 操作词汇:Reactor 提供了一系列操作符。这些操作符允许我们选择、过滤、转换和组合流。

第二章:Reactor 中的发布者和订阅者 API

  1. 我们如何验证反应式流的发布者和订阅者实现?

为了验证发布者,反应式流 API 发布了一个名为reactive-streams-tck的测试兼容性套件。可以使用PublisherVerifier接口验证反应式发布者。同样,可以使用SubscriberBlackboxVerification<T>抽象类验证订阅者。

  1. 反应式流发布者-订阅者模型与 JMS API 有何不同?

在 JMS 中,生产者负责在队列或主题上生成无界事件,而消费者积极消费事件。生产者和消费者独立工作,以自己的速率。订阅管理的任务由 JMS 代理负责。JMS 中没有背压的概念。此外,它缺乏事件建模,如订阅、错误或完成。

  1. 反应式流发布者-订阅者模型与 Observer API 有何不同?

Observable API 负责确定变化并将其发布给所有感兴趣方。API 是关于实体状态变化的。这不是我们使用PublisherSubscriber接口所建模的内容。Publisher接口负责生成无界事件。另一方面,Subscriber列出了所有类型的事件,如数据、错误和完成。

  1. Flux 和 Mono 之间有什么区别?

Flux是一个通用反应式发布者。它表示一个异步事件流,其中包含零个或多个值。另一方面,Mono 只能生成最多一个事件。

  1. SynchronousSinkFluxSink之间有什么区别?

SynchronousSink一次只能生成一个事件。它是同步的。订阅者必须在生成下一个事件之前消费事件。另一方面,FluxSink可以异步生成多个事件。此外,它不考虑订阅取消或背压。这意味着即使订阅者取消了其订阅,create API 也会继续生成事件。

  1. Reactor 提供了哪些不同的生命周期钩子?
  • doOnSubscribe: 用于订阅事件

  • doOnRequest: 用于请求事件

  • doOnNext: 用于下一个事件

  • doOnCancel: 用于订阅取消事件

  • doOnError: 用于错误事件

  • doOnCompletion: 用于完成事件

  • doOnTerminate: 用于由于错误、完成或取消而终止

  • doFinally: 用于流终止后的清理

  • doOnEach: 用于所有事件

第三章:数据和流处理

  1. 哪个算子用于从流中选择数据元素?
  • filter

  • filterWhen

  • take

  • takeLast

  • last

  • distinct

  • single

  • elementAT

  1. 哪个算子用于从流中拒绝数据元素?
  • filter

  • filterWhen

  • skip

  • skipLast

  • SkipUntil

  • ignoreElements

  1. Reactor 提供了哪些算子用于数据转换?这些算子之间有何不同?
  • map: 这用于一对一转换

  • flatMap: 这用于一对一转换

  1. 我们如何使用 Reactor 算子进行数据聚合?
  • collectList

  • collectMap

  • `collectMultiMap`

  1. Reactor 提供了哪些条件算子?
  • all: 表示AND运算符

  • any: 表示OR运算符

第四章:处理器

  1. DirectProcessor有哪些局限性?

DirectProcessor不提供任何背压处理。

  1. UnicastProcessor 的局限性是什么?

UnicastProcessor 只能与单个订阅者一起工作。

  1. EmitterProcessor 的功能有哪些?

EmitterProcessor 是一个可以与多个订阅者一起使用的处理器。多个订阅者可以根据它们各自的消费速率请求处理器获取下一个值事件

  1. ReplayProcessor 的功能有哪些?

ReplayProcessor 是一个特殊用途的处理器,能够缓存并回放事件给其订阅者。

  1. TopicProcessor 的功能有哪些?

TopicProcessor 是一个能够使用事件循环架构与多个订阅者一起工作的处理器。处理器以异步方式从发布者向附加的订阅者传递事件,并通过使用 RingBuffer 数据结构为每个订阅者尊重背压。

  1. WorkQueueProcessor 的功能有哪些?

WorkQueueProcessor 可以连接到多个订阅者。它不会将所有事件发送给每个订阅者。每个订阅者的需求被添加到队列中,发布者的事件被发送给任何订阅者。

  1. 热发布者和冷发布者之间有什么区别?

冷发布者为每个订阅者有一个单独的订阅状态。它们将所有数据发布给每个订阅者,而不考虑订阅时间。另一方面,热发布者将公共数据发布给所有订阅者。因此,新订阅者只能获得当前事件,不会向他们传递旧事件。

第五章:SpringWebFlux 用于微服务

  1. 我们如何配置 SpringWebFlux 项目?

SpringWebFlux 可以以两种方式配置:

  • 使用注解SpringWebFlux 支持 SpringWebMVC 注解。这是配置 SpringWebFlux 的最简单方法。

  • 使用功能端点:此模型允许我们构建 Java 8 函数作为网络端点。应用程序可以配置为一系列路由、处理程序和过滤器。

  1. SpringWebFlux 支持哪些 MethodParameter 注解?
  • @PathVariable:此注解用于访问 URI 模板变量的值

  • @RequestParam:此注解用于确定作为查询参数传递的值

  • @RequestHeader:此注解用于确定请求头中传递的值

  • @RequestBody:此注解用于确定请求体中传递的值

  • @CookieValue:此注解用于确定作为请求一部分的 HTTP cookie 值

  • @ModelAttribute:此注解用于确定请求模型中的属性或在没有时实例化一个

  • @SessionAttribute:此注解用于确定现有的会话属性

  • @RequestAttribute:此注解用于确定由先前过滤器执行创建的现有请求属性

  1. ExceptionHandler 的用途是什么?

SpringWebFlux 通过创建带有 @ExceptionHandler 注解的方法来支持异常处理。

  1. HandlerFunction 的用途是什么?

SpringWebFlux 处理函数负责服务给定的请求。它以 ServerRequest 类的形式接收请求,并以 ServerResponse 形式生成响应。

  1. RouterFunction 的用途是什么?

SpringWebFlux 路由函数负责将传入的请求路由到正确的处理函数。

  1. HandlerFilter 的用途是什么?

HandlerFilter 与 Servlet 过滤器类似。它在 HandlerFunction 处理请求之前执行。可能存在链式过滤器,它们在请求被服务之前执行。

第六章:动态渲染

  1. SpringWebFlux 框架如何解析视图?

框架使用端点调用返回的 HandlerResult 调用 ViewResolutionResultHandler。然后 ViewResolutionResultHandler 通过验证以下返回值来确定正确的视图:

  • 字符串:如果返回值是一个字符串,那么框架将使用配置的 ViewResolvers 构建视图

  • :如果没有返回任何内容,它将尝试构建默认视图

  • 映射:框架查找默认视图,但它还将返回到请求模型中的键值对添加进去

ViewResolutionResultHandler 查找请求中传递的内容类型。为了确定应该使用哪个视图,它将传递给 ViewResolver 的内容类型与支持的内容类型进行比较。然后选择第一个支持请求内容类型的 ViewResolver

  1. 需要配置哪些组件才能使用 Thymeleaf 模板引擎?
  • spring-boot-starter-thymeleaf 添加到项目中

  • 创建一个 ThymeleafReactiveViewResolver 实例

  • 将解析器添加到 ViewResolverRegistry 中,该解析器在 configureViewResolvers 方法中可用

  1. 哪个 API 用于配置 SpringWebFlux 中的静态资源?
  • addResourceHandler 方法接受一个 URL 模式并将其配置为静态位置

  • addResourceLocations 方法配置了一个位置,从该位置需要提供静态内容

  1. WebClient 的好处是什么?

WebClient 是一个非阻塞、异步 HTTP 客户端,用于发送请求。它可以配置 Java 8 lambda 表达式来处理数据。

  1. WebClient 的检索和交换 API 之间有什么区别?
  • 检索:这可以将请求体解码为 Flux 或 Mono

  • 交换Exchange 方法提供了完整的信息,可以将其转换回目标类型

第七章:流程控制和背压

  1. 为什么我们需要 groupBy 操作符?

groupBy() 操作符将 Flux<T> 转换为批次。该操作符将一个键与 Flux<T> 的每个元素关联。然后它将具有相同键的元素分组。然后操作符发出这些组。

  1. groupBybuffer 操作符之间有什么区别?

groupBy 操作符根据配置的键对事件流进行分组,但 buffer 操作符将流分割成指定大小的块。因此,buffer 操作符保持事件的原始顺序。

  1. 我们如何在 Reactor 中节流事件?

sample() 操作符允许我们实现节流。

  1. Overflow.IgnoreOverflow.Latest 策略之间的区别是什么?

Overflow.Ignore 忽略订阅者背压的限制,并继续向订阅者发送下一个事件。Overflow.Latest 保持缓冲区中提出的最新事件。当下一次请求提出时,订阅者将只获得最新产生的事件。

  1. 哪些操作符可用于更改生产者的背压策略?
  • onBackpressureDrop()

  • onBackpressureLatest()

  • onBackpressureError()

  • onBackpressureBuffer()

第八章:处理错误

  1. 在 Reactor 中是如何处理错误的?

当发布者或订阅者抛出异常时,会出现错误。Reactor 会在拦截异常后构建一个 Error 事件,并将其发送给订阅者。订阅者必须实现 ErrorCallbackHandler 来处理错误。

  1. 哪些操作符允许我们配置错误处理?
  • onErrorReturn

  • onErrorResume

  • onErrorMap

  1. onErrorResumeonErrorReturn 之间的区别是什么?

OnErrorReturn 操作符在发生错误时提供后备值。另一方面,OnErrorResume 操作符提供后备值流而不是单个后备值。

  1. 我们如何为反应式流生成及时响应?

timeout() 操作符可以配置时间间隔。当操作符首次发现延迟超过配置时间时,将引发错误。操作符还有一个后备 Flux。当超时到期时,将返回后备值。

  1. retry 操作符是如何表现的?

retry 操作符允许我们在发现错误时重新订阅已发布的流。retry 操作只能执行固定次数。重新订阅的事件由订阅者作为后续事件处理。如果流正常完成,则不会进行后续重试。

第九章:执行控制

  1. Reactor 中可用的不同类型的调度器有哪些?
  • Schedulers.immediate: 这将在当前线程上调度

  • Schedulers.single: 这将在单个线程上调度

  • Schedulers.parallel: 这将在线程池上调度

  • Schedulers.elastic: 这将在线程池上调度

  • Schedulers.fromExecutor: 这将在配置的执行器服务上调度

  1. 应该使用哪个调度器来处理阻塞操作?

Schedulers.elastic 在线程池上调度。

  1. 应该使用哪个调度器来处理计算密集型操作?
  • Schedulers.single: 这将在单个线程上调度

  • Schedulers.parallel: 这将在线程池上调度

  1. PublishOnSubscriberOn 与彼此有何不同?

subscribeOn 操作符会拦截执行链中的发布者事件并将它们发送到不同的调度器以完成整个链。需要注意的是,该操作符会改变整个链的执行上下文,与仅改变下游链执行的 publishOn 操作符不同。

  1. ParallelFlux 的局限性是什么?

ParallelFlux 不提供 doFinally 生命周期钩子。它可以使用 sequential 操作符转换回 Flux,然后可以使用 doFinally 钩子进行配置。

  1. 哪些操作符可用于生成 ConnectedFlux
  • replay

  • publish

第十章:测试和调试

  1. 在 Reactor 中,哪个测试实用类可用于验证流上调用的操作?

Reactor 提供了 StepVerifier 组件来独立验证所需的操作。

  1. PublisherProbeTestPublisher 之间的区别是什么?

PublisherProbe 实用类可以用于对现有的发布者进行监控。该监控器会跟踪发布者发布的信号,这些信号可以在测试结束时进行验证。另一方面,TestPublisher 能够生成 Publisher 模拟器,这可以用于对 Reactor 操作符进行单元测试。

  1. 应如何配置虚拟时钟以验证时间限制操作?

在执行任何基于时间的操作之前,必须注入虚拟时钟。

  1. onOperatorDebug 钩子和 checkpoint 操作符之间的区别是什么?

onOperatorDebug 钩子会对所有反应式管道进行全局更改。另一方面,checkpoint 操作符会对应用到的流进行特定更改。

  1. 我们如何开启流处理日志记录?

可以使用 log 操作符来开启日志记录。

posted @ 2025-09-11 09:46  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报