react实例_React堆实例

react实例

重要要点

  • Reactor是针对Java 8并提供符合Rx要求的API的React式流库
  • 尽管API有所不同,但它使用与RxJava相同的方法和原理
  • 它是允许操作员融合的第四代React式库,例如RxJava 2
  • Reactor是Spring Framework 5的React式编程模型支持中的核心依赖项。

RxJava回顾

像RxJava 2一样,Reactor是第四代React式库。 它是由Spring托管人Pivotal发起的,并且建立在Reactive Streams规范,Java 8和ReactiveX词汇表的基础上。 它的设计是由Reactor 2(以前的主要版本)和RxJava的设计和核心贡献者共同推动的一种巧妙混合的结果。

在本系列的前几篇文章“ RxJava示例 ”和“ 测试RxJava ”中,您了解了React式编程的基础知识:如何将数据概念化为流,Observable类及其各种运算符,从中创建Observables的工厂方法静态和动态来源。

Observable是推送源,Observer是通过订阅操作使用此源的简单接口。 请记住,一个Observable的约定是通过onNext通知其Observer 0个或更多数据项,还可以选择随后跟随一个onError或onComplete终止事件。

为了测试Observable,RxJava提供了TestSubscriber ,它是Observer的一种特殊形式,可让您在流中声明事件。

在本文中,我们将在Reactor和您已经了解的RxJava之间进行比较,并展示其共同之处和差异。

React堆类型

Reactor的两个主要类型是Flux<T>Mono<T> 。 Flux等效于RxJava Observable ,能够发出0个或更多项,然后可选地完成或出错。

另一方面,单声道最多只能发射一次。 它在RxJava端对应于SingleMaybe类型。 因此,只想发出完成信号的异步任务可以使用Mono<Void>

两种类型之间的这种简单区别使得在提供响应式API中有意义的语义的同时,事情也很容易掌握:仅查看返回的响应式,就可以知道某个方法是“即发即弃”还是“请求-请求”。响应”( Mono )或实际上是将多个数据项作为流( Flux )处理。

当使用某些运算符时,Flux和Mono都通过强制到相关类型来利用此语义。 例如,在Flux<T>上调用single()将返回Mono<T> ,而使用concatWith将两个monos串联在一起将产生Flux 。 同样,一些运算符在Mono上没有意义(例如take(n) ,产生n> 1个结果),而其他运算符Mono上有意义(例如or(otherMono) )。

Reactor设计理念的一个方面是保持API精简,将这种两种React类型分开是在表达性和API表面之间的良好中间立场。

“建立在Rx上,每个阶段都有React流”

正如“ RxJava示例”中所表达的那样,RxJava在概念上与Java 8 Streams API有一些表面上的相似之处。 另一方面,Reactor看起来很像RxJava,但这绝不是巧合。 目的是提供一个Reactive Streams本机库,该库公开用于异步逻辑组合的符合Rx的运算符API。 因此,尽管Reactor植根于Reactive Streams,但它尽可能地寻求与RxJava的通用API对齐。

React式库和React式流的采用

React性流 (在本文的其余部分中简称为RS)是“为无阻塞背压的异步流处理提供标准的一项举措”。 它是一组文本规范以及一个TCK和四个简单的接口( PublisherSubscriberSubscriptionProcessor ),它们将集成在Java 9中。

它主要涉及反作用拉背压的概念(稍后会详细介绍)以及如何在几个实现的反作用源之间进行互操作。 它根本不涉及运营商,而是专门关注流的生命周期。

Reactor的主要区别在于其RS优先方法FluxMono都是RS Publisher实现,并符合React式拉背压。

在RxJava 1中,只有一部分运算符支持反压,即使RxJava 1具有针对RS类型的适配器,其Observable也不直接实现这些类型。 RxJava 1早于RS规范并在该规范的设计过程中充当了基础工作之一,这一事实很容易解释。

这意味着,每次使用这些适配器时,您都将拥有一个Publisher ,而Publisher又没有任何运算符。 为了从那里做任何有用的事情,您可能需要回到Observable ,这意味着要使用另一个适配器。 这种视觉混乱可能会损害可读性,尤其是当像Spring 5这样的整个框架直接建立在Publisher之上时。

