Spring Reactor

Flux, 包含 0-N 个元素的异步序列

在这里插入图片描述
Flux 是一个能够发出 0 到 N 个元素的标准的 Publisher,它会被一个“错误(error)” 或“完成(completion)”信号终止。因此,一个 flux 的可能结果是一个 value、completion 或 error。

  • 如果没有 onNext 事件但是有一个 onComplete 事件, 那么发出的就是 空的 有限序列
    // subscribe 首先触发request,然后onNext, 默认request(Long.MAX_VALUE)。这里没有定义onNext, 所以什么都不会发生, 不会打印end
    Flux.from(Subscriber::onComplete).subscribe(it -> System.out.println("end"));
    
  • 如果没有 onNext 和 onComplete 事件, 那么得到的就是一个 无限的 空序列
    Flux.empty().subscribe(it -> System.out.println("end"));
    

Mono, 异步的 0-1 结果

在这里插入图片描述
Mono 是一种特殊的 Publisher, 它最多发出一个元素,然后终止于一个 onComplete 信号或一个 onError 信号。

它只适用其中一部分可用于 Flux 的操作。比如,(两个 Mono 的)结合类操作可以忽略其中之一 而发出另一个 Mono,也可以将两个都发出,对于后一种情况会切换为一个 Flux。

例如,Mono#concatWith(Publisher) 返回一个 Flux,而 Mono#then(Mono) 返回另一个 Mono。

注意,Mono 可以用于表示“空”的只有完成概念的异步处理(比如 Runnable)。这种用 Mono 来创建。

简单的创建和订阅 Flux 或 Mono 的方法

Flux<String> seq1 = Flux.just("foo", "bar", "foobar"); // 参数列表
Flux<String> seq2 = Flux.fromIterable(Arrays.asList("foo", "bar", "foobar")); // List
Mono<String> noData = Mono.empty(); // 空
Mono<String> data = Mono.just("foo"); //一个值
Flux<Integer> numbersFromFiveToSeven = Flux.range(5, 3); // 递增值, 起始值5,长度3 

基于 lambda 的对 Flux 的订阅(subscribe)

// 订阅并触发序列
subscribe(); 
// 对每一个生成的元素进行消费。
subscribe(Consumer<? super T> consumer);  
// 对正常元素进行消费,也对错误进行响应。
subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer);
// 对正常元素和错误均有响应,还定义了序列正常完成后的回调。
subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer); 
// 对正常元素、错误和完成信号均有响应, 同时也定义了对该 subscribe 方法返回的 Subscription 执行的回调。
subscribe(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer,
          Consumer<? super Subscription> subscriptionConsumer); 

subscribe 方法示例

Flux<Integer> ints = Flux.range(1, 3); 
ints.subscribe();
// 无输出, 但Flux确实已经执行
Flux<Integer> ints = Flux.range(1, 3); 
ints.subscribe(i -> System.out.println(i)); 
// 1
// 2
// 3
Flux<Integer> ints = Flux.range(1, 4) 
      .map(i -> { 
        if (i <= 3) return i; 
        throw new RuntimeException("Got to 4"); 
      });
ints.subscribe(i -> System.out.println(i), 
      error -> System.err.println("Error: " + error));
// 1
// 2
// 3
// Error: java.lang.RuntimeException: Got to 4
Flux<Integer> ints = Flux.range(1, 3); 
ints.subscribe(i -> System.out.println(i),
    error -> System.err.println("Error " + error),
    () -> {System.out.println("Done");}); 
// 1
// 2
// 3
// Done
// Reactor 推荐用户扩展 BaseSubscriber 来实现自定义的 Subscriber
public class SampleSubscriber<T> extends BaseSubscriber<T> {
    public void hookOnSubscribe(Subscription subscription) {
        System.out.println("Subscribed");
        request(1);
    }
    public void hookOnNext(T value) {
        System.out.println(value);
        request(1);
    }
}

Flux<Integer> ints = Flux.range(1, 4);
ints.subscribe(new SampleSubscriber<Integer>());
// Subscribed
// 1
// 2
// 3
// 4

使用 BaseSubscriber 来配置“背压”

Flux<String> source = someStringSource();
source.map(String::toUpperCase)
// BaseSubscriber 是一个抽象类,所以我们创建一个匿名内部类。
.subscribe(new BaseSubscriber<String>() { 
    @Override
    protected void hookOnSubscribe(Subscription subscription) {
        // 	BaseSubscriber 定义了多种用于处理不同信号的 hook。它还定义了一些捕获 Subscription 对象的现成方法,这些方法可以用在 hook 中。
        // 	request(n) 就是这样一个方法。它能够在任何 hook 中,通过 subscription 向上游传递 背压请求。这里我们在开始这个流的时候请求1个元素值。
        request(1); 
    }

    @Override
    protected void hookOnNext(String value) {
        // 随着接收到新的值,我们继续以每次请求一个元素的节奏从源头请求值。
        request(1); 
    }
	// 	其他 hooks 有 hookOnComplete, hookOnError, hookOnCancel, and hookFinally (它会在流终止的时候被调用,传入一个 SignalType 作为参数)。
});

注意: BaseSubscriber 在进行扩展的时候要覆盖 hookOnSubscribe 和 onNext,这样你至少会调用 request 一次,否则上游的 Flux 可能会被“卡住”

  • BaseSubscriber 还提供了 requestUnbounded() 方法来切换到“无限”模式(等同于 request(Long.MAX_VALUE))。
  • request(0) 等同于 cancel()

可编程式地创建一个序列

Generate

这是一种同步地, 逐个地产生值的方法,意味着 sink 是一个 SynchronousSink 而且其 next() 方法在每次回调的时候最多只能被调用一次。你也可以调用 error(Throwable) 或者 complete(),不过是可选的。
在这里插入图片描述

