WebFlux学习总结


前言

我们知道传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的,在Servlet3.1之后才有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上,因此它的运行环境的可选择行要比传统web框架多的多。
如果现在的场景是服务器为多种设备提供连接,需要许多线程,且每个线程处理的时间可能会很长,怎么办?
事件驱动


一、什么是Reactive编程?

官方解释是这样的:

refers to programming models that are built around reacting to change

意思是:Reactive,就是一种围绕对变化做出反应而构建的编程模型。

如网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应等等。从这个意义上说,非阻塞是反应性的,因为我们现在不是被阻塞,而是在操作完成或数据可用时对通知做出反应。

反应式还有一个很重要的机制,那就是“ non-blocking back pressure” (背压),

二、反应式API

Reactor是 Spring WebFlux 的首选响应式库。它 通过与 ReactiveX运算符词汇表对齐的一组丰富的运算符,提供了 0…1( ) 和 0…N( ) 的数据序列Mono和 FluxAPI 类型。Reactor 是一个 Reactive Streams 库,因此,它的所有操作符都支持非阻塞背压。

Mono

一个Mono对象代表一个包含零/一个(0…1)元素
在这里插入图片描述

Mono是一个专门的,它通过onNext发出信号,然后用onComplete信号终止(成功的Mono,有或没有值),或者只发出一个onError信号(失败的Mono)。
大多数实现都应该在调用后Mono立即调用onComplete 。是一个异常值:它不发出任何信号,这在技术上并没有被禁止,尽管在测试之外并不是非常有用。另一方面,明确禁止and的组合。

Mono仅提供可用于的运算符的子集Flux,并且一些运算符(特别是那些将 theMono与另一个结合的运算符Publisher)切换到Flux。例如,Mono#concatWith(Publisher)返回一个FluxwhileMono#then(Mono) 返回另一个Mono。

请注意,您可以使用 Mono来表示只有完成概念的无值异步进程(类似于 a Runnable)。要创建一个,您可以使用一个空的 Mono.

Flux

一个Flux对象代表一个包含0…N个元素的响应式序列
在这里插入图片描述

Flux<T>是一个标准Publisher<T>,表示 0 到 N 个的异步序列,可选地由完成信号或错误终止。正如在 Reactive Streams 规范中一样,onNext、onComplete和onError这三种类型的信号转换为对下游订阅者的方法的调用

思考:既然Flux具有发布一个数据元素的能力,为什么还要专门定义一个Mono类呢?

举个例子,一个HTTP请求产生一个响应,所以对其进行“count”操作是没有多大意义的。表示这样一个结果的话,应该用Mono而不是 Flux,对于的操作通常只用于处理 0/1 个元素。它们从语义上就原生包含着元素个数的信息,从而避免了对Mono对象进行多元素场景下的处理。

当然,有些操作可以改变基数,从而需要切换类型。比如,count操作用于Flux,但是操作返回的结果是Mono。

创建数据流

Flux和Mono提供了多种创建数据流的方法。

// just就是一种比较直接的声明数据流的方式,参数就是需要传入的数据元素
Flux.just(1, 2, 3, 4, 5, 6);
Mono.just(1);

// 还可以通过这三种方式声明
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.just();
Flux.empty();
Mono.empty();
Mono.justOrEmpty(Optional.empty());
// 只有错误信号的数据流
Flux.error(new Exception("some error"));
Mono.error(new Exception("some error"));

订阅

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、map -元素映射为新元素

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

在这里插入图片描述

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

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

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

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

2、flatMap -元素映射为流

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

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();

1、对于每一个字符串s,将其拆分为包含一个字符的字符串流;
2、对每个元素延迟100ms;
3、对每个元素进行打印(注doOnNext方法是“偷窥式”的方法,不会消费数据流);
4、验证是否发出了8个元素。

打印结果为mfolnuox,原因在于各个拆分后的小字符串都是间隔100ms发出的,因此会交叉。

3、filter -过滤

和stream中的filter很相似,这个函数式的作用就是根据参数中定义好的Boolean表达式,判断是否为true,不为true则被过滤。

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