迁移到Reactor或RxJava 2时要记住的RxJava 1的另一个区别是,在RS规范中,未授权null值。 如果您的代码库使用null表示某些特殊情况,则可能很重要。

RxJava 2是在Reactive Streams规范之后开发的,因此以其新的Flowable类型直接实现了Publisher 。 但是RxJava 2不仅保留了RS类型,还保留了“传统” RxJava 1类型( ObservableCompletableSingle ),并引入了Maybe的“ RxJava Optional”。 尽管它们仍然提供了我们之前讨论的语义区分,但是这些类型的缺点是未实现RS接口。 请注意,与RxJava 1不同,RxJava 2中的Observable不支持RxJava 2中的背压协议(此功能现在仅保留给Flowable )。 保留它的目的是为在用户界面事件(例如,反压不可行或无法实现)的情况下提供丰富而流畅的API。 CompletableSingleMaybe在设计上不需要背压支持,它们还将提供丰富的API,并将所有工作负荷推迟到订阅为止。

Reactor在这一领域再次变得更精益,采用MonoFlux类型,都实现了Publisher并且都支持背压。 作为MonoPublisherMono开销相对较小,但其他Mono优化却可以抵消大部分开销。 我们将在后面的部分中了解Mono对背压的意义。

与RxJava类似但不相等的API

有时,操作员的ReactiveX和RxJava词汇可能不知所措,并且某些操作员由于历史原因可能会混淆名称。 Reactor的目的是拥有一个更紧凑的API,并在某些情况下会有所不同,例如为了选择更好的名称,但是总体而言,这两个API看起来很相似。 实际上,RxJava 2中的最新迭代实际上也从Reactor借来了一些词汇,这暗示了两个项目之间正在进行的密切协作。 一些运算符和概念首先出现在一个库中或另一个库中,但常常同时出现在两个库中。

例如, Flux具有相同的熟悉的just工厂方法(尽管只有两个正义变量:一个元素和一个vararg)。 但是from ,已由几个显式变体替代,最值得注意的是fromIterable 。 Flux在运算符方面也具有所有通常的flatMapmapmergeconcatflatMaptake …等。

Reactor避开的RxJava运算符名称的一个示例是令人困惑的amb运算符,该运算符已被更恰当地命名为firstEmitting 。 此外,为了在API中引入更大的一致性, toList已重命名为collectList 。 实际上,所有collectXXX运算符现在都将值聚合到特定类型的集合中,但仍会生成所述集合的Mono ,而toXXX方法保留用于类型转换,例如,将您带出被动世界。 toFuture()

这次,就类实例化和资源使用而言,Reactor可以精简的另一种方法是融合 :Reactor能够将某些运算符的多个顺序用法(例如,两次调用concatWith )合并为单一用法,仅实例化运算符的内部类一次(宏融合)。 其中包括一些基于数据源的优化,这可以极大地帮助Mono抵消实施Publisher的成本。 它还能够在几个兼容的操作程序之间共享资源,例如内部队列(微融合)。 这些功能使Reactor成为了第四代React库。 但这是以后文章的主题。

让我们仔细看看一些Reactor运算符。 (您会注意到与本系列前面几篇文章中的一些示例的对比。)

一些操作员示例

本节包含代码片段,我们建议您尝试使用它们,并进一步使用Reactor进行试验。为此,您应打开所选的IDE并以Reactor作为依赖项创建一个测试项目。)

为此,请在pom.xml的“ dependencies”部分中添加以下内容:

<dependency>
    <groupId>io.projectreactor</groupId>	
    <artifactId>reactor-core</artifactId>
    <version>3.0.3.RELEASE</version>
</dependency>

要在Gradle中执行相同的操作,请编辑依赖项部分以添加React堆,与此类似:

dependencies {
    compile "io.projectreactor:reactor-core:3.0.3.RELEASE"
}

让我们玩一下本系列前几篇文章中使用的示例!

与在RxJava中创建第一个Observable方式非常相似,可以使用just(T…)fromIterable(Iterable<T>) Reactor工厂方法创建Flux 。 请记住,一个给定的Listjust发出列表作为一个整体,单一的排放,而fromIterable 发出可迭代列表中的每个元素:

public class ReactorSnippets {
  private static List<String> words = Arrays.asList(
        "the",
        "quick",
        "brown",
        "fox",
        "jumped",
        "over",
        "the",
        "lazy",
        "dog"
        );