Flux<String> flux = Flux.generate(
	// 初始化状态值(state)为0。
    () -> 0, 
    (state, sink) -> {
      // 我们基于状态值 state 来生成下一个值(state 乘以 3)
      sink.next("3 x " + state + " = " + 3*state); 
      // 我们也可以用状态值来决定什么时候终止序列
      if (state == 3) sink.complete(); 
      // 返回一个新的状态值 state,用于下一次调用
      return state + 1; 
    });
// 3 x 0 = 0
// 3 x 1 = 3
// 3 x 2 = 6
// 3 x 3 = 9

下面是一个在 generate 方法中增加 Consumer 的例子:

Flux<String> flux = Flux.generate(
	// 初始化一个可变对象作为状态变量。(原生类型及其包装类,以及String等属于不可变类型)
    AtomicLong::new,
      (state, sink) -> { 
      // 改变状态值。
      long i = state.getAndIncrement(); 
      sink.next("3 x " + i + " = " + 3*i);
      if (i == 10) sink.complete();
      // 返回 同一个 实例作为新的状态。
      return state; 
    },
    // Comsumer 在序列终止才被调用: 我们会看到最后一个状态值(11)会被这个 Consumer lambda 输出。
    // 可用于清理状态对象
    (state) -> System.out.println("state: " + state)); 
}

Create

创建 Flux 的方式, create 方法的生成方式既可以是同步, 也可以是异步的,并且还可以每次发出多个元素。
在这里插入图片描述

该方法用到了 FluxSink,后者同样提供 next,error 和 complete 等方法。 与 generate 不同的是,create 不需要状态值,另一方面,它可以在回调中触发 多个事件(即使是在未来的某个时间)
create 有个好处就是可以将现有的 API 转为响应式,比如监听器的异步方法。

// 一个监听器
interface MyEventListener<T> {
    void onDataChunk(List<T> chunk);
    void processComplete();
}
Flux<String> bridge = Flux.create(sink -> {
	// 桥接监听器
    myEventProcessor.register( 
      new MyEventListener<String>() { 

        public void onDataChunk(List<String> chunk) {
          for(String s : chunk) {
            sink.next(s); 
          }
        }

        public void processComplete() {
            sink.complete(); 
        }
    });
});

create 可以是异步地,并且能够控制背压, 因此还有一个重载函数用于控制背压,你可以通过提供一个 OverflowStrategy 来定义背压行为。

Flux<T> create(Consumer<? super FluxSink<T>> emitter, OverflowStrategy backpressure)
  • IGNORE: 完全忽略下游背压请求,这可能会在下游队列积满的时候导致 IllegalStateException。
  • ERROR: 当下游跟不上节奏的时候发出一个 IllegalStateException 的错误信号。
  • DROP:当下游没有准备好接收新的元素的时候抛弃这个元素。
  • LATEST:让下游只得到上游最新的元素。
  • BUFFER:(默认的)缓存所有下游没有来得及处理的元素(这个不限大小的缓存可能导致 OutOfMemoryError)。

Mono 也有一个用于 create 的生成器 —— MonoSink,它不能生成多个元素, 因此会抛弃第一个元素之后的所有元素。

推送(push)模式

create 的一个变体是 push,适合生成事件流。与 create类似,push 也可以是异步地, 并且能够使用以上各种溢出策略(overflow strategies)管理背压。每次只有一个生成线程可以调用 next,complete 或 error。
有关 push 与 create 的对比请参考:Flux.push PK Flux.create

清理(Cleaning up)

onDispose 和 onCancel 这两个回调用于在被取消和终止后进行清理工作。 onDispose 可用于在 Flux 完成,有错误出现或被取消的时候执行清理。 onCancel 只用于针对“取消”信号执行相关操作,会先于 onDispose 执行。

Flux<String> bridge = Flux.create(sink -> {
    sink.onRequest(n -> channel.poll(n))
        .onCancel(() -> channel.cancel()) 
        .onDispose(() -> channel.close())  
    });
  • onCancel 在取消时被调用。
  • onDispose 在有完成、错误和取消时被调用。

Handle

handle 方法有些不同,它在 Mono 和 Flux 中都有。然而,它是一个实例方法 (instance method),意思就是它要链接在一个现有的源后使用(与其他操作符一样)
它与 generate 比较类似,因为它也使用 SynchronousSink,并且只允许元素逐个发出。 然而,handle 可被用于基于现有数据源中的元素生成任意值,有可能还会跳过一些元素。 这样,可以把它当做 map 与 filter 的组合。handle 方法签名如下:

handle(BiConsumer<T, SynchronousSink<R>>)
public String alphabet(int letterNumber) {
        if (letterNumber < 1 || letterNumber > 26) {
                return null;
        }
        int letterIndexAscii = 'A' + letterNumber - 1;
        return "" + (char) letterIndexAscii;
}
Flux.just(-1, 30, 13, 9, 20)
    .handle((i, sink) -> {
    	// 获取数字映射的字母
        String letter = alphabet(i); 
        if (letter != null) 
        	// 如果不是空才会调用 sink.next
            sink.next(letter); 
    }).subscribe(System.out::println);

调度器

