webflux

本文介绍了Spring WebFlux中的核心概念,包括如何创建数据流,理解完成和错误信号,以及订阅前的行为。重点讨论了Flux和Mono的操作符,如map、flatMap、filter和zip,同时也涉及了错误处理和线程调度。通过实例展示了如何在WebFlux中使用这些操作符处理数据流。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

webflux

  • 官方文档
    https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#zipWith-org.reactivestreams.Publisher-

  • 写的比较好的文章
    https://cloud.tencent.com/developer/article/1526029

介绍

Reactor中的发布者(Publisher)由Flux和Mono两个类定义,它们都提供了丰富的操作符(operator)。一个Flux对象代表一个包含0…N个元素的响应式序列,而一个Mono对象代表一个包含零/一个(0…1)元素的结果。

一、 创建数据流

Flux.just(1, 2, 3, 4, 5, 6);
Mono.just(1);

Flux和Mono提供了多种创建数据流的方法,just就是一种比较直接的声明数据流的方式,其参数就是数据元素。

对于图中的Flux,还可以通过如下方式声明(数组、集合、Stream、range):

Integer[] array = new Integer[]{1,2,3,4,5,6};
Flux.fromArray(array);
List<Integer> list = Arrays.asList(array);
Flux.fromIterable(list);
Stream<Integer> stream = list.stream();
Flux.fromStream(stream);
Flux<Integer> rangeFlux =Flux.range(0, 10000);

二、 完成信号和错误信号

既然是“数据流”的发布者,Flux和Mono都可以发出三种“数据信号”:元素值、错误信号、完成信号,错误信号和完成信号都是终止信号,完成信号用于告知下游订阅者该数据流正常结束,错误信号终止数据流的同时将错误传递给下游订阅者。

不过,这三种信号都不是一定要具备的:

首先,错误信号和完成信号都是终止信号,二者不可能同时共存;
如果没有发出任何一个元素值,而是直接发出完成/错误信号,表示这是一个空数据流;
如果没有错误信号和完成信号,那么就是一个无限数据流。
比如,对于只有完成/错误信号的数据流:

// 只有完成信号的空数据流
Flux.just();
Flux.empty();
Mono.empty();
Mono.justOrEmpty(Optional.empty());
// 只有错误信号的数据流
Flux.error(new Exception("some error"));
Mono.error(new Exception("some error"));

三、订阅前什么都不会发生

数据流有了,假设我们想把每个数据元素原封不动地打印出来:

Flux.just(1, 2, 3, 4, 5, 6).subscribe(System.out::print);
System.out.println();
Mono.just(1).subscribe(System.out::println);

输出如下:
123456
1

可见,subscribe方法中的lambda表达式作用在了每一个数据元素上。此外,Flux和Mono还提供了多个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(Consumer<? super T> consumer,
          Consumer<? super Throwable> errorConsumer,
          Runnable completeConsumer,
          Consumer<? super Subscription> subscriptionConsumer); 

1)如果是订阅上边声明的Flux:

Flux.just(1, 2, 3, 4, 5, 6).subscribe(
    System.out::println,
    System.err::println,
    () -> System.out.println("Completed!"));

输出如下:

1
2
3
4
5
6
Completed!

2)再举一个有错误信号的例子:

Mono.error(new Exception("some error")).subscribe(
        System.out::println,
        System.err::println,
        () -> System.out.println("Completed!")
);
输出如下:

java.lang.Exception: some error

打印出了错误信号,没有输出Completed!表明没有发出完成信号。
这里需要注意的一点是,Flux.just(1, 2, 3, 4, 5, 6)仅仅声明了这个数据流,此时数据元素并未发出,只有subscribe()方法调用的时候才会触发数据流。所以,订阅前什么都不会发生。

webflux中的subscribe

在webFlux框架中 我们都是返回mono或flux类型的result, 不需要主动调用subscribe方法,

四、操作符

1. map - 元素映射为新元素

map操作可以将数据元素进行转换/映射,得到一个新元素。

public final <V> Flux<V> map(Function<? super T,? extends V> mapper)
public final <R> Mono<R> map(Function<? super T, ? extends R> mapper) 

上图是Flux的map操作示意图,上方的箭头是原始序列的时间轴,下方的箭头是经过map处理后的数据序列时间轴。

map接受一个Function的函数式接口为参数,这个函数式的作用是定义转换操作的策略。举例说明:

StepVerifier.create(Flux.range(1, 6)    
            .map(i -> i * i))   
            .expectNext(1, 4, 9, 16, 25, 36)    
            .expectComplete();  

Flux.range(1, 6)用于生成从“1”开始的,自增为1的“6”个整型数据;
map接受lambda i -> i * i为参数,表示对每个数据进行平方;
验证新的序列的数据;
verifyComplete()相当于expectComplete().verify()。