  @Test
  public void simpleCreation() {
     Flux<String> fewWords = Flux.just("Hello", "World");
     Flux<String> manyWords = Flux.fromIterable(words);

     fewWords.subscribe(System.out::println);
     System.out.println();
     manyWords.subscribe(System.out::println);
  }
}

像在相应的RxJava示例中一样,
Hello
World

the
quick
brown
fox
jumped
over
the
lazy
dog

为了输出fox句子中的单个字母,我们还需要flatMap (就像我们在Example中的RxJava一样),但是在Reactor中,我们使用fromArray而不是from 。 然后,我们要过滤掉重复的字母,并使用distinctsort对其进行sort 。 最后,我们要为每个不同的字母输出一个索引,可以使用zipWithrange来完成:

@Test
public void findingMissingLetter() {
  Flux<String> manyLetters = Flux
        .fromIterable(words)
        .flatMap(word -> Flux.fromArray(word.split("")))
        .distinct()
        .sort()
        .zipWith(Flux.range(1, Integer.MAX_VALUE),
              (string, count) -> String.format("%2d. %s", count, string));

  manyLetters.subscribe(System.out::println);
}

这有助于我们注意s的缺失:

1. a
2. b
...
18. r
19. t
20. u
...
25. z

解决该问题的一种方法是纠正原始单词数组,但我们也可以使用concat/concatWithMono手动将“ s”值添加到字母的Flux中:

@Test
public void restoringMissingLetter() {
  Mono<String> missing = Mono.just("s");
  Flux<String> allLetters = Flux
        .fromIterable(words)
        .flatMap(word -> Flux.fromArray(word.split("")))
        .concatWith(missing)
        .distinct()
        .sort()
        .zipWith(Flux.range(1, Integer.MAX_VALUE),
              (string, count) -> String.format("%2d. %s", count, string));

  allLetters.subscribe(System.out::println);
}

这会在我们过滤出重复项并对字母进行排序/计数之前添加缺少

1. a
2. b
...
18. r
19. s
20. t
...
26. z

上一篇文章指出了Rx词汇表与Streams API之间的相似之处,并且实际上,当可以从内存中轻松获得数据时,Reactor像Java Streams一样以简单的推送模式运行(请参阅下面的反压部分以了解原因)。 更复杂,真正异步的片段不适用于仅在主线程中进行订阅的这种模式,这主要是因为控制将返回到主线程,并在订阅完成后立即退出应用程序。 例如:

@Test
public void shortCircuit() {
  Flux<String> helloPauseWorld = 
              Mono.just("Hello")
                  .concatWith(Mono.just("world")
                  .delaySubscriptionMillis(500));

  helloPauseWorld.subscribe(System.out::println);
}

此代码段显示“ Hello”,但由于测试终止太早而无法打印延迟的“ world”。 在代码片段和测试中,您只需要像这样编写一个主类,通常就需要恢复为阻塞行为。 为此,您可以创建一个CountDownLatch并在您的订户中调用countDown (在onErroronComplete )。 但这不是很被动,是吗? (如果您忘记倒计时,例如发生错误,该怎么办?)

解决该问题的第二种方法是使用返回到非React性环境的运算符之一。 具体来说, toIterabletoStream都会产生一个阻塞实例。 因此,让我们使用toStream作为示例:

@Test
public void blocks() {
  Flux<String> helloPauseWorld = 
    Mono.just("Hello")
        .concatWith(Mono.just("world")
                        .delaySubscriptionMillis(500));

  helloPauseWorld.toStream()
                 .forEach(System.out::println);
}

如您所料,此命令将打印“ Hello”,然后停顿片刻,然后打印“ world”并终止。

如上所述,RxJava amb()运算符已重命名为firstEmitting (这更清楚地表明了该运算符的目的:选择要发射的第一个Flux )。 在下面的示例中,我们创建一个Mono其启动延迟了450ms,并且创建了一个Flux ,该Flux发出其值,并每个值之前暂停400ms。 当将firstEmitting()一起使用时,由于Flux的第一个值在Mono的值之前出现,因此最终会播放Flux