Scheduler 是一个拥有广泛实现类的抽象接口。 Schedulers 类提供的静态方法用于达成如下的执行环境:

  • 当前线程(Schedulers.immediate())
  • 可重用的单线程(Schedulers.single())。注意,这个方法对所有调用者都提供同一个线程来使用, 直到该调度器(Scheduler)被废弃。如果你想使用专一的线程,就对每一个调用使用 Schedulers.newSingle()。
  • 弹性线程池(Schedulers.elastic()。它根据需要创建一个线程池,重用空闲线程。线程池如果空闲时间过长 (默认为 60s)就会被废弃。对于 I/O 阻塞的场景比较适用。 Schedulers.elastic() 能够方便地给一个阻塞 的任务分配它自己的线程,从而不会妨碍其他任务和资源,见 如何包装一个同步阻塞的调用?。
  • 固定大小线程池(Schedulers.parallel())。所创建线程池的大小与 CPU 个数等同。

此外,你还可以使用 Schedulers.fromExecutorService(ExecutorService) 基于现有的 ExecutorService 创建 Scheduler。(虽然不太建议,不过你也可以使用 Executor 来创建)。你也可以使用 newXXX 方法来创建不同的调度器。比如 Schedulers.newElastic(yourScheduleName) 创建一个新的名为 yourScheduleName 的弹性调度器。
一些操作符默认会使用一个指定的调度器(通常也允许开发者调整为其他调度器)例如, 通过工厂方法 Flux.interval(Duration.ofMillis(300)) 生成的每 300ms 打点一次的 Flux, 默认情况下使用的是 Schedulers.parallel(),下边的代码演示了如何将其装换为 Schedulers.single():

Flux.interval(Duration.ofMillis(300), Schedulers.newSingle("test"))

Reactor 提供了两种在响应式链中调整调度器 Scheduler 的方法:publishOn 和 subscribeOn。 它们都接受一个 Scheduler 作为参数,从而可以改变调度器。但是 publishOn 在链中出现的位置 是有讲究的,而 subscribeOn 则无所谓。要理解它们的不同,你首先要理解 nothing happens until you subscribe()。

  • 基于此,我们仔细研究一下 publishOn 和 subscribeOn 这两个操作符:
    publishOn 的用法和处于订阅链(subscriber chain)中的其他操作符一样。它将上游 信号传给下游,同时执行指定的调度器 Scheduler 的某个工作线程上的回调。 它会 改变后续的操作符的执行所在线程 (直到下一个 publishOn 出现在这个链上)。
  • subscribeOn 用于订阅(subscription)过程,作用于那个向上的订阅链(发布者在被订阅 时才激活,订阅的传递方向是向上游的)。所以,无论你把 subscribeOn 至于操作链的什么位置, 它都会影响到源头的线程执行环境(context)。 但是,它不会影响到后续的 publishOn,后者仍能够切换其后操作符的线程执行环境。

我的理解:publishOn至上而下,会影响下游的线程。而subscribeOn至下而上,(由于在订阅前什么都不会发生)直接影响源头的线程(多个subscribeOn只有第一个生效)

线程模型

publishOn 强制下一个操作符(很可能包括下一个的下一个…​)来运行在一个不同的线程上。 类似的,subscribeOn 强制上一个操作符(很可能包括上一个的上一个…​)来运行在一个不同的线程上。 记住,在你订阅(subscribe)前,你只是定义了处理流程,而没有启动发布者。基于此,Reactor 可以使用这些规则来决定如何执行操作链。然后,一旦你订阅了,整个流程就开始工作了。
Scheduler.parallel() 创建一个基于单线程 ExecutorService 的固定大小的任务线程池。 因为可能会有一个或两个线程导致问题,它总是至少创建 4 个线程。然后 publishOn 方法便共享了这些任务线程, 当 publishOn 请求元素的时候,会从任一个正在发出元素的线程那里获取元素。这样, 就是进行了任务共享(一种资源共享方式)。Reactor 还提供了好几种共享资源的方式,请参考 Schedulers。
Scheduler.elastic() 也能创建线程,它能够很方便地创建专门的线程(以便跑一些可能会阻塞资源的任务, 比如一个同步服务)Scheduler.elastic() 也能创建线程,它能够很方便地创建专门的线程(以便跑一些可能会阻塞资源的任务, 比如一个同步服务)

处理错误

在响应式流中,错误(error)是终止(terminal)事件。当有错误发生时,它会导致流序列停止, 并且错误信号会沿着操作链条向下传递,直至遇到你定义的 Subscriber 及其 onError 方法。
这样的错误还是应该在应用层面解决的。比如,你可能会将错误信息显示在用户界面,或者通过某个 REST 端点(endpoint)发出。因此,订阅者(subscriber)的 onError 方法是应该定义的。
如果没有定义,onError 会抛出 UnsupportedOperationException。你可以接下来再 检测错误,并通过 Exceptions.isErrorCallbackNotImplemented 方法捕获和处理它。
响应式流中的任何错误都是一个终止事件。 即使用了错误处理操作符,也不会让源头流序列继续。而是将 onError 信号转化为一个 新的 序列 的开始。换句话说,它代替了被终结的 上游 流序列。
错误处理通常分为以下几种 :

  1. 捕获并返回一个静态的缺省值。
  2. 捕获并执行一个异常处理方法。
  3. 捕获并动态计算一个候补值来顶替。
  4. 捕获,并再包装为某一个 业务相关的异常,然后再抛出业务异常。
  5. 捕获,记录错误日志,然后继续抛出。
  6. 使用 finally 来清理资源,或使用 Java 7 引入的 “try-with-resource”。
Flux<String> s = Flux.range(1, 10)
	// 执行 map 转换,有可能抛出异常。
    .map(v -> doSomethingDangerous(v)) 
    // 	如果第一个没问题,执行第二个 map 转换操作。
    .map(v -> doSecondTransform(v)); 
    //	所有转换成功的值都打印出来。
s.subscribe(value -> System.out.println("RECEIVED " + value), 
			// 一旦有错误,序列(sequence)终止,并打印错误信息。
            error -> System.err.println("CAUGHT " + error) 
);

这与 try/catch 代码块是类似的:

try {
    for (int i = 1; i < 11; i++) {
        String v1 = doSomethingDangerous(i); 
        String v2 = doSecondTransform(v1); 
        System.out.println("RECEIVED " + v2);
    }
} catch (Throwable t) {
    System.err.println("CAUGHT " + t); 
}

1. 静态缺省值

Flux.just(10)
    .map(this::doSomethingDangerous)
    // 捕获异常并返回一个静态的缺省值
    .onErrorReturn("RECOVERED");
Flux.just(10)
    .map(this::doSomethingDangerous)
    // 通过判断错误信息的内容,来筛选哪些要给出缺省值,哪些仍然让错误继续传递下去
    .onErrorReturn(e -> e.getMessage().equals("boom10"), "recovered10");

2. 异常处理方法

onErrorResume, 该方法比onErrorReturn 更加灵活
假设,你会尝试从一个外部的不稳定服务获取数据,但仍然会在本地缓存一份 可能 有些过期的数据, 因为缓存的读取更加可靠。可以这样来做:

Flux.just("key1", "key2")
    .flatMap(k -> callExternalService(k)) 
    .onErrorResume(e -> getFromCache(k)); 

onErrorResume 也可以用于预先过滤错误内容

Flux.just("timeout1", "unknown", "key2")
    .flatMap(k -> callExternalService(k))
    .onErrorResume(error -> { 
        if (error instanceof TimeoutException) 
            return getFromCache(k);
        else if (error instanceof UnknownKeyException)  
            return registerNewEntry(k, "DEFAULT");
        else
            return Flux.error(error); 
    });

3. 动态候补值

有时候并不想提供一个错误处理方法,而是想在接收到错误的时候计算一个候补的值。

Flux.create(s -> {
    throw new RuntimeException("create error");
})
    .onErrorResume(error -> Mono.just("异常已解决:" + error.getMessage()))
    .subscribe(System.out::println, System.err::println);

4. 捕获并重新抛出

Flux.just("timeout1")
	// 这里可能发生异常
    .flatMap(k -> callExternalService(k))
    .onErrorResume(original -> Flux.error(
        new BusinessException("oops, SLA exceeded", original)
    );

或者使用专用于错误映射的方法: onErrorMap

Flux.just("timeout1")
    .flatMap(k -> callExternalService(k))
    .onErrorMap(original -> new BusinessException("oops, SLA exceeded", original));

5. 记录错误日志

doOnError 方法不会修改错误(该方法与其他以 doOn 开头的方法一样,对序列都是只读的),并且让错误继续传递下去

LongAdder failureStat = new LongAdder();
Flux<String> flux =
Flux.just("unknown")
    .flatMap(k -> callExternalService(k)) 
    .doOnError(e -> {
        failureStat.increment();
        log("uh oh, falling back, service failed for key " + k); 
    })
    .onErrorResume(e -> getFromCache(k)); 

6. 使用资源和 try-catch 代码块

资源块

//        try(FileInputStream input = new FileInputStream(""))
        Flux.using(() -> new FileInputStream(""),
//                发布序列
                inputStream -> Flux.just(inputStream),
//                finally{ input.close() }
                inputStream -> {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });

finally

FileInputStream input = new FileInputStream("");
Flux.just(1, 2, 3)
        .doFinally(type -> {
            try {
                input.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

重试

retry,见文知意,用它可以对出现错误的序列进行重试。
它对于上游 Flux 是基于重订阅(re-subscribing)的方式。这实际上已经一个不同的序列了, 发出错误信号的序列仍然是终止了的。为了验证这一点,我们举出下面例子,增加一个 retry(1) 代替 onErrorReturn 来重试一次。

 Flux.interval(Duration.ofMillis(250))
    .map(input -> {
        if (input < 3) return "tick " + input;
        throw new RuntimeException("boom");
    })
    // 	elapsed 会关联从当前值与上个值发出的时间间隔
    .elapsed() 
    // 重试一次
    .retry(1)
    .subscribe(System.out::println, System.err::println); 

// Thread.sleep(2100); 
// 259,tick 0
// 249,tick 1
// 251,tick 2
// 506,tick 0 
// 248,tick 1
// 253,tick 2
// java.lang.RuntimeException: boom

高级重试

retryWhen , 它使用一个伴随(“companion”) Flux 来判断对某次错误是否要重试
这个伴随 Flux 是一个 Flux,它作为 retryWhen 的唯一参数被传递给一个 Function,你可以定义这个 Function 并让它返回一个新的 Publisher<?>。重试的循环 会这样运行:

  • 每次出现错误,错误信号会发送给伴随 Flux,后者已经被你用 Function 包装。
  • 如果伴随 Flux 发出元素,就会触发重试。
  • 如果伴随 Flux 完成(complete),重试循环也会停止,并且原始序列也会 完成(complete)。
  • 如果伴随 Flux 产生一个错误,重试循环停止,原始序列也停止 或 完成,并且这个错误会导致 原始序列失败并终止。

如果让伴随 Flux 完成(complete)等于吞掉了错误。
如下代码用 retryWhen 模仿了 retry(3) 的效果:

Flux<String> flux = Flux
	// 持续产生错误
    .<String>error(new IllegalArgumentException()) 
    // 在 retry 之前 的 doOnError 可以让我们看到错误。
    .doOnError(System.out::println) 
 .retryWhen(Retry.from(companion -> 
        companion.take(3))); 

事实上,上边例子最终得到的是一个 空的 Flux,但是却 成功 完成了。反观对同一个 Flux 调用 retry(3) 的话,最终是以最后一个 error 终止 Flux,故而 retryWhen 与之不同。

AtomicInteger errorCount = new AtomicInteger();
Flux.<String>error(new IllegalArgumentException())
    .log()
    .doOnError(e -> errorCount.incrementAndGet())
    .retryWhen(Retry.from(companion ->
            companion.map(rs -> {
                if (rs.totalRetries() < 3) return rs.totalRetries();
                else throw Exceptions.propagate(rs.failure());
            })
    )).blockLast();

在操作符或函数式中处理异常

一个 不受检异常(Unchecked Exception) 总是由 onError 传递。例如, 在一个 map 方法中抛出 RuntimeException 会被翻译为一个 onError 事件,如下:

Flux.just("foo")
    .map(s -> { throw new IllegalArgumentException(s); })
    .subscribe(v -> System.out.println("GOT VALUE"),
               e -> System.out.println("ERROR: " + e));
// ERROR: java.lang.IllegalArgumentException: foo

如果你需要调用一个声明为 throws 异常的方法,你仍然需要使用 try-catch 代码块处理异常。 有几种方式:

  • 捕获异常,并修复它,流序列正常继续。
  • 捕获异常,并把它包装(wrap)到一个 不受检异常 中,然后抛出(中断序列)。工具类 Exceptions 可用于这种方式(我们马上会讲到)。
  • 如果你气我返回一个 Flux (例如在 flatMap 中),将异常包装在一个产生错误的 Flux中:return Flux.error(checkedException)(流序列也会终止)。

Reactor 有一个工具类 Exceptions,可以确保在收到受检异常的时候将其包装(wrap)起来。

  • 如果需要,可以使用 Exceptions.propagate 方法来包装异常,它同样会首先调用 throwIfFatal, 并且不会包装 RuntimeException。
  • 使用 Exceptions.unwrap 方法来得到原始的未包装的异常(追溯最初的异常)。

下面是一个 map 的例子,它使用的 convert 方法会抛出 IOException:

public String convert(int i) throws IOException {
    if (i > 3) {
        throw new IOException("boom " + i);
    }
    return "OK " + i;
}
Flux<String> converted = Flux
    .range(1, 10)
    .map(i -> {
        try { return convert(i); }
        catch (IOException e) { throw Exceptions.propagate(e); }
    });

当后边订阅上边的这个 Flux 并响应错误(比如在用户界面)的时候,如果你想处理 IOException, 你还可以再将其转换为原始的异常。如下:

converted.subscribe(
    v -> System.out.println("RECEIVED: " + v),
    e -> {
        if (Exceptions.unwrap(e) instanceof IOException) {
            System.out.println("Something bad happened with I/O");
        } else {
            System.out.println("Something bad happened");
        }
    }
);

Processors

高级特性与概念

打包重用操作符

使用 transform 操作符

transform 操作符可以将一段操作链封装为一个函数式(function)。这个函数式能在操作期(assembly time) 将被封装的操作链中的操作符还原并接入到调用 transform 的位置。这样做和直接将被封装的操作符 加入到链上的效果是一样的。示例如下:

Function<Flux<String>, Flux<String>> filterAndMap =
f -> f.filter(color -> !color.equals("orange"))
      .map(String::toUpperCase);

Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple"))
        .doOnNext(System.out::println)
        .transform(filterAndMap)
        .subscribe(d -> System.out.println("Subscriber to Transformed MapAndFilter: "+d));
// blue
// Subscriber to Transformed MapAndFilter: BLUE
// green
// Subscriber to Transformed MapAndFilter: GREEN
// orange
// purple
// Subscriber to Transformed MapAndFilter: PURPLE

在这里插入图片描述

使用 transformDeferred 操作符

transformDeferred 操作符与 transform 类似,也能够将几个操作符封装到一个函数式中。 主要的区别就是,这个函数式作用到原始序列上的话,是 基于每一个订阅者的(on a per-subscriber basis) 。这意味着它对每一个 subscription 可以生成不同的操作链(通过维护一些状态值)。 如下例所示:

AtomicInteger ai = new AtomicInteger();
Function<Flux<String>, Flux<String>> filterAndMap = f -> {
	if (ai.incrementAndGet() == 1) {
return f.filter(color -> !color.equals("orange"))
        .map(String::toUpperCase);
	}
	return f.filter(color -> !color.equals("purple"))
	        .map(String::toUpperCase);
};

Flux<String> composedFlux =
Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple"))
    .doOnNext(System.out::println)
    .transformDeferred(filterAndMap);

composedFlux.subscribe(d -> System.out.println("Subscriber 1 to Composed MapAndFilter :"+d));
composedFlux.subscribe(d -> System.out.println("Subscriber 2 to Composed MapAndFilter: "+d));

在这里插入图片描述

输出如下:

blue
Subscriber 1 to Composed MapAndFilter :BLUE
green
Subscriber 1 to Composed MapAndFilter :GREEN
orange
purple
Subscriber 1 to Composed MapAndFilter :PURPLE
blue
Subscriber 2 to Composed MapAndFilter: BLUE
green
Subscriber 2 to Composed MapAndFilter: GREEN
orange
Subscriber 2 to Composed MapAndFilter: ORANGE
purple

Hot vs Cold

广义上有两种发布者:“热”与“冷”(hot and cold)。
本文档)到目前介绍的其实都是 cold 家族的发布者。它们为每一个订阅(subscription) 都生成数据。如果没有创建任何订阅(subscription),那么就不会生成数据。
热 发布者,不依赖于订阅者的数量。即使没有订阅者它们也会发出数据, 如果有一个订阅者接入进来,那么它就会收到订阅之后发出的元素。对于热发布者, 在你订阅它之前,确实已经发生了什么。
just 是 Reactor 中为数不多的“热”操作符的例子之一:它直接在组装期(assembly time) 就拿到数据,如果之后有谁订阅它,就重新发送数据给订阅者。如果想将 just 转化为一种 冷 的发布者,你可以使用 defer。
如下是一个冷发布者例子:

Flux<String> source = Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple"))
                          .doOnNext(System.out::println)
                          .filter(s -> s.startsWith("o"))
                          .map(String::toUpperCase);

source.subscribe(d -> System.out.println("Subscriber 1: "+d));
source.subscribe(d -> System.out.println("Subscriber 2: "+d));

输出如下:

blue
green
orange
Subscriber 1: ORANGE
purple
blue
green
orange
Subscriber 2: ORANGE
purple

在这里插入图片描述
两个订阅者都触发了所有的颜色,因为每一个订阅者都会让构造 Flux 的操作符运行一次。
如下是一个热发布者例子:

Sinks.Many<String> hotSource = Sinks.unsafe().many().multicast().directBestEffort();

Flux<String> hotFlux = hotSource.asFlux().map(String::toUpperCase);

hotFlux.subscribe(d -> System.out.println("Subscriber 1 to Hot Source: "+d));

hotSource.emitNext("blue", FAIL_FAST); 
hotSource.tryEmitNext("green").orThrow(); 

hotFlux.subscribe(d -> System.out.println("Subscriber 2 to Hot Source: "+d));

hotSource.emitNext("orange", FAIL_FAST);
hotSource.emitNext("purple", FAIL_FAST);
hotSource.emitComplete(FAIL_FAST);
Subscriber 1 to Hot Source: BLUE
Subscriber 1 to Hot Source: GREEN
Subscriber 1 to Hot Source: ORANGE
Subscriber 2 to Hot Source: ORANGE
Subscriber 1 to Hot Source: PURPLE
Subscriber 2 to Hot Source: PURPLE

第一个订阅者收到了所有的四个颜色,第二个订阅者由于是在前两个颜色发出之后订阅的, 故而收到了之后的两个颜色,在输出中有两次 “ORANGE” 和 “PURPLE”。从这个例子可见, 无论是否有订阅者接入进来,这个 Flux 都会运行。在这里插入图片描述

使用 ConnectableFlux 对多个订阅者进行广播

有时候,你不仅想要延迟到某一个订阅者订阅之后才开始发出数据,可能还希望在多个订阅者 到齐 之后 才开始。
ConnectableFlux 的用意便在于此。Flux API 中有两种主要的返回 ConnectableFlux 的方式:publish 和 replay。

  • publish 会尝试满足各个不同订阅者的需求(背压),并综合这些请求反馈给源。 尤其是如果有某个订阅者的需求为 0,publish 会 暂停 它对源的请求。(优点:同时发送给订阅者,缺点:处理速度受其他消费者影响)
  • replay 将对第一个订阅后产生的数据进行缓存,最多缓存数量取决于配置(时间/缓存大小)。 它会对后续接入的订阅者重新发送数据。(优点:利用缓存,先来先发,处理速度不受其他消费者影响,缺点:发送时间不一致)

ConnectableFlux 提供了多种对下游订阅的管理。包括:

  • connect 当有足够的订阅接入后,可以对 flux 手动执行一次。它会触发对上游源的订阅。 (手动连接)
  • autoConnect(n) 与 connect 类似,不过是在有 n 个订阅的时候自动触发。 (达到订阅数自动连接)
  • refCount(n) 不仅能够在订阅者接入的时候自动触发,还会检测订阅者的取消动作。如果订阅者数量不够, 会将源“断开连接”,再有新的订阅者接入的时候才会继续“连上”源。(达到订阅数自动连接,取消后订阅数减少立即断开)
  • refCount(int, Duration) 增加了一个 “优雅的倒计时”:一旦订阅者数量太低了,它会等待 Duration 的时间,如果没有新的订阅者接入才会与源“断开连接”。(达到订阅数自动连接;取消后在Duration时间内若没增加订阅者则断开连接)
Flux<Integer> source = Flux.range(1, 3)
                           .doOnSubscribe(s -> System.out.println("subscribed to source"));
// 获取ConnectableFlux 
ConnectableFlux<Integer> co = source.publish();
// 在ConnectableFlux 上订阅 (这里不会立即发布)
co.subscribe(System.out::println, e -> {}, () -> {});
co.subscribe(System.out::println, e -> {}, () -> {});

System.out.println("done subscribing");
Thread.sleep(500);
System.out.println("will now connect");
// 执行connect, 开始发布
co.connect();
done subscribing
will now connect
subscribed to source
1
1
2
2
3
3

使用 autoConnect:

Flux<Integer> source = Flux.range(1, 3)
                           .doOnSubscribe(s -> System.out.println("subscribed to source"));
// 获取ConnectableFlux 并设置订阅数为2时自动发布
Flux<Integer> autoCo = source.publish().autoConnect(2);

autoCo.subscribe(System.out::println, e -> {}, () -> {});
System.out.println("subscribed first");
Thread.sleep(500);
System.out.println("subscribing second");
autoCo.subscribe(System.out::println, e -> {}, () -> {});
done subscribing
will now connect
subscribed to source
1
1
2
2
3
3

三种分批处理方式

用 Flux<GroupedFlux> 进行分组

分组能够根据 key 将源 Flux 拆分为多个批次。对应的操作符是 groupBy。每一组用 GroupedFlux 类型表示,使用它的 key() 方法可以得到该组的 key。
在组内,元素并不需要是连续的。当源发出一个新的元素,该元素会被分发到与之匹配的 key 所对应的组中(如果还没有该 key 对应的组,则创建一个)。
这意味着组:

  1. 是互相没有交集的(一个元素只属于一个组)。
  2. 会包含原始序列中任意位置的元素。
  3. 不会为空。
StepVerifier.create(
        Flux.just(1, 3, 5, 2, 4, 6, 11, 12, 13)
        		// 分组
                .groupBy(i -> i % 2 == 0 ? "even" : "odd")
                // concatMap 会将返回的Publisher按顺序连接为一个Flux
                .concatMap(g -> g.defaultIfEmpty(-1) //如果组为空,显示为 -1
                                .map(String::valueOf) //转换为字符串
                                .startWith(g.key())) //以该组的 key 开头
        )
        .expectNext("odd", "1", "3", "5", "11", "13")
        .expectNext("even", "2", "4", "6", "12")
        .verifyComplete();

使用 Flux<Flux> 进行 window 操作

window 可以把源 Flux 拆分为 windows。
对应的操作符有 window、windowTimeout、windowUntil、windowWhile,以及 windowWhen。
与 groupBy 的主要区别在于,窗口操作能够保持序列顺序。并且同一时刻最多只能有两个 window 是开启的。
它们 可以 重叠。操作符参数有 maxSize 和 skip,maxSize 指定收集多少个元素就关闭 window,而 skip 指定收集多数个元素后就打开下一个 window。所以如果 maxSize > skip 的话, 一个新的 window 的开启会先于当前 window 的关闭, 从而二者会有重叠。

StepVerifier.create(
        Flux.range(1, 10)
                .window(5, 3) //overlapping windows
                .concatMap(g -> g.defaultIfEmpty(-1)) //将 windows 显示为 -1
        )
                .expectNext(1, 2, 3, 4, 5)
                .expectNext(4, 5, 6, 7, 8)
                .expectNext(7, 8, 9, 10)
                .expectNext(10)
                .verifyComplete();

如果将两个参数的配置反过来(maxSize < skip),序列中的一些元素就会被丢弃掉, 而不属于任何 window。

使用 Flux<List> 进行缓存

缓存与窗口类似,不同在于:缓存操作之后会发出 buffers (类型为Collection<T>, 默认是List),而不是 windows (类型为 Flux)。
缓存的操作符与窗口的操作符是对应的:buffer、bufferTimeout、bufferUntil、bufferWhile, 以及bufferWhen

全局Hooks

丢弃事件的 Hooks

当生成源的操作符不遵从响应式流规范的时候,Dropping hooks(用于处理丢弃事件的 hooks)会被调用。 这种类型的错误是处于正常的执行路径之外的(也就是说它们不能通过 onError 传播)。

典型的例子是,假设一个发布者即使在被调用 onCompleted 之后仍然可以通过操作符调用 onNext。 这种情况下,onNext 的值会被 丢弃,如果有多余的 onError 的信号亦是如此。

相应的 hook,onNextDropped 以及 onErrorDropped,可以提供一个全局的 Consumer, 以便能够在丢弃的情况发生时进行处理。例如,你可以使用它来对丢弃事件记录日志,或进行资源清理 (使用资源的值可能压根没有到达响应式链的下游)。

内部错误Hook

如果操作符在执行其 onNext、onError 以及 onComplete 方法的时候抛出异常,那么 onOperatorError 这一个 hook 会被调用。

与上一类 hook 不同,这个 hook 还是处在正常的执行路径中的。一个典型的例子就是包含一个 map 函数式的 map 操作符抛出的异常(比如零作为除数),这时候还是会执行到 onError 的。

首先,它会将异常传递给 onOperatorError。利用这个 hook 你可以检查这个错误(以及有问题的相关数据), 并可以 改变 这个异常。当然你还可以做些别的事情,比如记录日志或返回原始异常。

Hooks.onOperatorError((throwable, o) -> {
            System.out.println(throwable);
            System.out.println(o);
            return throwable;
        });
        Flux.just(1).map(i -> {
            throw new IllegalArgumentException();
        }).blockLast();
// java.lang.IllegalArgumentException
// 1

组装 Hooks

这些组装(assembly) hooks 关联了操作符的生命周期。它们会在一个操作链被组装起来的时候(即实例化的时候) 被调用。每一个新的操作符组装到操作链上的时候, onEachOperator 都会返回一个不同的发布者, 从而可以利用它动态调整操作符。
onLastOperator 与之类似,不过只会在被操作链上的最后一个 (subscribe 调用之前的)操作符调用。

预置 Hooks

Hooks 工具类还提供了一些预置的 hooks。利用他们可以改变一些默认的处理方式,而不用自己 编写 hook:

  • onNextDroppedFail():onNextDropped 通常会抛出 Exceptions.failWithCancel() 异常。 现在它默认还会以 DEBUG 级别对被丢弃的值记录日志。如果想回到原来的只是抛出异常的方式,使用 onNextDroppedFail()。

  • onOperatorDebug(): 这个方法会激活 debug mode。它与 onOperatorError hook 关联,所以调用 resetOnOperatorError() 同时也会重置它。不过它内部也用到了特别的识别符, 你可以通过 resetOnOperatorDebug() 方法来重置它。

ThreadLocal的替代方案 Context

自从版本 3.1.0,Reactor 引入了一个类似于 ThreadLocal 的高级功能:Context。它作用于一个 Flux 或一个 Mono 上,而不是应用于一个线程(Thread)。
为了说明,这里有个读写 Context 的简单例子:

String key = "message";
Mono<String> r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                                   .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "World"));

StepVerifier.create(r)
            .expectNext("Hello World")
            .verifyComplete();

Context API

Context 是一个类似于 Map(这种数据结构)的接口:它存储键值(key-value)对,你需要通过 key 来获取值:

  • key 和 value 都是 Object 类型,所以 Context 可以包含任意数量的任意对象。
  • Context 是 不可变的(immutable)。
  • 用 put(Object key, Object value) 方法来存储一个键值对,返回一个新的 Context 对象。 你也可以用 putAll(Context) 方法将两个 - context 合并为一个新的 context。
  • 用 hasKey(Object key) 方法检查一个 key 是否已经存在。
  • 用 getOrDefault(Object key, T defaultValue) 方法取回 key 对应的值(类型转换为 T), 或在找不到这个 key 的情况下返回一个默认值。
  • 用 getOrEmpty(Object key) 来得到一个 Optional (context 会尝试将值转换为 T)。
  • 用 delete(Object key) 来删除 key 关联的值,并返回一个新的 Context。

创建一个 Context 时,你可以用静态方法 Context.of 预先存储最多 5 个键值对。 它接受 2, 4, 6, 8 或 10 个 Object 对象,两两一对作为键值对添加到 Context。

你也可以用 Context.empty() 方法来创建一个空的 Context。

把 Context 绑定到 Flux and Writing

了使用 context,它必须要绑定到一个指定的序列,并且链上的每个操作符都可以访问它。 注意,这里的操作符必须是 Reactor 内置的操作符,因为 Context 是 Reactor 特有的。

实际上,一个 Context 是绑定到每一个链中的 Subscriber 上的。 它使用 Subscription 的传播机制来让自己对每一个操作符都可见(从最后一个 subscribe 沿链向上)。

为了填充 Context ——只能在订阅时(subscription time)——你需要使用 contextWrite 操作符。

contextWrite(Context) 方法会将你提供的 Context 与来自下游(记住,Context 是从下游 向上游传播的)的 Context合并。 这通过调用 putAll 实现,最后会生成一个新的 Context 给上游。

你也可以用更高级的 contextWrite(Function<Context, Context>)。它接受来自下游的 Context,然后你可以根据需要添加或删除值,然后返回新的 Context。你甚至可以返回一个完全不同 的对象,虽然不太建议这样(这样可能影响到依赖这个 Context 的库)。

读取 Context

读取 context 数据使用静态方法 Mono.deferContextual()。

String key = "message";
Mono<String> r = Mono.just("Hello")
	 // 读取context
    .flatMap(s -> Mono.deferContextual(ctx ->
         Mono.just(s + " " + ctx.get(key)))) 
     // 写入context
    .contextWrite(ctx -> ctx.put(key, "World")); 

StepVerifier.create(r)
            .expectNext("Hello World") 
            .verifyComplete();

自上而下
由于这个例子的subscriberContext设置的太高了,不能作用在flatMap里头的Mono.subscriberContext()

String key = "message";
Mono<String> r = Mono.just("Hello")
    .contextWrite(ctx -> ctx.put(key, "World")) 
    .flatMap( s -> Mono.deferContextual(ctx ->
        Mono.just(s + " " + ctx.getOrDefault(key, "Stranger")))); 

StepVerifier.create(r)
            .expectNext("Hello Stranger") 
            .verifyComplete();

多个连续的contextWrite 只会读取下游最近一个

String key = "message";
Mono<String> r = Mono
    .deferContextual(ctx -> Mono.just("Hello " + ctx.get(key)))
    .contextWrite(ctx -> ctx.put(key, "Reactor")) 
    .contextWrite(ctx -> ctx.put(key, "World")); 

StepVerifier.create(r)
            .expectNext("Hello Reactor") 
            .verifyComplete();

多个不连续的contextWrite 只会读取下游最近一个

String key = "message";
Mono<String> r = Mono
	// Hello Reactor
    .deferContextual(ctx -> Mono.just("Hello " + ctx.get(key))) 
    .contextWrite(ctx -> ctx.put(key, "Reactor")) 
    // Hello Reactor World
    .flatMap( s -> Mono.deferContextual(ctx ->
        Mono.just(s + " " + ctx.get(key)))) 
    .contextWrite(ctx -> ctx.put(key, "World")); 

StepVerifier.create(r)
            .expectNext("Hello Reactor World") 
            .verifyComplete();

flatMap无法读取第二个flatMap内部的context

String key = "message";
Mono<String> r = Mono.just("Hello")
	// Hello World
    .flatMap( s -> Mono
        .deferContextual(ctxView -> Mono.just(s + " " + ctxView.get(key)))
    )
    .flatMap( s -> Mono
    	// Hello World Reactor
        .deferContextual(ctxView -> Mono.just(s + " " + ctxView.get(key)))
        .contextWrite(ctx -> ctx.put(key, "Reactor")) 
    )
    .contextWrite(ctx -> ctx.put(key, "World")); 

StepVerifier.create(r)
            .expectNext("Hello World Reactor")
            .verifyComplete();

注意

Flux.just("Hello")
    .doOnNext(s -> {
        Mono.deferContextual(ctx -> Mono.just(ctx.get(key))).subscribe(System.out::println);
    })
    .contextWrite(ctx -> ctx.put(key, "World"))
    .subscribe(System.out::println);

上面这段代码尝试在doOnNext中使用Mono.deferContextual获取context中的内容,这里会抛出一个异常java.util.NoSuchElementException: Context is empty ,原因是这里的上下文只存在于操作符内部,而无法获取到外部Flux中context的内容。

flatMap 通过将lambad返回的Mono.deferContextual和外部的Plblisher合并,从而使得Mono.deferContextual可以获取到外部的context。如下例所示:

Flux.just("Hello")
    .flatMap(s -> Mono.deferContextual(ctx ->
            Mono.just(ctx.get(key))))
    .contextWrite(ctx -> ctx.put(key, "World"))
    .subscribe(System.out::println);

操作符查询

我需要哪个操作符?
Which operator do I need?

替代CompletableFuture.anyOf

Mono.firstWithSignal(
    Mono.just("World").delayElement(Duration.ofMillis(3000)).doOnNext { println("World doOnNext") },
    Mono.just("Hello").delayElement(Duration.ofMillis(1000)).doOnNext { println("Hello doOnNext") })
    .subscribe { println(it) }
// Hello doOnNext
// Hello

替代CompletableFuture.allOf

val block = Mono.zip(
    Mono.just("World").delayElement(Duration.ofMillis(3000)).doOnNext { println("World doOnNext") },
    Mono.just("Hello").delayElement(Duration.ofMillis(1000)).doOnNext { println("Hello doOnNext") })
   .block()
println(block)
// Hello doOnNext
// World doOnNext
// [World,Hello]

参考

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值