5.4 反应式扩展
反应式扩展是**observable(可观察)**设计模式的一种详细形式。 它们首先由 Erik Meijer 在 Microsoft .NET 生态系统中推广。 现代应用程序越来越多地由异步事件流组成,不仅在服务器上,而且在 Web、桌面和移动客户端中。 实际上,我们可以将图形用户界面事件视为应用程序必须响应的事件流。
反应式扩展由三个方面定义:
- 观察事件或数据流(例如,可以观察传入的 HTTP 请求)
- 组合运算符来转换流(例如,将多个 HTTP 请求流合并为一个)
- 订阅流并对事件和错误做出响应
ReactiveX 计划为后端和前端项目(http://reactivex.io/)提供了多种语言的通用 API 和实现。 RxJS 项目为浏览器中的 JavaScript 应用程序提供响应式扩展,而像 RxJava 这样的项目为 Java 生态系统提供通用的响应式扩展实现。
Vert.x 为 RxJava 版本 1 和 2 提供绑定。建议使用版本 2,因为它支持背压,而版本 1 不支持。
5.4.1 简单说说RxJava
让我们探索一下 RxJava 的基础知识,看看它做了什么以及它如何与 Vert.x 完美集成
💡提示: Timo Tuominen 的 RxJava for Android Developers(Manning,2019 年)是学习 RxJava 的可靠资源。
可观察的类型
首先,RxJava 2 提供了五种不同类型的可观察源,如 表 5.1 所示。
表 5.1 RxJava 中的 Observable 类型
类型 | 描述 | 例子 |
---|---|---|
Observable<T> | T 类型的事件流。不支持背压。 | 定时器事件,我们无法像 GUI 事件那样应用背压的可观察源 |
Flowable<T> | 可以应用背压的 T 型事件流 | 网络数据、文件系统输入 |
Single<T> | 只发出一个 T 类型事件的源 | 通过键从数据存储中获取条目 |
Maybe<T> | 可以发出一个 T 类型事件的源,或者没有 | 通过键从数据存储中获取条目,但该键可能不存在 |
Completable | 通知某些操作已完成但未给出任何值的源 | 删除文件 |
您有时可能会阅读有关 hot 和 cold 来源的信息。 热源是无论是否有订阅者,都会发出事件的源。 冷源是在第一次订阅后才开始发出事件的源。 周期性计时器是热源,而要读取的文件是冷源。 使用冷源,您可以获得所有事件,但使用热源,您只能在订阅后获得那些发出的事件。
基本示例
我们将从清单 5.22 中的简单示例开始,如图 5.3 所示。
运行清单5.22 中的代码会得到如下控制台输出:
@1
@2
@3
这个例子创建了一个由三个整数组成的 observable。 just 工厂方法创建一个 Observable<Integer>
源。 然后我们使用两个映射运算符来转换流。 第一个从 Observable<Integer>
转换为 Observable<String>
。 第二个将 @
字符添加到每个项目。 最后,subscribe 执行订阅,其中为每个项目调用 System.out.println。
源可能会发出错误,在这种情况下可以通知订阅者。 考虑以下清单中的 observable。
可观察的字符串值将发出一个错误。 map 运算符永远不会被调用,因为它只对值进行操作,而不是对错误进行操作。 我们可以看到 subscribe 现在有两个参数; 第二个是处理错误的回调。 在这个例子中,我们只打印堆栈跟踪,但在网络应用程序中,例如,我们会进行错误恢复。
🏷注意: 在示例和测试中使用 just 工厂方法非常有用,但在实际场景中,您需要调整更复杂的源以将事件生成到 RxJava 可观察类型。 为此,您可以实现一个通用的 Publisher 接口,以使用 fromPublisher 方法(而不是 just)向订阅者发送项目。 还有用于 JDK futures、可迭代iterable对象以及从 JDK 可调用对象生成项目的适配器方法。
生命周期
前面的例子没有展示 observable 的完整生命周期。 一旦进行了订阅,就会发出零个或多个项目。 然后流以错误或已完成的通知终止。
让我们看一个更详细的例子。
运行前面的代码会给出以下输出。
此示例向我们展示了 subscribe 的形式,其中可以处理所有事件:事件、错误和流的完成。 该示例还显示了更多运算符:
- doOnSubscribe 和 doOnNext 是可以在项目沿流传递时触发的操作(具有潜在的副作用)。
- delay 允许在事件开始在流的下游发出时进行延迟。
- buffer 将事件分组(到列表中),所以在这里我们成对地获取事件。
当然,RxJava 的内容比我们在本节中讨论的要多,但我们已经涵盖了足够多的内容来深入研究 Vert.x 和 RxJava 的集成。
5.4.2 RxJava 和 Vert.x
Vert.x 中的 RxJava 集成可从 vertx-rx-java2 模块获得。 在 Gradle 中(在 Maven 中也是如此),可以将依赖项添加为
implementation("io.vertx:vertx-rx-java2:version")
官方 Vert.x 堆栈中项目的所有 API 都支持 RxJava。 RxJava API 是从核心 API 自动生成的。 RxJava API 有几个惯用的转换规则,但作为一个简单的例子,当你有
void foo(String s, Handler<AsyncResult<String>> callback)
RxJava 的翻译是
Single<String> foo(String s)
RxJava API 位于 io.vertx.reactivex 的子包中。 例如,AbstractVerticle 的 RxJava 版本是 io.vertx.reactivex.core.AbstractVerticle。
让我们看一个使用 RxJava API 的示例 Verticle。
这个例子打开了一个经典的 HTTP 服务器,它对任何请求都回复 Ok。 有趣的部分是AbstractVerticle 的RxJava 变体有一个rxStart(和rxStop)方法来通知部署成功。 在我们的例子中,当 HTTP 服务器启动时,verticle 已经成功部署,所以我们返回一个 Completable 对象。 您可以检查以 rx 为前缀的方法是否对应于生成的支持 RxJava 的方法。 如果您检查 RxJava API,您会注意到原始方法(包括回调)仍然存在。
这个例子的另一个有趣的部分是每秒发出事件的 observable。 它的行为本质上与 Vert.x 计时器一样。 RxJava API 中有几个接受调度器对象的操作符方法,因为它们需要延迟异步任务。 默认情况下,他们从他们管理的内部工作线程池中回调,这打破了 Vert.x 线程模型假设。 我们总是可以传递一个 Vert.x 调度器来确保事件仍然在原始上下文事件循环中被回调。
5.4.3 RxJava 中的收集器服务
我们现在可以回到我们的边缘服务示例并使用 RxJava 重写 CollectorService verticle 类。
首先,我们将更新导入以使用 io.vertx.reactivex.*
包。 由于 Verticle 启动了一个 HTTP 服务器,我们可以利用 rxStart 如下。
下一步是编写一个并行获取温度的方法,然后将响应组装为 JSON 对象。 就像回调版本一样,我们可以有一个获取单个温度的方法。 代码显示在以下清单中。
同样,与回调版本的区别在于我们使用 rxSend(返回 Single)而不是 send(使用回调)。
下一个清单显示了一种组合并行异步 HTTP 请求并根据响应组装 JSON 对象的方法。
通过使用 fetchTemperature 来获取单个响应,我们获得了观察单个 HTTP 响应的 Single 对象。 为了组合结果,我们使用 zip 运算符,它采用可分割的源并将结果组合为另一个 Single 对象。 当所有 HTTP 响应都可用时,zip 运算符将值传递给必须产生值(任何类型)的函数。 然后返回的值是 zip 运算符发出的 Single 对象。 在这里,我们使用 Vert.x Web 客户端为我们转换为 JSON 的 HTTP 响应主体构建了一个 JSON 数组,然后我们将数组包装在一个 JSON 对象中。
请注意,zip 有许多带有不同数量参数的重载定义,以应对两个来源、三个来源等等。 当代码需要处理未定义数量的源时,有一个变体获取源列表,传递给 zip 的函数接受值列表。
这导致我们定义了 HTTP 请求处理方法,该方法收集温度,发布到快照服务,然后响应请求者。 代码在以下清单中。
此方法还执行订阅:成功时将 JSON 数据返回给请求者,失败时返回 HTTP 500 错误。 需要注意的是,订阅会触发对传感器服务的 HTTP 请求,然后是对快照服务的请求,依此类推。 在进行订阅之前,RxJava 可观察管道只是处理事件的“配方”。
最后缺少的部分是向快照服务发送数据的方法。
该方法引入了函数式编程爱好者所熟知的 flatMap 运算符。 如果 flatMap 对您来说听起来很神秘,请不要担心; 在组合顺序异步操作的情况下,您可以将“flatmap”读为“and then(然后)”。
由于 data 发出一个 JSON 对象,所以 flatMap 运算符允许我们在发出该 JSON 对象后向 Web 客户端发出 HTTP 请求。 在对快照服务的 HTTP 请求成功后,我们需要另一个(嵌套的)flatMap。 实际上,rxSendJsonObject 提供了一个发出 HTTP 响应的 observable。 但是,我们需要 JSON 对象,因为它必须在发送到快照服务成功后返回给请求者,因此第二个 flatMap 允许我们这样做并将其重新注入管道。 这是 RxJava 中非常常见的模式。
运行 RxJava 版本的边缘服务与运行回调版本没有区别。 我们需要做的就是将 CollectorService 的部署更改为以下内容:
vertx.deployVerticle("chapter5.reactivex.CollectorService");
与服务交互产生与回调版本相同的结果。
map
和 flatMap
的区别`
flatMap 来自“flatten”和“map”运算符。 为了更好地理解它的工作原理,让我们用 JavaScript 数组来说明 flatMap(您可以使用 node 或直接从 Web 浏览器控制台对其进行测试)。
使用 let a = [1, 2, 3]
,a 是一个包含值 1、2 和 3 的数组。现在假设对于每个值,我们希望将值乘以 10 和 100。使用 map ,我们可以写let b = a.map(x => [x * 10, x * 100])
,它给我们一个数组的数组:[ [ 10, 100 ], [ 20, 200 ], [ 30 , 300]]
。
如果我们只想要值而不是嵌套数组,这不是很方便,所以我们可以“flatten(展平)” b,b.flat()
,这给了我们 [10, 100, 20, 200, 30, 300]
。 您可以直接使用 a.flatMap(x => [x * 10, x * 100])
获得相同的结果。
这直接转化为其他操作,如HTTP客户端请求或数据库调用,因为flatMap避免了可观察对象的嵌套可观察对象。