@Test
public void firstEmitting() {
  Mono<String> a = Mono.just("oops I'm late")
                       .delaySubscriptionMillis(450);
  Flux<String> b = Flux.just("let's get", "the party", "started")
                       .delayMillis(400);

  Flux.firstEmitting(a, b)
      .toIterable()
      .forEach(System.out::println);
}

这将打印句子的每个部分,每个部分之间的间隔为400ms。

在这一点上,您可能想知道,如果您正在编写针对通量的测试,该测试引入了4000ms而不是400ms的延迟? 您不想在单元测试中等待4秒! 幸运的是,我们将在后面的部分中看到Reactor带有强大的测试工具,可以很好地涵盖这种情况。

但是到目前为止,我们已经采样了Reactor如何比较一些常见的运算符,因此让我们放大一下,看看库的其他区别方面。

Java 8基础

Reactor面向Java 8,而不是先前的Java版本。 这再次与减少API外观的目标保持一致:RxJava针对Java 6,因为Java 6中没有java.util.function包,因此无法利用FunctionConsumer类的类。 相反,他们不得不像添加特定类Func1Func2Action0Action1 ,等等。在RxJava 2这些类镜java.util.functionReact器2用来做什么时,它仍然有支持Java 7的方式。

Reactor API还包含Java 8中引入的类型。大多数与时间相关的运算符将具有持续时间(例如timeoutintervaldelay等),因此使用Java 8 Duration class是合适的。

Java 8 Stream API和CompletableFuture都可以轻松转换为Flux/Mono ,反之亦然。 我们是否应该通常将Stream转换为Flux ? 并不是的。 当FluxMono装饰更昂贵的操作(如IO或内存绑定操作)时,由FluxMono添加的间接级别的费用可以忽略不计,但是大多数情况下, Stream并不暗示这种延迟,因此完全可以使用直接使用Stream API。 请注意,对于RxJava 2中的这些用例,我们将使用Observable ,因为它没有背压,因此一旦订阅便成为简单的推送用例。 但是Reactor是基于Java 8的,并且Stream API对于大多数用例而言都足够表达。 还要注意,即使您可以找到用于文字或简单对象的FluxMono工厂,它们的主要目的是将它们合并到更高级别的流中。 因此,通常在将现有代码库迁移到React性模式时,不希望将“ long getCount() ”这样的访问器转换为“ Mono<Long> getCount() ”。

背压的故事

其中一个主要的焦点的RS规范的(如果不是主焦点)的,电抗器的本身是背压 。 背压的想法是,在生产者比消费者更快的推动场景中,有价值的是让消费者向生产者发出信号并说“嘿!放慢一点,我不知所措”。 这使生产者有机会控制其速度,而不必诉诸于丢弃数据(采样)或更糟的数据,从而有可能导致级联故障。

您可能会想知道, Mono在背压方面会出现什么:一次排放可能会淹没什么样的消费者? 简短的回答是“可能没有”。 但是, Mono工作方式与CompletableFuture工作方式之间仍然存在关键的区别。 后者仅是push :如果您对Future的引用,则意味着处理异步结果的任务已经在执行。 另一方面,背压式FluxMono启用的是延迟的推拉式交互:

  1. 延迟,因为在调用subscribe()之前没有任何React
  2. 之所以要拉,是因为在订阅和请求步骤中, Subscriber将向源上游发送信号,并从本质上拉出下一个数据块
  3. 从生产上到消费者从那里,请求的元素数量的边界内

对于Monosubscribe()是您按下的按钮,它说“我已经准备好接收数据”。 对于Flux,此按钮是request(n) ,它是前者的一种概括。

意识到Mono是一个Publisher通常会代表一个昂贵的任务(在IO,延迟等方面)是在这里认识背压的价值至关重要:如果你不订阅,您不支付的,该费用任务。 由于Mono通常会在带有规则背压Flux的React链中进行编排,可能会组合来自多个异步源的结果,因此按需订阅触发的可用性是避免阻塞的关键。

背压有助于我们将最后一个用例与另一个Mono广泛用例区分开:将数据从Flux异步聚合到Mono 。 诸如reducehasElement类的运算符能够使用Flux每个项目,聚合有关该项目的某种形式的数据(分别是reduce函数和boolean的结果),并将该数据作为Mono公开。 在这种情况下,上游发出的背压是Long.MAX_VALUE ,它使上游以完全推动的方式工作。

