Reactor

Reactor 是用于 Java 的异步非阻塞响应式编程框架,同时具备背压控制的能力。它与 Java 8 函数式 Api 直接集成,比如 分为CompletableFuture、Stream、以及 Duration

。它提供了异步 Api 响应流 Flux (用于 [0 - N] 个元素)和 Mono (用于 [0或1] 个元素),并完全遵守和实现了响应式规范。

引入 reactor

reactor 自 3.0.4 版本之后,采用了 BOM (Bill Of Materials)的方式,使用 BOM 可以管理一组良好集成的 maven artifacts,而无需担心不同版本组件之间的相互依赖问题,在 maven 项目中在 dependencyManagement 中 加入 reactor 的 bom 定义即可。

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>io.projectreactor</groupId>
  5. <artifactId>reactor-bom</artifactId>
  6. <version>Dysprosium-SR8</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>

在需要使用 reactor 的项目中,依赖对应 reactor 模块即可。

  1. <dependencies>
  2. <dependency>
  3. <groupId>io.projectreactor</groupId>
  4. <artifactId>reactor-core</artifactId>
  5. </dependency>
  6. </dependencies>

在我学习的过程中,reactor 的最新版本是 Dysprosium-SR8 ,它的命名来自元素周期表,按顺序递增。通过 https://github.com/reactor/reactor/releases 获取最新版本。

reactor bom 中 包含了如下组件:

序号 模块 artifactId 说明
1 Reactor Core reactor-core 基于 Java 8 的响应式流规范实现
2 Reactor Test reactor-test reactor 的测试工具集
3 Reactor Extra reactor-extra 为 Flux 额外提供的操作符
4 Reactor Netty reactor-netty 基于 Netty 实现的 TCP、UDP、HTTP 的客户端和服务端
5 Reactor Adapter reactor-adapter 和其他响应式库(如RxJava2、SWT Scheduler、 Akka Scheduler)的适配器
6 Reactor Kafka reactor-kafka Apache Kafka 的响应式桥接实现
7 Reactor Kotlin Extensions reactor-kotlin-extensions 在 Dysprosium 版本后额外提供的 Kotlin 扩展
8 Reactor RabbitMQ reactor-rabbitmq RabbitMQ 的响应式桥接实现
9 Reactor Pool reactor-pool 响应式应用程序的通用对象池
10 Reactor Tools reactor-tools 一组用于改善 Project Reactor 调试和开发经验的工具。

序号 [1 - 3] 为我们学习 Reactor 过程中主要涉及的模块,序号 [4 - 9] 在我们学习 Spring WebFlux 的过程中会有所涉及,序号 [10] 是用于 Reactor 调试的,下面会讲到。

使用 gradle 的同学请自行百度。

如果需要尝鲜 Reactor 里程碑版或开发者预览版的同学,添加 Spring Milestones repository 的仓库即可。

Reactor 之 初体验

上面说了那么多,我们先来体验下 Reactor。

在学习 Java Stream 的环节中,不知是否有同学有提出这样的疑问:在进行中间操作或终端消费操纵时,如何获取流中元素的序号值呢?

假如有如下单词 [ the, quick, brown, fox, jumped, over, the, lazy, dog ] ,使用 Stream 可否实现输出时并打印每个单词的序号呢?

仔细想想,似乎没有直接的办法可以获取,我们只能通过外部创建变量获取并递增来实现。

来看下 Stream 的实现:

  1. AtomicInteger index = new AtomicInteger(1);
  2. Arrays.stream(WORDS)
  3. .map(word -> StrUtil.format("{}. {}", index.getAndIncrement(), word))
  4. .forEach(System.out::println);

来看下 Reactor 的实现:

  1. Flux.fromArray(WORDS) // ①
  2. .zipWith(Flux.range(1, Integer.MAX_VALUE), // ②
  3. (word, index) -> StrUtil.format("{}. {}", index, word)) // ③
  4. .subscribe(System.out::println); // ④

先不看 Reactor 代码的含义,感觉怎么样,Reactor 的代码看起来是不是更清新一点,没有定义任何三方变量解决了这个问题。

有了 Stream 的基础,Reactor 的代码很容易理解了,我们稍微来解释下 Reactor 上段的代码:

  1. 序号① 的代码 Flux 是我们之前提到的 一个能够发出 0 到 N 个元素的响应流发布者,fromArray 是它的静态方法,用来创建 Flux 响应流
  2. 序号② 的代码 Flux 的 range 操作符和 Stream 的 range 相同,用来生成 整数 Flux 响应流;zipWith 操作符用来合并两个 Flux,并将响应流中的元素一一对应,当其中一个响应流完成时,合并结束,之前未结束的响应流剩下的元素将被忽略
  3. 序号③ 的代码 zipWith 操作符 支持传递一个 BiFunction 的函数式接口实现,定义如何来合并两个数据流中的元素,本例中我们将索引和单词连接起来
  4. 序号④ 的代码 subscribe 即为订阅方法,此处我们做了数据流中元素输出至控制台