4、zip -一对一合并

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

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) 

除了zip静态方法之外,还有zipWith等非静态方法,效果与之类似:

getZipDescFlux().zipWith(Flux.interval(Duration.ofMillis(200)))

在异步条件下,数据流的流速不同,使用zip能够一对一地将两个或多个数据流的元素对齐发出。

5、publishOn,subscribeOn

publishOnsubscribeOn。这两个方法的作用是指定执行 Reactive StreamingScheduler(可理解为线程池), 不过这两个方法是用的场景是不相同的.

  • Scheduler
    在 Reactor 中,Scheduler 用来定义执行调度任务的抽象。可以简单理解为线程池,但其实际作用要更多。Scheduler 的实现包括:
    1、Schedulers.elastic(): 调度器会动态创建工作线程,线程数无上界
    
    2、Execturos.newCachedThreadPool()
    
    3、当前线程,通过 Schedulers.immediate()方法来创建。
    
    4、单一的可复用的线程,通过 Schedulers.single()方法来创建。
    
    5、使用对并行操作优化的线程池,通过 Schedulers.parallel()方法来创建。其中的线程数量取决于 CPU 的核的数量。该调度器适用于计算密集型的流的处理。
    
    6、使用支持任务调度的调度器,通过 Schedulers.timer()方法来创建。
    从已有的 ExecutorService 对象中创建调度器,通过 Schedulers.fromExecutorService()方法来创建。
    	
    

这里举一个例子:

Mono<Void> fluxToBlockingRepository(Flux<User> flux, 
                                    BlockingRepository<User> repository) {
    return flux
            .publishOn(Schedulers.elastic())
            .doOnNext(repository::save)
            .then();
}

Flux<User> blockingRepositoryToFlux(BlockingRepository<User> repository)  {
    return Flux.defer(() -> Flux.fromIterable(repository.findAll()))
               .subscribeOn(Schedulers.elastic());
}

这里的 repository 的类型是BlockingRepository,通常指的是会导致线程阻塞的IO操作

在第一个例子中,在执行了publishOn(Schedulers.elastic()) 之后,repository::save就会被 Schedulers.elastic()定义的线程池所执行。

而在第二个例子中,subscribeOn(Schedulers.elastic()) 的作用类似。它使得 repository.findAll()(也包括 Flux.fromIterable)的执行发生在 Schedulers.elastic()所定义的线程池中。

  • subscribeOn和publishOn的区别

    简单说,两者的区别在于影响范围。publishOn 影响在其之后的 operator执行的线程池,而 subscribeOn 则会从源头影响整个执行过程。所以,publishOn 的影响范围和它的位置有关,而 subscribeOn的影响范围则和位置无关。

测试

在响应式的异步代码中测试,会比以往的命令式和同步式编程的测试复杂。
在这,会用到一个基本的测试工具:StepVerifier

在这里插入图片描述

private Flux<Integer> generateFluxFrom1To6() {
    return Flux.just(1, 2, 3, 4, 5, 6);
}
private Mono<Integer> generateMonoWithError() {
    return Mono.error(new Exception("some error"));
}
@Test
public void testViaStepVerifier() {
	//create()传入待测试流数据
    StepVerifier.create(generateFluxFrom1To6())
            .expectNext(1, 2, 3, 4, 5, 6) //测试是否期望的数据元素
            .expectComplete() //测试是否是完成信号
            .verify(); //进行核实
    StepVerifier.create(generateMonoWithError())
            .expectErrorMessage("some error") //校验是否为错误信号
            .verify();
}

调度器与线程模型

Schedulers类已经预先创建了几种常用的线程池:使用single()、elastic()和parallel()方法可以分别使用内置的单线程、弹性线程池和固定大小线程池。如果想创建新的线程池,可以使用newSingle()、newElastic()和newParallel()方法。

Executors提供的几种线程池在Reactor中都支持:

Schedulers.single()和Schedulers.newSingle()对应Executors.newSingleThreadExecutor();
Schedulers.elastic()和Schedulers.newElastic()对应Executors.newCachedThreadPool();
Schedulers.parallel()和Schedulers.newParallel()对应Executors.newFixedThreadPool()