背压的另一个有趣方面是它如何自然地限制流所保存在内存中的对象数量。 作为Publisher ,数据源在生产商品时很可能会很慢(至少较慢),因此来自下游的请求很可能会开始于现有商品数量之外。 在这种情况下,整个流程自然会陷入推送模式,在此模式中,新商品会通知给消费者。 但是,当生产达到顶峰并且生产速度加快时,情况就会很好地恢复为拉动模型。 在这两种情况下,最多N数据( the request()数量)都保存在内存中。

您可以通过将对N需求与某项消耗的千字节数相关联来推断出异步处理使用的内存, W :然后您可以推断出最多将消耗W*N内存。 实际上,Reactor在大多数情况下都会利用了解N优势来进行优化:创建相应限制的队列,并应用预取策略,每次收到相同的3/4数量时,它可以自动请求N的75%。

最后,React堆操作员有时会更改背压信号,以使其与它们所代表的期望和语义相关联。 此行为的一个典型的例子将被buffer(10)为每一个请求N从下游,即操作者将请求10N从上游,它代表足够的数据以填充缓冲器的数目的订户准备消费。 这称为“主动背压”,开发人员可以很好地利用它,以明确告知Reactor如何在微分批处理方案中从输入量切换到不同的输出量。

与春天的关系

Reactor是整个Spring生态系统的React基础,最显着的是Spring 5(通过Spring Web Reactive)和Spring Data“ Kay”(对应于spring-data-commons 2.0)。

在这两个项目中都必须有一个React式版本,因为这使我们能够编写一个从头到尾都是React式的Web应用程序:一个请求传入,一直到数据库(包括数据库)一直被异步处理,结果也会异步返回。 这使Spring应用程序可以非常有效地利用资源,避免了将线程专用于请求并将其阻塞以进行I / O的通常模式。

因此,Reactor将用于将来的Spring应用程序的内部React式管道,以及这些各种Spring组件公开的API中。 更一般而言,他们将能够与RS Publishers打交道,但是大多数情况下,它们将恰好是Flux/Mono ,带来了Reactor的丰富功能集。 当然,您将能够使用您选择的React式库,因为该框架提供了用于在Reactor类型与RxJava类型甚至更简单的RS类型之间进行调整的挂钩。

在撰写本文时,您已经可以通过使用Spring Boot 2.0.0.BUILD-SNAPSHOTspring-boot-starter-web-reactive依赖项在Spring Boot中尝试使用Spring Web Reactive(例如,通过生成这样的在start.spring.io上的项目):

<dependency>
  <groupId>org.springframework.boot.experimental</groupId>
  <artifactId>spring-boot-starter-web-reactive</artifactId>
</dependency>

这使您可以像往常一样编写@Controller ,但是用响应层替换底层的Spring MVC传统层,用响应非阻塞的层替换许多Spring MVC合同。 默认情况下,该响应层基于Tomcat 8.5,但是您也可以选择使用Undertow或Netty。

此外,尽管Spring API基于Reactor类型,但Spring Web Reactive模块可让您将各种React类型用于请求和响应:

  • Mono<T> :作为@RequestBody ,请求实体T被异步反序列化,之后您可以将处理链接到生成的Mono。 作为返回类型, Mono发出值后,T将被异步序列化并发送回客户端。 您可以通过扩展请求Mono并将该扩展链作为结果Mono返回,来结合两种方法。
  • Flux<T> :在流场景中使用(包括用作@RequestBody和具有Flux<ServerSentEvent>返回类型的服务器发送事件时的输入流)
  • Single/Observable :分别与MonoFlux相同,但是切换到RxJava实现。
  • Mono<Void>作为返回类型:Mono完成后,请求处理完成。
  • 非React性返回类型( voidT ):现在这意味着您的控制器方法是同步的, 但应该是非阻塞的 (短期处理)。 方法执行后,请求处理完成。 返回的T异步地序列化回客户端。

这是使用实验性WebReact模块的纯文本@Controller的快速示例:

@RestController
public class ExampleController {

   private final MyReactiveLibrary reactiveLibrary;

   //Note Spring Boot 4.3+ autowires single constructors now
   public ExampleController(MyReactiveLibrary reactiveLibrary) {
      this.reactiveLibrary = reactiveLibrary;
   }