Reactor 之 测试 & 调试

测试

Reactor 的测试需要依赖测试模块:

  1. <dependency>
  2. <groupId>io.projectreactor</groupId>
  3. <artifactId>reactor-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>

编写测试代码如下:

  1. // 创建 Flux 响应流
  2. Flux<String> source = Flux.just("foo", "bar");
  3. // 使用 concatWith 操作符连接 2 个响应流
  4. Flux<String> boom = source.concatWith(Mono.error(new IllegalArgumentException("boom")));
  5. // 创建一个 StepVerifier 构造器来包装和校验一个 Flux。
  6. StepVerifier.create(boom)
  7. .expectNext("foo") // 第一个我们期望的信号是 onNext,它的值为 foo
  8. .expectNext("bar") // 第二个我们期望的信号是 onNext,它的值为 bar
  9. .expectErrorMessage("boom") // 最后我们期望的是一个终止信号 onError,异常内容应该为 boom
  10. .verify(); // 使用 verify() 触发测试。

除了正常测试外,Reactor 还提供了诸如:

  1. 测试基于时间操作符相关的方法,使用 StepVerifier.withVirtualTime 来进行
  2. 使用 StepVerifier 的 expectAccessibleContext 和 expectNoAccessibleContext 来测试 Context
  3. 用 TestPublisher 手动发出元素
  4. 用 PublisherProbe 检查执行路径

测试方面暂时不是我们学习的重点,这块内容,我们快速跳过,等到学习到相关场景,需要的时候,我们回过头来再弥补。

调试

响应式编程的调试令人生畏,因为它不像命令式编程,我们可以从异常的堆栈信息中看到发生错误代码的位置及具体错误信息,这也是响应式编程学习曲线比较陡峭的原因。

有如下代码:

  1. Flux.range(1, 3)
  2. .flatMap(n -> Mono.just(n + 100))
  3. .single()
  4. .map(n -> n * 2)
  5. .subscribe(System.out::println);

执行测试时,打印错误日志如下:

  1. reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IndexOutOfBoundsException: Source emitted more than one item
  2. Caused by: java.lang.IndexOutOfBoundsException: Source emitted more than one item
  3. at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:129)
  4. at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:480)
  5. at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:413)
  6. at reactor.core.publisher.FluxRange$RangeSubscription.slowPath(FluxRange.java:154)
  7. at reactor.core.publisher.FluxRange$RangeSubscription.request(FluxRange.java:109)
  8. at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:363)
  9. at reactor.core.publisher.FluxRange.subscribe(FluxRange.java:68)
  10. at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
  11. at reactor.core.publisher.Mono.subscribeWith(Mono.java:4330)
  12. at reactor.core.publisher.Mono.subscribe(Mono.java:4190)
  13. at reactor.core.publisher.Mono.subscribe(Mono.java:4126)
  14. at reactor.core.publisher.Mono.subscribe(Mono.java:4073)
  15. at top.todev.note.web.flux.reactor.ReactorFirstExperienceTest.testReactorDebug(ReactorFirstExperienceTest.java:79)
  16. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  17. ...
  18. at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)

我们从上述的错误中获取到发生了 IndexOutOfBoundsException 数据越界异常,从上往下看,应该是 MonoSingle 响应式流发出了不止一个元素,查看 Mono#singe 操作符描述,我们看到 single 有一个规定: 源必须只能发出一个元素。看来是有一个源发出了多于一个元素,从而违反了这一规定。

粗略过一下这些行,我们可以大概勾画出一个大致的出问题的链:涉及 MonoSingle、FluxFlatMap、FluxRange(每一个都对应 trace 中的几行,但总体涉及这三个类)。 所以难道是 range().flatMap().single() 这样的链?

但是如果在我们的应用中多处都用到这一模式,那怎么办?通过这些还是不能确定为什么会抛除这个异常, 搜索 single 也找不到问题所在。直到最后几行指向了我们的代码,查看代码和我们之前的预测的调用链一样。

但是最终我们怎么快速确定代码的问题在哪里呢?

方案1: 开启调试模式

使用 Hooks.onOperatorDebug(); 在程序初始的地方开启调试模式