Schedulers提供的以上三种调度器底层都是基于ScheduledExecutorService的,因此都是支持任务定时和周期性执行的;
Flux和Mono的调度操作符subscribeOn和publishOn支持work-stealing。

错误处理

在响应式流中,错误(error)是终止信号。当有错误发生时,它会导致流序列停止。
所以我们可以再subscribe中对错误进行处理。

@Test
public void testErrorHandling() {
    Flux.range(1, 6)
            .map(i -> 10/(i-3)) // 1
            .map(i -> i*i)
            .subscribe(System.out::println, System.err::println);
}

除了再subscribe中进行错误处理,Reactor 中还提供了不少处理方式:

  • 捕获并返回一个静态缺省值
    Flux.range(1, 6)
    .map(i -> 10/(i-3))
    .onErrorReturn(0)   // 1
    .map(i -> i*i)
    .subscribe(System.out::println, System.err::println);
    
    
  • 捕获并执行一个异常处理方法或计算一个候补值来顶替
    Flux.range(1, 6)
    .map(i -> 10/(i-3))
    .onErrorResume(e -> Mono.just(new Random().nextInt(6))) // 提供新的数据流
    .map(i -> i*i)
    .subscribe(System.out::println, System.err::println);
    
    
  • 捕获,并再包装为某一个业务相关的异常,然后再抛出业务异常
    Flux.just("timeout1")
    .flatMap(k -> callExternalService(k))   // 1
    .onErrorMap(original -> new BusinessException("SLA exceeded", original)); // 2
    
    
  • 捕获,记录错误日志,然后继续抛出
    Flux.just(endpoint1, endpoint2)
    .flatMap(k -> callExternalService(k)) 
    .doOnError(e -> {   // 1
        log("uh oh, falling back, service failed for key " + k);    // 2
    })
    .onErrorResume(e -> getFromCache(k)); 
    
    
  • 使用 finally 来清理资源,或使用 Java 7 引入的 “try-with-resource”
    Flux.using(
        () -> getResource(),    // 1
        resource -> Flux.just(resource.getAll()),   // 2
        MyResource::clean   // 3
    );
    
  • 重试
    Flux.range(1, 6)
    .map(i -> 10 / (3 - i))
    .retry(1)
    .subscribe(System.out::println, System.err::println);
    Thread.sleep(100);  // 确保序列执行完
    

回压

回压,简单地说就是流量控制,也就是当执行.subscribe()的时候,直接发起了一个无限的请求,就是对于数据流中的元素无论快慢都照单全收。

正确的接受姿势,当然是这样:

//自定义具有流量控制能力的Subscriber进行订阅
subscribe(Subscriber subscriber)

假设,我们现在有一个非常快的Publisher——Flux.range(1, 6),然后自定义一个每秒处理一个数据元素的慢的Subscriber,Subscriber就需要通过request(n)的方法来告知上游它的需求速度。

@Test
public void testBackpressure() {
    Flux.range(1, 6)    // Flux.range是一个快的Publisher;
            .doOnRequest(n -> System.out.println("Request " + n + " values..."))    // 在每次request的时候打印request个数;
            .subscribe(new BaseSubscriber<Integer>() {  // 通过重写BaseSubscriber的方法来自定义Subscriber;

                @Override
                protected void hookOnSubscribe(Subscription subscription) { // hookOnSubscribe定义在订阅的时候执行的操作;

                    System.out.println("Subscribed and make a request...");
                    request(1); // 订阅时首先向上游请求1个元素;

                }

                @Override
                protected void hookOnNext(Integer value) {  // hookOnNext定义每次在收到一个元素的时候的操作;

                    try {
                        TimeUnit.SECONDS.sleep(1);  // sleep 1秒钟来模拟慢的Subscriber;

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Get value [" + value + "]");    // 打印收到的元素;

                    request(1); // 打印收到的元素;

                }
            });
}

最后,可以实现每秒处理一个元素,由此可见range方法生成的Flux采用的是缓存的回压策略,能够缓存下游暂时来不及处理的元素。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值