   @GetMapping("hello/{who}")
   public Mono<String> hello(@PathVariable String who) {
      return Mono.just(who)
                 .map(w -> "Hello " + w + "!");
   }

   @GetMapping("helloDelay/{who}")
   public Mono<String> helloDelay(@PathVariable String who) {
      return reactiveLibrary.withDelay("Hello " + who + "!!", 2);
   }

   @PostMapping("heyMister")
   public Flux<String> hey(@RequestBody Mono<Sir> body) {
      return Mono.just("Hey mister ")
            .concatWith(body
                .flatMap(sir -> Flux.fromArray(sir.getLastName().split("")))
                .map(String::toUpperCase)
                .take(1)
            ).concatWith(Mono.just(". how are you?"));
   }
}

第一个端点采用路径变量,将其转换为Mono<String>并将该名称maps到返回给客户端的问候语。

通过在/hello/Simon上执行GET,我们得到“ Hello Simon!”。 作为文本/普通回复。

第二个端点稍微复杂一点:它异步接收序列化的Sir实例(一个仅由firstNamelastName属性组成的类), flatMapsflatMaps到姓氏字母的流中。 那么它需要第一这些信件, maps它为大写和concat enates它变成一个问候语。

因此,将以下JSON对象发布到/heyMister

{
	"firstName": "Paul",
	"lastName": "tEsT"
}

返回字符串“ 你好先生T。你好吗?”

Kay发布系列目前也在开发Spring Data的响应方面,对于spring-data-commons ,它是2.0.x分支。 通过将Spring Data Kay-M1 bom添加到pom中可以获得第一个里程碑

<dependencyManagement>
  <dependencies>
     <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-releasetrain</artifactId>
        <version>Kay-M1</version>
        <scope>import</scope>
        <type>pom</type>
     </dependency>
  </dependencies>
</dependencyManagement>

然后,对于这个简单的示例,只需在pom中添加Spring Data Commons依赖项(它将从上面的BOM中获取版本):

<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-commons</artifactId>
</dependency>

Spring Data中的React性支持围绕新的ReactiveCrudRepository<T, ID>接口展开,该接口扩展了Repository<T, ID> 。 该接口使用Reactor输入和返回类型公开CRUD方法。 还有一个称为RxJava1CrudRepository基于RxJava 1的版本。 例如,在经典阻塞式CrudRepository ,将使用“ T findOne(ID id) ”来通过其id检索一个实体。 在ReactiveCrudRepositoryRxJava1CrudRepository中, RxJava1CrudRepository分别变为“ Mono<T> findOne(ID id) ”和“ Observable<T> findOne(ID id) ”。 甚至有采用Mono / Single作为参数的变体,以异步方式提供密钥并以此为基础进行编写。

假设一个React式后备存储(或模拟ReactiveCrudRepository bean),以下(非常幼稚)的控制器将从头到尾都是React性的:

@RestController
public class DataExampleController {

   private final ReactiveCrudRepository<Sir, String> reactiveRepository;

   //Note Spring Boot 4.3+ autowires single constructors now
   public DataExampleController(ReactiveCrudRepository<Sir, String> repo) {
      this.reactiveRepository = repo;
   }

   @GetMapping("data/{who}")
   public Mono<ResponseEntity<Sir>> hello(@PathVariable String who) {
      return reactiveRepository.findOne(who)
                   .map(ResponseEntity::ok)
                   .defaultIfEmpty(ResponseEntity.status(404).body(null));
   }
}

请注意,数据存储库的使用情况是如何自然地流入响应路径的:我们异步获取实体,并使用map将其包装为ResponseEntity ,获得可以立即返回的Mono 。 如果Spring Data存储库找不到该键的数据,它将返回一个空的Mono 。 我们通过使用defaultIfEmpty并返回404使其明确。

测试React堆

文章“测试RxJava”介绍了用于测试Observable技术。 如我们所见,RxJava附带了一个TestScheduler ,您可以将其与接受Scheduler作为参数的运算符一起使用,以操纵这些运算符上的虚拟时钟。 它还具有一个TestSubscriber类,可以利用该类来等待Observable的完成并对每个事件进行断言( onNext数量和值,触发onError等)。在RxJava 2中, TestSubscriber是一个RS Subscriber ,这样您就可以用它测试Reactor的FluxMono