错误日志如下:

  1. reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IndexOutOfBoundsException: Source emitted more than one item
  2. Caused by: java.lang.IndexOutOfBoundsException: Source emitted more than one item
  3. at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:129)
  4. Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
  5. Assembly trace from producer [reactor.core.publisher.MonoSingle] :
  6. reactor.core.publisher.Flux.single(Flux.java:7851)
  7. top.todev.note.web.flux.reactor.ReactorFirstExperienceTest.testReactorDebug(ReactorFirstExperienceTest.java:83)
  8. Error has been observed at the following site(s):
  9. |_ Flux.single at top.todev.note.web.flux.reactor.ReactorFirstExperienceTest.testReactorDebug(ReactorFirstExperienceTest.java:83)
  10. |_ Mono.map at top.todev.note.web.flux.reactor.ReactorFirstExperienceTest.testReactorDebug(ReactorFirstExperienceTest.java:84)
  11. Stack trace:
  12. at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:129)
  13. at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:480)
  14. at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:413)
  15. at reactor.core.publisher.FluxRange$RangeSubscription.slowPath(FluxRange.java:154)
  16. at reactor.core.publisher.FluxRange$RangeSubscription.request(FluxRange.java:109)
  17. at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:363)
  18. at reactor.core.publisher.FluxRange.subscribe(FluxRange.java:68)
  19. at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
  20. at reactor.core.publisher.Mono.subscribeWith(Mono.java:4330)
  21. at reactor.core.publisher.Mono.subscribe(Mono.java:4190)
  22. at reactor.core.publisher.Mono.subscribe(Mono.java:4126)
  23. at reactor.core.publisher.Mono.subscribe(Mono.java:4073)
  24. at top.todev.note.web.flux.reactor.ReactorFirstExperienceTest.testReactorDebug(ReactorFirstExperienceTest.java:85)

我们从 Error has been observed at the following site(s) 这行错误起,可以看到错误沿着操作链传播的轨迹(从错误点到订阅点),我们从 Assembly trace from

producer 这行开始的错误中也看到了源代码 83 行开始报错,也确定了上一行的 flatMap 操作符发出了不止一个元素导致。

方案2: 使用 checkpoint 操作符埋点调试

使用方案1开启全局调试有较高的成本即影响性能,我们可以在可能发生错误的代码中加入操作符 checkpoint 来检测本段响应式流的问题,而不影响其他数据流的执行。

checkpoint 通常用在明确的操作符的错误检查,类似于埋点检查的概念。同时该操作符支持 3个重载方法:checkpoint(); checkpoint(String description); checkpoint

(String description, boolean forceStackTrace);

description 为埋点描述,forceStackTrace 为是否打印堆栈

方案3: 启用调试代理

1. 在项目中引入 reactor-tools 依赖

  1. <dependency>
  2. <groupId>io.projectreactor</groupId>
  3. <artifactId>reactor-tools</artifactId>
  4. </dependency>

2. 使用 ReactorDebugAgent.init(); 初始化代理

由于该代理是在加载类时对其进行检测,因此放置它的最佳位置是在main(String [])方法中的所有其他项之前

3. 如果是测试类,使用如下代码处理现有的类

注意,在测试类中需要提前运行,比如在 @Before 中

  1. ReactorDebugAgent.init();
  2. ReactorDebugAgent.processExistingClasses();

总结

本篇我们了解了如何引入 Reactor ;初步体验了 Reactor 的 Hello World 代码;最后我们了解了如何测试及调试 Reactor,这些内容为我们后面学习 Reactor 的基础,希望大家都能掌握。

今天的内容就学到这里,我们下篇开始 Reactor 的基础和特性学习。

源码详见:https://github.com/crystalxmumu/spring-web-flux-study-note{:target="_blank"} 下 02-reactor-core-learning 模块。

参考

  1. Reactor 3 Reference Guide
  2. Reactor 3 中文指南

