springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;
本文介绍reactor响应式编程风格。spring 5 响应式编程底层默认首选就是reactor ,所以很有必要学习一下。
Reactor 用于创建高效的响应式系统。Reactor 是一个用于JVM的完全非阻塞的响应式编程框架,具备高效的需求管理(即对 “背压(backpressure)”的控制)能力。它与 Java 8 函数式 API 直接集成,比如 CompletableFuture, Stream, 以及 Duration。它提供了异步序列 API Flux(用于[N]个元素)和 Mono(用于 [0|1]个元素),并完全遵循和实现了“响应式扩展规范”(Reactive Extensions Specification)。
Reactor 的 reactor-ipc 组件还支持非阻塞的进程间通信(inter-process communication, IPC)。 Reactor IPC 为 HTTP(包括 Websockets)、TCP 和 UDP 提供了支持背压的网络引擎,从而适合 应用于微服务架构。并且完整支持响应式编解码(reactive encoding and decoding)。
官网地址:project reactor
官网文档:Reactor 3 Reference Guide
引入依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.1.1.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>io.projectreactorgroupId>
<artifactId>reactor-bomartifactId>
<version>Bismuth-RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>io.projectreactorgroupId>
<artifactId>reactor-coreartifactId>
dependency>
<dependency>
<groupId>io.projectreactorgroupId>
<artifactId>reactor-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.4version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.testnggroupId>
<artifactId>testngartifactId>
<version>6.8.7version>
<scope>testscope>
dependency>
dependencies>
编写个main方法
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
项目搭建完成。这里我们只写test方进行测试。
flux
是一个能够发出 0 到 N 个元素的标准的 Publisher,它会被一个“错误(error)” 或“完成(completion)”信号终止。
Mono
是一种特殊的 Publisher, 它最多发出一个元素,然后终止于一个 onComplete 信号或一个 onError 信号。
Flux和Mono是“数据流”的发布者,都可以发出三种“数据信号”:元素值、错误信号、完成信号
,错误信号和完成信号都是终止信号,完成信号用于告知下游订阅者该数据流正常结束,错误信号终止数据流的同时将错误传递给下游订阅者。
Flux:
Mono:
@Test
public void test1() {
Flux.just(1, 2, 3, 4, 5, 6).subscribe(System.out::print);
System.out.println();
Flux.just(1, 2, 3, 4, 5, 6).subscribe(System.out::print, System.out::println, () -> System.out.println("Completed"));
Mono.just(100).subscribe(System.out::println);
}
执行结果
11:04:52.073 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
123456
123456Completed
100
@Test
public void test4() {
Flux.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6))
.filter(i -> i % 2 == 0)//过滤
.map(i -> i * i)//与map reduce中的map类型,逐个元素计算
.subscribe(System.out::println);
}
输出结果
4
16
36
Flux.just(1, 2, 3, 4, 5, 6)仅仅声明了这个数据流,此时数据元素并未发出,只有subscribe()方法调用的时候才会触发数据流。订阅前什么都不会发生。
/**
* flatMap操作可以将每个数据元素转换/映射为一个流,然后将这些流合并为一个大的数据流。
* 流的合并是异步无序的
*
* @throws InterruptedException
*/
@Test
public void test5() throws InterruptedException {
Flux.just("flux", "mono")
.flatMap(s -> Flux.fromArray(s.split("\\s*"))//java中的future可以用这个替换。比如多个请求合并,串行改并行
.delayElements(Duration.ofMillis(100)))
.subscribe(System.out::println);
TimeUnit.SECONDS.sleep(1);//主线程阻塞1秒,因为是异步的,如果不阻塞则会等不到上面处理结果程序就结束了
}
输出结果
f
m
l
o
u
n
x
o
如果一个服务中需要调用多个独立的外部服务,然后组装结果返回。这种情况过去通常使用future,学习了reactor以后就可以使用flux的flatMap方法来实现了。
@Test
public void test2() {
Mono.error(new Exception("my error")).subscribe(
System.out::print,
System.out::println,
() -> System.out.println("Completed"));
}
输出java.lang.Exception: my error
响应式异步编程最麻烦的就是调试了。这里简单介绍一下使用StepVerifier进单元行测试。
@Test
public void testViaStepVerifier() {
StepVerifier.create(Flux.just(1, 2, 3, 4, 5, 6))
.expectNext(1, 2, 3, 4, 5, 6)//下一个期望的数据元素
.expectComplete()//测试下一个元素是否为完成信号
.verify();
StepVerifier.create(Mono.error(new Exception("some error")))
.expectErrorMessage("some error")//校验下一个元素是否为错误信号
.verify();
}
Flux.interval(Duration) 生成的是一个 Flux, 是一个无限地周期性发出规律 tick 的时钟序列。
@Test
public void test7() throws InterruptedException {
Flux.interval(Duration.ofSeconds(1))
.map(aLong -> "a" + aLong) // 类似于map reduce 中的map,对flux发出的消息逐个处理
.subscribe(System.out::println);
TimeUnit.SECONDS.sleep(10);
}
这里设置了每隔1秒生成一个Long数据。由于这里sleep了10秒,所以会输出a0-a9。
@Test
public void testSimpleOperators() throws InterruptedException {
String desc = "zip: 将多个流一对一的合并起来";
// 使用这个来控制程序结束,替换之前的sleep操作
CountDownLatch countDownLatch = new CountDownLatch(1);
Flux.zip(
Flux.fromArray(desc.split("\\s+")),
// 使用Flux.interval声明一个每1000ms发出一个元素的long数据流;因为zip操作是一对一的,故而将其与字符串流zip之后,字符串流也将具有同样的速度;
Flux.interval(Duration.ofMillis(1000)))
// zip之后的流中元素类型为Tuple2,使用getT1方法拿到字符串流的元素
.subscribe(t -> System.out.println(t.getT1() + " " + t.getT2()), null, countDownLatch::countDown);
countDownLatch.await(); // 5
}
输出
Reactor 0
is 1
a 2
fully 3
non-blocking 4
reactive 5
//省略后续输出
使用zip将连个输出流进行压缩,一对一的组装成新的数据流。由于其中一个数据流树一秒发送一个,所以合并后的数据流也具有了相同的速度。
Reactor, 就像 RxJava,也可以被认为是 并发无关(concurrency agnostic) 的。意思就是, 它并不强制要求任何并发模型。更进一步,它将选择权交给开发者。不过,它还是提供了一些方便 进行并发执行的库。
在 Reactor 中,执行模式以及执行过程取决于所使用的 Scheduler。 Scheduler 是一个拥有广泛实现类的抽象接口。 Schedulers 类提供的静态方法用于达成如下的执行环境:
当前线程(Schedulers.immediate())
可重用的单线程(Schedulers.single())。注意,这个方法对所有调用者都提供同一个线程来使用, 直到该调度器(Scheduler)被废弃。如果你想使用专一的线程,就对每一个调用使用 Schedulers.newSingle()。
弹性线程池(Schedulers.elastic()。它根据需要创建一个线程池,重用空闲线程。线程池如果空闲时间过长 (默认为 60s)就会被废弃。对于 I/O 阻塞的场景比较适用。 Schedulers.elastic() 能够方便地给一个阻塞 的任务分配它自己的线程,从而不会妨碍其他任务和资源,见 如何包装一个同步阻塞的调用?。
固定大小线程池(Schedulers.parallel())。所创建线程池的大小与 CPU 个数等同。
使用Schedulers将同步阻塞的getStringSync
方法提交到异步线程池中执行:
/**
* Schedulers线程池
*
* @return
*/
private String getStringSync() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello, Reactor!";
}
@Test
public void testSyncToAsync() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
Mono.fromCallable(() -> getStringSync()) // 声明一个异步回调的mono
.subscribeOn(Schedulers.elastic()) // 将任务提交到内置的弹性线程池中执行
.subscribe(System.out::println, null, countDownLatch::countDown);
countDownLatch.await();
}
Reactor 提供了两种在响应式链中调整调度器 Scheduler的方法:publishOn和subscribeOn
。它们都接受一个 Scheduler作为参数,从而可以改变调度器。
但是两者的作用范围是不同的,具体情况代码注释:
/**
* 切换调度器的操作符
*
* publishOn会影响链中其后的操作符,比如第一个publishOn调整调度器为elastic,则filter的处理操作是在弹性线程池中执行的;
* 同理,flatMap是执行在固定大小的parallel线程池中的;
* subscribeOn无论出现在什么位置,都只影响源头的执行环境,也就是range方法是执行在单线程中的,
* 直至被第一个publishOn切换调度器之前,所以range后的map也在单线程中执行。
*
* @throws InterruptedException
*/
@Test
public void test6() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
Flux.range(1, 1000)
.map(i -> i * i)
.publishOn(Schedulers.elastic()).filter(i -> i % 2 == 0)
.publishOn(Schedulers.parallel()).flatMap(i -> Flux.just(i).delayElements(Duration.ofMillis(1000)))
.subscribeOn(Schedulers.single())
.subscribe(System.out::println, null, countDownLatch::countDown);
countDownLatch.await();
}
/**
* 异常处理
* 错误和完成两个都是终止符,不可同时存在
*/
@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, () -> System.err.println("ok"));
}
输出:
25
100
java.lang.ArithmeticException: / by zero
错误和完成两个都是终止符,不可同时存在
onErrorReturn方法能够在收到错误信号的时候提供一个缺省值。此时终止信号不是error,而是complete
@Test
public void testErrorHandling1() {
Flux.range(1, 6)
.map(i -> 10 / (i - 3)) // 1
.onErrorReturn(0)
.map(i -> i * i)
.subscribe(System.out::println, System.err::println, () -> System.err.println("ok"));
}
输出
25
100
0 //这个是出错后的缺省值
ok //终止信号编程了complete!!
onErrorResume:在收到错误信号的时候提供一个新的数据流。
可用于fallback处理, 类似于hystrix的fallback。或者类似于读取不到缓存则读取DB。
@Test
public void testErrorHandling2() {
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, () -> System.err.println("ok"));
}
输出
25
100
25 //这个是新的数据源,或者说重新执行错误信号
ok //这里终止信号是complete
onErrorMap可以捕获异常,并再包装为某一个业务相关的异常,然后再抛出业务异常
public Flux callExternalService(String str) {
return Flux.range(1, 6)
.map(i -> 10 / (i - 3)) // 1
.map(i -> i * i);
}
/**
* 捕获,并再包装为某一个业务相关的异常,然后再抛出业务异常
* @throws InterruptedException
*/
@Test
public void testErrorHandling3() throws InterruptedException {
Flux.just("a")
.flatMap(k -> callExternalService(k))
// 重新包装
.onErrorMap(original -> new Exception("business error"))
.subscribe(System.out::println, System.err::println, () -> System.err.println("ok"));
TimeUnit.SECONDS.sleep(2);
}
输出
25
100
java.lang.Exception: business error
这里写一个捕获异常,记录错误日志,然后继续抛出的例子。
doOnXxx是只读的,对数据流不会造成影响
@Test
public void testErrorHandling4() {
Flux.range(1, 6)
.map(i -> 10 / (i - 3))
.doOnError(throwable -> log.error(throwable.getMessage()))
.map(i -> i * i)
.subscribe(System.out::println, System.err::println, () -> System.err.println("ok"));
}
输出
25
100
11:35:19.195 [main] ERROR helloworld.MyTest - / by zero //这个是log.error打印的
java.lang.ArithmeticException: / by zero
doFinally在序列终止(无论是 onComplete、onError还是取消)的时候被执行
@Test
public void testErrorHandling5() {
Flux.range(1, 6)
.map(i -> 10 / (i - 3)) // 1
.map(i -> i * i)
.doFinally(type -> {//doFinally在序列终止(无论是 onComplete、onError还是取消)的时候被执行
if (type == SignalType.CANCEL) // 2
log.info("SignalType.CANCEL");
else if(type == SignalType.ON_ERROR)
log.info("SignalType.ON_ERROR");
})
.take(1)//能够在发出N个元素后取消流。
.subscribe(System.out::println, System.err::println, () -> System.err.println("ok"));
}
输出
25
11:37:05.735 [main] INFO helloworld.MyTest - SignalType.CANCEL
ok
背压(backpressure) 具体来说即 消费者能够反向告知生产者生产内容的速度的能力
订阅者可以无限接受数据并让它的源头 “满负荷”推送所有的数据,也可以通过使用 request 机制来告知源头它一次最多能够处理 n 个元素
上游数据源可以根据下游处理的速度进行生产数据,其实是一种“推送+拉取”的混合模式。
我们通过BaseSubscriber
来实现该功能。前面讲到的所有subscribe方法都最终会转换为subscribe(BaseSubscriber)
来执行。hookOnSubscribe和hookOnNext方法时必须要重写的,保证至少获取一个上游发送的一条数据。hookOnSubscribe
用来获取第一个元素,hookOnNext
配合request
来逐个获取剩下的元素。
/**
* 背压测试:
* BaseSubscriber 是顶层的抽象类,上面介绍的所有subscribe方法最终都会转化为 subscribe(BaseSubscriber)执行
* BaseSubscriber 最少要重写hookOnSubscribe和hookOnNext
*/
@Test
public void testBackpressure() {
Flux.range(1, 6)
.doOnRequest(n -> System.out.println("Request " + n + " values..."))
.subscribe(new BaseSubscriber<>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
System.out.println("Subscribed and make a request...");
request(1); //发起第一个请求
}
@Override
protected void hookOnNext(Integer value) { // 逐个处理剩余请求
try {
TimeUnit.SECONDS.sleep(1); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Get value [" + value + "]");
request(1); // 随着接收到新的值,我们继续以每次请求一个元素的节奏从源头请求值。 这是背压核心
}
});
}
输出
Subscribed and make a request...
Request 1 values...
Get value [1]
Request 1 values...
Get value [2]
Request 1 values...
Get value [3]
Request 1 values...
Get value [4]
Request 1 values...
Get value [5]
Request 1 values...
Get value [6]
Request 1 values...
从命令式编程到响应式编程的切换并不是一件容易的事,需要一个适应的过程。不过相信你通过本节的了解和实操,已经可以体会到使用Reactor编程的一些特点:
springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;