在Reactor中,这两个主要功能组合到了StepVerifier类中。 可以在reactor-addons存储库的附加模块reactor-test找到它。 该StepVerifier可以从任意创建一个实例初始化Publisher ,使用StepVerifier.create建设者。 如果要使用虚拟时间,则可以使用StepVerifier.withVirtualTime构建器,该构建器使用Supplier<Publisher> 。 这样做的原因是,它将首先确保创建VirtualTimeScheduler并将其启用为默认的Scheduler实现以供使用,从而无需将调度程序显式传递给操作员。 然后,StepVerifier将在必要时配置在供应商中创建的Flux/Mono ,将定时运算符转换为“虚拟定时运算符”。 然后,您可以编写流期望值和时间进度的脚本:下一个元素应该是什么,应该有错误,应该在时间上向前移动等。其他方法包括验证数据是否与给定的Predicate匹配,甚至消耗onNext事件,从而允许您与值进行更高级的交互(例如使用断言库)。 其中之一引发的任何AssertionError都会反映在最终的验证结果中。 最后,调用verify()来检查您的期望,这将通过StepVerifier.createStepVerifier.withVirtualTime真正订阅已定义的源。

让我们举几个简单的例子,演示StepVerifier工作原理。 对于这些片段,您需要向pom添加以下测试依赖项:

<dependency>
  <groupId>io.projectreactor.addons</groupId>
  <artifactId>reactor-test</artifactId>
  <version>3.0.3.RELEASE</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <version>3.5.2</version>
  <scope>test</scope>
</dependency>

首先,假设您有一个名为MyReactiveLibraryReact式类,它会生成一些要测试的Flux

@Component
public class MyReactiveLibrary {

  public Flux<String> alphabet5(char from) {
     return Flux.range((int) from, 5)
           .map(i -> "" + (char) i.intValue());
  }

  public Mono<String> withDelay(String value, int delaySeconds) {
     return Mono.just(value)
                .delaySubscription(Duration.ofSeconds(delaySeconds));
  }
}

第一种方法旨在返回给定起始字母之后(包括在内)的5个字母。 第二种方法返回的磁通量在给定的延迟后以秒为单位发出一个给定的值。

我们要编写的第一个测试可确保从x调用alphabet5将输出限制为x,y,z。 使用StepVerifier ,它将像这样:

@Test
public void testAlphabet5LimitsToZ() {
  MyReactiveLibrary library = new MyReactiveLibrary();
  StepVerifier.create(library.alphabet5('x'))
        .expectNext("x", "y", "z")
        .expectComplete()
        .verify();
}

我们要对alphabet5运行的第二个测试是,每个返回的值都是一个字母字符。 为此,我们想使用像AssertJ这样的丰富的断言库:

@Test
public void testAlphabet5LastItemIsAlphabeticalChar() {
  MyReactiveLibrary library = new MyReactiveLibrary();
  StepVerifier.create(library.alphabet5('x'))
              .consumeNextWith(c -> assertThat(c)
                    .as("first is alphabetic").matches("[a-z]"))
              .consumeNextWith(c -> assertThat(c)
                    .as("second is alphabetic").matches("[a-z]"))
              .consumeNextWith(c -> assertThat(c)
                    .as("third is alphabetic").matches("[a-z]"))
              .consumeNextWith(c -> assertThat(c)
                    .as("fourth is alphabetic").matches("[a-z]"))
              .expectComplete()
              .verify();
}