学习响应式编程 Reactor (2) - 初识 reactor的更多相关文章

  1. 学习响应式编程 Reactor (1) - 响应式编程

    响应式编程 命令式编程(Imperative Programing),是一种描述计算机所需做出的行为的编程范式.详细的命令机器怎么(How)去处理以达到想要的结果(What). 声明式编程(Decla ...

  2. 学习响应式编程 Reactor (4) - reactor 转换类操作符(1)

    Reactor 操作符 数据在响应式流中的处理,就像流过一条装配流水线.Reactor 既是传送带,又是一个个的装配工或机器人.原材料从源头(最初的 Publisher )流出,经过一个个的装配线中装 ...

  3. 1小时让你掌握响应式编程,并入门Reactor

    我看同步阻塞 “你知道什么是同步阻塞吗”,当然知道了.“那你怎么看它呢”,这个... 在同步阻塞的世界里,代码执行到哪里,数据就跟到哪里.如果数据很慢跟不上来,代码就停在那里等待数据的到来,然后再带着 ...

  4. 学习响应式编程 Reactor (3) - reactor 基础

    Reactor Reactor 项目的主要 artifact 是 reactor-core,这是一个基于 Java 8 的实现了响应式流规范的响应式库. Reactor 提供了实现 Publisher ...

  5. 学习响应式编程 Reactor (5) - reactor 转换类操作符(2)

    Reactor 操作符 上篇文章我们将 Flux 和 Mono 的操作符分了 11 类,我们来继续学习转换类操作符的第 2 篇. 转换类操作符 转换类的操作符数量最多,平常过程中也是使用最频繁的. F ...

  6. Spring 5 响应式编程

    要点 Reactor 是一个运行在 Java8 之上的响应式流框架,它提供了一组响应式风格的 API 除了个别 API 上的区别,它的原理跟 RxJava 很相似 它是第四代响应式框架,支持操作融合, ...

  7. 响应式编程(Reactive Programming)(Rx)介绍

    很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的. 学习响应式编程是很困难的一个过程,特别是在缺乏优秀资料的前提下.刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它 ...

  8. 响应式编程系列(一):什么是响应式编程?reactor入门

    响应式编程 系列文章目录 (一)什么是响应式编程?reactor入门 (二)Flux入门学习:流的概念,特性和基本操作 (三)Flux深入学习:流的高级特性和进阶用法 (四)reactor-core响 ...

  9. Java reactor响应式编程

    转载自:https://www.cnblogs.com/lixinjie/p/a-reactive-streams-on-jvm-is-reactor.html 响应式编程 作为响应式编程方向上的第一 ...

随机推荐

  1. 计算机网络-OSI参考模型

    通信分层的好处 1.每一层的更改不会影响其他层2.有利于不同网络设备厂商生产出标准的网络设备 分层方法(比喻) OSI参考模型

  2. MySQL字段默认值设置详解

    前言: 在 MySQL 中,我们可以为表字段设置默认值,在表中插入一条新记录时,如果没有为某个字段赋值,系统就会自动为这个字段插入默认值.关于默认值,有些知识还是需要了解的,本篇文章我们一起来学习下字 ...

  3. Linux上的Shebang符号(#!)

    使用Linux或者unix系统的同学可能都对#!这个符号并不陌生,但是你真的了解它吗? 本文了将给你简单介绍一下Shebang("#!")这个符号. 首先,这个符号(#!)的名称, ...

  4. jQurey判断下一项是否为指定项、下一项是否有指定项

    jQurey判断下一项是否为指定项.下一项是否有指定项 此例子中,如果某个列表项没有二级列表,那么去掉它的展开.收起按钮.就是前边那个减号. 此时我们需要判断VOC综合治理技术这一项是否含有二级菜单, ...

  5. ESLint语法报错问题

    编写javaScript过程中ESLint语法报错问题 ESLint语法要求: 双引号""需要替换成单引号'' 分号不允许出现 ()之前需要一个空格比如 login () (VSC ...

  6. 山东浪潮超越3B4000申泰RM5120-L

    龙芯解决方案 首页 > 龙芯业务 > 龙芯解决方案和产品生态 > 整机产品 > 服务器 > 详情 超越申泰RM5120-L 服务器 超越申泰RM5120-L 服务器 20 ...

  7. 磁盘IO过高时的处理办法 针对系统中磁盘IO负载过高的指导性操作

    磁盘IO过高时的处理办法 针对系统中磁盘IO负载过高的指导性操作 主要命令:echo deadline > /sys/block/sda/queue/scheduler 注:以下的内容仅是提供参 ...

  8. Linux权限问题(2)-unzip引发的权限问题

    背景:依然是上一个朋友,在用php调用unzip命令时,再次出现了权限被拒绝的问题. Notice:此处描述的问题,为使用php命令行执行php文件,因此进程属主为登录的用户,而不是nginx用户. ...

  9. 008.Ansible文件管理模块

    一  stat模块 检查文件状态使用,模块获取文件的状态等信息,类似与linux中的STAT命令可以用来获取文件的属主.可读/写.文件状态等信息 [root@node1 ansible]#  stat ...

  10. fprintf函数

    描述 C 库函数 int fprintf(FILE *stream, const char *format, ...) 发送格式化输出到流 stream 中. 声明 下面是 fprintf() 函数的 ...