2、flatMap - 元素映射为流

flatMap操作可以将每个数据元素转换/映射为一个流,然后将这些流合并为一个大的数据流。

flatMap

注意到,流的合并是异步的,先来先到,并非是严格按照原始序列的顺序(如图蓝色和红色方块是交叉的)。

public final <R> Flux<R> flatMap(Function<? super T, ? extends Publisher<? extends R>> mapper)
public final <R> Mono<R> flatMap(Function<? super T, ? extends Mono<? extends R>> transformer) 

flatMap也是接收一个Function的函数式接口为参数,这个函数式的输入为一个T类型数据值,对于Flux来说输出可以是Flux和Mono,对于Mono来说输出只能是Mono。举例说明:

    StepVerifier.create(
        Flux.just("flux", "mono")
                .flatMap(s -> Flux.fromArray(s.split("\\s*"))   // 1
                        .delayElements(Duration.ofMillis(100))) // 2
                .doOnNext(System.out::print)) // 3
        .expectNextCount(8) // 4
        .verifyComplete();

对于每一个字符串s,将其拆分为包含一个字符的字符串流;
对每个元素延迟100ms;
对每个元素进行打印(ps: doOnNext方法是“偷窥式”的方法,不会消费数据流);
验证是否发出了8个元素。
打印结果为mfolnuox,原因在于各个拆分后的小字符串都是间隔100ms发出的,因此会交叉。

flatMap通常用于每个元素又会引入数据流的情况,比如我们有一串url数据流,需要请求每个url并收集respon<se数据。假设响应式的请求方法如下:

Mono requestUrl(String url) {…}
而url数据流为一个Flux urlFlux,那么为了得到所有的HttpResponse,就需要用到flatMap:

urlFlux.flatMap(url -> requestUrl(url));
其返回内容为Flux类型的HttpResponse流。

3)filter - 过滤

filter操作可以对数据元素进行筛选。


public final Flux<T> filter(Predicate<? super T> tester)
public final Mono<T> filter(Predicate<? super T> tester) 

filter接受一个Predicate的函数式接口为参数,这个函数式的作用是进行判断并返回boolean。举例说明:

StepVerifier.create(Flux.range(1, 6)
            .filter(i -> i % 2 == 1)    // 1
            .map(i -> i * i))
            .expectNext(1, 9, 25)   // 2
            .verifyComplete();

filter的lambda参数表示过滤操作将保留奇数;
验证仅得到奇数的平方。

4)zip - 一对一合并

看到zip这个词可能会联想到拉链,它能够将多个流一对一的合并起来。zip有多个方法变体,我们介绍一个最常见的二合一的。

ps:注意对多个流进行zip后,合并后的长度跟这些流中最短的那个长度是一致的。有可能会丢数据,这点要特别注意

zip它对两个Flux/Mono流每次各取一个元素,合并为一个二元组(Tuple2):

public static <T1,T2> Flux<Tuple2<T1,T2>> zip(Publisher<? extends T1> source1,
                                          Publisher<? extends T2> source2)
public static <T1, T2> Mono<Tuple2<T1, T2>> zip(Mono<? extends T1> p1, Mono<? extends T2> p2) 

Flux的zip方法接受Flux或Mono为参数,Mono的zip方法只能接受Mono类型的参数。

举个例子,假设我们有一个关于zip方法的说明:“Zip two sources together, that is to say wait for all the sources to emit one element and combine these elements once into a Tuple2.”,我们希望将这句话拆分为一个一个的单词并以每200ms一个的速度发出,除了前面flatMap的例子中用到的delayElements,可以如下操作:

private Flux<String> getZipDescFlux() {
    String desc = "Zip two sources together, that is to say wait for all the sources to emit one element and combine these elements once into a Tuple2.";
    return Flux.fromArray(desc.split("\\s+"));  // 1
}
 
@Test
public void testSimpleOperators() throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);  // 2
    Flux.zip(
            getZipDescFlux(),
            Flux.interval(Duration.ofMillis(200)))  // 3
            .subscribe(t -> System.out.println(t.getT1()), null, countDownLatch::countDown);    // 4
    countDownLatch.await(10, TimeUnit.SECONDS);     // 5
}

将英文说明用空格拆分为字符串流;
定义一个CountDownLatch,初始为1,则会等待执行1次countDown方法后结束,不使用它的话,测试方法所在的线程会直接返回而不会等待数据流发出完毕;
使用Flux.interval声明一个每200ms发出一个元素的long数据流;因为zip操作是一对一的,故而将其与字符串流zip之后,字符串流也将具有同样的速度;
zip之后的流中元素类型为Tuple2,使用getT1方法拿到字符串流的元素;定义完成信号的处理为countDown;
countDownLatch.await(10, TimeUnit.SECONDS)会等待countDown倒数至0,最多等待10秒钟。
除了zip静态方法之外,还有zipWith等非静态方法,效果与之类似:

getZipDescFlux().zipWith(Flux.interval(Duration.ofMillis(200)))
在异步条件下,数据流的流速不同,使用zip能够一对一地将两个或多个数据流的元素对齐发出。

5)错误处理

onErrorReturn 返回静态值

onErrorResume 可以动态的返回

public static final Function<Throwable, Mono<ServerResponse>> onErrorResume = throwable -> {
        log.warn("", throwable);
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(
                fromObject(Result.of(-1, "异常,", throwable.getMessage())));
    };

onErrorReturn 和 onErrorResume 有什么区别吗?

从源码上看它们的区别不大,只不过 onErrorReturn 的范围更大一些而已。onErrorReturn 返回一个静态值,onErrorResume 返回一个动态值,并且可以捕获,包装和重新抛出错误,例如作为自定义业务异常等
onErrorResume与onErrorReturn的唯一区别是可以编写异常处理函数来返回一个给定的缺省值 * 实际上, onErrorReturn正是通过调用onErrorResume来返回缺省值 */ StepVerifier.create(generateTextFluxError() // onErrorResume还有个实用的场景是: 当发生错误时, 根据异常类型来切换到另一个流 .onErrorResume(e -> generateTextFlux().filter(s -> !Strings.isNullOrEmpty(s))) .map(s -> Integer.valueOf(s)) .reduce((x, y) -> x + y)) .expectNext(12) .verifyComplete(); }

6) 线程调度

a) 没有执行上下文(Schedulers.immediate()): 在处理时,将直接执行提交的Runnable,从而在当前线程上有效地运行它们(可以视为“空对象”或无操作调度程序)。

b) 单个可重用线程(Schedulers.single())。请注意,此方法对所有调用方都使用相同的线程,直到调度程序被释放为止。如果您需要每次调用一个专用线程,请对每个调用使用Schedulers.newSingle()。

c) 无限制的弹性线程池(Schedulers.elastic())。随着Schedulers.boundedElastic()的引入,Schedulers.boundedElastic()不再是首选方法,因为它倾向于隐藏背压问题并导致线程过多(请参见下文)。

d) 有界弹性线程池(Schedulers.boundedElastic())。像其前身elastic()一样,它根据需要创建新的工作池并重用空闲的工作池。闲置时间过长(默认值为60s)的工作池也将被丢弃。与其前身的elastic()有所不同,它对可以创建的线程数进行限制(默认为CPU核心数x 10)。达到上限后,最多可再提交10万个任务,并在有线程可用时重新调度(当任务被设置延迟执行时,延迟计时是在线程可用时开始)。这是I/O阻塞任务的更好选择。Schedulers.boundedElastic()是一种为阻塞处理提供自己的线程的简便方法,这样它就不会占用其他资源。

e) 为并行工作而调整的固定工作线程池(Schedulers.parallel())。它创建的工作线程数量与CPU内核数量一样多。

7) Mono和Flux互相转换

 // flux转mono
        Flux<String> stringFlux = Flux.fromIterable(Lists.newArrayList("a", "b2", "cc3","d4"));
        Mono<List<String>> listMono = stringFlux.collectList();
        Mono<Map<Integer, String>> mapMono = stringFlux.collectMap(String::length, String::trim);

 //mono 转flux
        Flux<String> newFlux =listMono.flatMapMany(Flux::fromIterable);
        Flux<String> newFlux2 = mapMono.map(a->{return a.values();}).flatMapMany(Flux::fromIterable);

        stringFlux.subscribe(System.out::print);
        System.out.println("---------");
        listMono.subscribe(System.out::print);
        System.out.println("---------");
        mapMono.subscribe(System.out::print);
        System.out.println("---------");
        newFlux.subscribe(System.out::print);
        System.out.println("---------");
        newFlux2.subscribe(System.out::print);

7)更多

Reactor中提供了非常丰富的操作符,除了以上几个常见的,还有:

用于编程方式自定义生成数据流的create和generate等及其变体方法;
用于“无副作用的peek”场景的doOnNext、doOnError、doOncomplete、doOnSubscribe、doOnCancel等及其变体方法;
用于数据流转换的when、and/or、merge、concat、collect、count、repeat等及其变体方法;
用于过滤/拣选的take、first、last、sample、skip、limitRequest等及其变体方法;
用于错误处理的timeout、onErrorReturn、onErrorResume、doFinally、retryWhen等及其变体方法;
用于分批的window、buffer、group等及其变体方法;
用于线程调度的publishOn和subscribeOn方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值