原来这两个测试都失败了:(。让我们看一下StepVerifier在每种情况下提供给我们的输出,看看是否可以发现该错误:

java.lang.AssertionError: expected: onComplete(); actual: onNext({)

java.lang.AssertionError: [fourth is alphabetic] 
Expecting:
 "{"
to match pattern:
 "[a-z]"

因此,看来我们的方法不会在z处停止,而是会继续发出ASCII范围内的字符。 我们可以通过添加一个.take(Math.min(5, 'z' - from + 1))来解决此问题,或者使用相同的Math.min作为range的第二个参数。

我们要进行的最后一个测试涉及虚拟时间操纵:我们将使用withVirtualTime构建器来测试延迟方法,但实际上无需等待给定的秒数:

@Test
public void testWithDelay() {
  MyReactiveLibrary library = new MyReactiveLibrary();
  Duration testDuration =
     StepVerifier.withVirtualTime(() -> library.withDelay("foo", 30))
                 .expectSubscription()
                 .thenAwait(Duration.ofSeconds(10))
                 .expectNoEvent(Duration.ofSeconds(10))
                 .thenAwait(Duration.ofSeconds(10))
                 .expectNext("foo")
                 .expectComplete()
                 .verify();
  System.out.println(testDuration.toMillis() + "ms");
}

这将测试以下情况下的通量,该通量将延迟30秒:立即订阅,然后是3x10s,什么都没发生,然后是onNext(“ foo ”)和完成。

System.out输出显示了验证所花费的实际时间,在我的最新运行中是8ms :)

请注意,当改用create builder时, thenAwaitexpectNoEvent方法仍然可用,但实际上会在提供的持续时间内阻塞。

StepVerifier提供了许多其他方法来描述发布者的期望值和声明Publisher状态(如果您考虑新的Publisher ,那么在github存储库中总是欢迎您提供文稿和反馈)。

定制热源

请注意,“ RxJava示例”末尾讨论的热和冷可观察性的概念也适用于Reactor。

如果要创建自定义Flux,而不是RxJava AsyncEmitter类,则可以使用Reactor的FluxSink 。 这将为您涵盖所有异步极端情况,并让您专注于体现自己的价值观。

使用Flux.create并在回调中获取FluxSink ,您可以使用它通过next发出数据。 此自定义Flux可能很冷,因此为了使其变热,可以使用publish()和connect()。 以上一篇文章的价格为基础的示例为基础,我们在Reactor中得到了几乎逐字的翻译:

SomeFeed<PriceTick> feed = new SomeFeed<>();
Flux<PriceTick> flux =
     Flux.create(emitter ->
     {
        SomeListener listener = new SomeListener() {
           @Override
           public void priceTick(PriceTick event) {
              emitter.next(event);
              if (event.isLast()) {
                 emitter.complete();
              }
           }

           @Override
           public void error(Throwable e) {
              emitter.error(e);
           }};
        feed.register(listener);
     }, FluxSink.OverflowStrategy.BUFFER);

ConnectableFlux<PriceTick> hot = flux.publish();

在连接到热通量之前,为什么不订阅两次 ? 一个订阅将打印每个报价的详细信息,而另一个将仅打印工具:

hot.subscribe(priceTick -> System.out.printf("%s %4s %6.2f%n", priceTick
     .getDate(), priceTick.getInstrument(), priceTick.getPrice()));

hot.subscribe(priceTick -> System.out.println(priceTick.getInstrument()));

然后,我们连接到热通量,并让其运行5秒钟,然后测试片段终止:

hot.connect();
Thread.sleep(5000);

(请注意,在示例存储库中,如果更改PriceTickisLast()方法,则供稿也将自行终止)。

FluxSink还允许您检查下游是否通过isCancelled()取消了其订阅。 您还可以通过requestedFromDownstream()获得有关未偿还请求金额的反馈,如果您只想遵守背压,这将非常有用。 最后,您可以通过setCancellation确保在Cancellation释放您的源使用的任何特定资源。

请注意,使用FluxSink有背压的含义:您必须显式提供OverflowStrategy以使操作员处理背压。 这等效于使用onBackpressureXXX运算符(例如FluxSink.OverflowStrategy.BUFFER等效于使用.onBackpressureBuffer() ),后者会覆盖下游的任何反压指令。

结论

在本文中,您了解了Reactor,它是第四代React式库,它基于Rx语言构建,但面向Java 8和Reactive Streams规范。 我们已经展示了您可能在RxJava中学到的概念如何也适用于Reactor,尽管API有所不同。 我们还展示了Reactor如何作为Spring 5的基础,它为测试Publisher/Flux/Mono提供了资源。

如果您想更深入地使用Reactor,可以在我们的github存储库中找到本文介绍的摘录。 还有一个研讨会,“ Lite Rx API动手实践 ”,涵盖了更多的操作员和用例。

最后,您可以联系Gitter上的Reactor团队并在那里或通过github问题提供反馈(当然,也欢迎请求请求)。

翻译自: https://www.infoq.com/articles/reactor-by-example/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

react实例

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值