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
publishOn
和 subscribeOn
。这两个方法的作用是指定执行 Reactive Streaming
的 Scheduler
(可理解为线程池), 不过这两个方法是用的场景是不相同的.
- 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采用的是缓存的回压策略,能够缓存下游暂时来不及处理的元素。