文章目录
一、什么是响应式编程
响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
在开发应⽤程序代码时,我们可以编写两种⻛格的代码,即命令式和响应式。
命令式(Imperative)的代码:它由⼀组任务组成,每次只运⾏⼀项任务,每项任务⼜都依赖于前⾯的任务。数据会按批次进⾏处理,在前⼀项任务还没有完成对当前数据批次的处理时,不能将这些数据递交给下⼀项处理任务。
响应式(Reactive)的代码:它定义了⼀组⽤来处理数据的任务,但是这些任务可以并⾏地执⾏。每项任务处理数据的⼀部分⼦集,并将结果交给处理流程中的下⼀项任务,同时继续处理数据的另⼀部分⼦集。
Reactor 是⼀个响应式编程库,同时也是Spring家族的⼀部分。它是Spring 5反应式编程功能的基础。
1、Java的流和响应式流
Java的Stream流通常都是同步的,并且只能处理有限的数据集。从本质上来说,它们只是使⽤函数来对集合进⾏迭代的⼀种⽅式。
响应式流⽀持异步处理任意⼤⼩的数据集,同样也包括⽆限数据集。只要数据就绪,它们就能实时地处理数据,并且能够通过回压来避免压垮数据的消费者。
2、Java中响应式的使用
JDK1.8时,是基于Observer/Observable接口而实现的观察者模式:
ObserverDemo observer = new ObserverDemo();
// 添加观察者
observer.addObserver(new Observer() {
@Override
public void update(Observable o, Object arg) {
System.out.println("发生了变化");
}
});
observer.addObserver(new Observer() {
@Override
public void update(Observable o, Object arg) {
System.out.println("收到了通知");
}
});
observer.setChanged(); // 数据变化
observer.notifyObservers(); // 通知
JDK9及以后,Observer/Observable接口就被弃用了,取而代之的是Flow类:
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.TimeUnit;
public class FlowDemo {
public static void main(String[] args) throws Exception {
// 1. 定义发布者, 发布的数据类型是 Integer
// 直接使用jdk自带的SubmissionPublisher, 它实现了 Publisher 接口
SubmissionPublisher<Integer> publiser = new SubmissionPublisher<Integer>();
// 2. 定义订阅者
Subscriber<Integer> subscriber = new Subscriber<Integer>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
// 保存订阅关系, 需要用它来给发布者响应
this.subscription = subscription;
// 请求一个数据
this.subscription.request(1);
}
@Override
public void onNext(Integer item) {
// 接受到一个数据, 处理
System.out.println("接受到数据: " + item);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 处理完调用request再请求一个数据
this.subscription.request(1);
// 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
// this.subscription.cancel();
}
@Override
public void onError(Throwable throwable) {
// 出现了异常(例如处理数据的时候产生了异常)
throwable.printStackTrace();
// 我们可以告诉发布者, 后面不接受数据了
this.subscription.cancel();
}
@Override
public void onComplete() {
// 全部数据处理完了(发布者关闭了)
System.out.println("处理完了!");
}
};
// 3. 发布者和订阅者 建立订阅关系
publiser.subscribe(subscriber);
// 4. 生产数据, 并发布
// 这里忽略数据生产过程
for (int i = 0; i < 1000; i++) {
System.out.println("生成数据:" + i);
// submit是个block方法
publiser.submit(i);
}
// 5. 结束后 关闭发布者
// 正式环境 应该放 finally 或者使用 try-resouce 确保关闭
publiser.close();
// 主线程延迟停止, 否则数据没有消费就退出
Thread.currentThread().join(1000);
// debug的时候, 下面这行需要有断点
// 否则主线程结束无法debug
System.out.println();
}
}
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.SubmissionPublisher;
/**
* 带 process 的 flow demo
*/
/**
* Processor, 需要继承SubmissionPublisher并实现Processor接口
*
* 输入源数据 integer, 过滤掉小于0的, 然后转换成字符串发布出去
*/
class MyProcessor extends SubmissionPublisher<String>
implements Processor<Integer, String> {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
// 保存订阅关系, 需要用它来给发布者响应
this.subscription = subscription;
// 请求一个数据
this.subscription.request(1);
}
@Override
public void onNext(Integer item) {
// 接受到一个数据, 处理
System.out.println("处理器接受到数据: " + item);
// 过滤掉小于0的, 然后发布出去
if (item > 0) {
this.submit("转换后的数据:" + item);
}
// 处理完调用request再请求一个数据
this.subscription.request(1);
// 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
// this.subscription.cancel();
}
@Override
public void onError(Throwable throwable) {
// 出现了异常(例如处理数据的时候产生了异常)
throwable.printStackTrace();
// 我们可以告诉发布者, 后面不接受数据了
this.subscription.cancel();
}
@Override
public void onComplete() {
// 全部数据处理完了(发布者关闭了)
System.out.println("处理器处理完了!");
// 关闭发布者
this.close();
}
}
public class FlowDemo2 {
public static void main(String[] args) throws Exception {
// 1. 定义发布者, 发布的数据类型是 Integer
// 直接使用jdk自带的SubmissionPublisher
SubmissionPublisher<Integer> publiser = new SubmissionPublisher<Integer>();
// 2. 定义处理器, 对数据进行过滤, 并转换为String类型
MyProcessor processor = new MyProcessor();
// 3. 发布者 和 处理器 建立订阅关系
publiser.subscribe(processor);
// 4. 定义最终订阅者, 消费 String 类型数据
Subscriber<String> subscriber = new Subscriber<String>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
// 保存订阅关系, 需要用它来给发布者响应
this.subscription = subscription;
// 请求一个数据
this.subscription.request(1);
}
@Override
public void onNext(String item) {
// 接受到一个数据, 处理
System.out.println("接受到数据: " + item);
// 处理完调用request再请求一个数据
this.subscription.request(1);
// 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
// this.subscription.cancel();
}
@Override
public void onError(Throwable throwable) {
// 出现了异常(例如处理数据的时候产生了异常)
throwable.printStackTrace();
// 我们可以告诉发布者, 后面不接受数据了
this.subscription.cancel();
}
@Override
public void onComplete() {
// 全部数据处理完了(发布者关闭了)
System.out.println("处理完了!");
}
};
// 5. 处理器 和 最终订阅者 建立订阅关系
processor.subscribe(subscriber);
// 6. 生产数据, 并发布
// 这里忽略数据生产过程
publiser.submit(-111);
publiser.submit(111);
// 7. 结束后 关闭发布者
// 正式环境 应该放 finally 或者使用 try-resouce 确保关闭
publiser.close();
// 主线程延迟停止, 否则数据没有消费就退出
Thread.currentThread().join(1000);
}
}
3、Reactor中响应式流的基本接口
响应式流规范可以总结为4个接⼝:Publisher、Subscriber、Subscription和Processor。
Publisher负责⽣成数据,并将数据发送给 Subscription(每个Subscriber对应⼀个Subscription)。
public interface Publisher<T> {
// Publisher接⼝声明了⼀个⽅法 subscribe(),Subscriber可以通过该⽅法向 Publisher发起订阅。
public void subscribe(Subscriber<? super T> s);
}
⼀旦Subscriber订阅成功,就可以接收来⾃Publisher的事件。
public interface Subscriber<T> {
// Subscriber的第⼀个事件是通过对 onSubscribe()⽅法的调⽤接收的。
public void onSubscribe(Subscription s);
// 每个数据项都会通过该方法处理
public void onNext(T t);
// 异常处理
public void onError(Throwable t);
// 结束
public void onComplete();
}
Publisher调⽤ onSubscribe() ⽅法时,会将Subscription对象传递给 Subscriber。
通过Subscription,Subscriber可以管理其订阅情况:
public interface Subscription {
// Subscriber可以通过调⽤ request()⽅法来请求 Publisher 发送数据,可以传⼊⼀个long类型的数值以表明它愿意接受多少数据
// 这也是回压能够发挥作⽤的地⽅,以避免Publisher 发送多于 Subscriber能够处理的数据量
public void request(long n);
// 调⽤ cancel()⽅法表明它不再对数据感兴趣并且取消订阅
public void cancel();
}
Subscriber 请求数据之后,数据就会开始流经响应式流,调用onNext方法。
Processor接⼝,它是Subscriber和Publisher的组合:
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}
4、Reactor中响应式接口的基本使用
import java.util.concurrent.TimeUnit;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux;
public class ReactorDemo {
public static void main(String[] args) {
// reactor = jdk8 stream + jdk9 reactive stream
// Mono 0-1个元素
// Flux 0-N个元素
String[] strs = { "1", "2", "3" };
// 2. 定义订阅者
Subscriber<Integer> subscriber = new Subscriber<Integer>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
// 保存订阅关系, 需要用它来给发布者响应
this.subscription = subscription;
// 请求一个数据
this.subscription.request(1);
}
@Override
public void onNext(Integer item) {
// 接受到一个数据, 处理
System.out.println("接受到数据: " + item);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 处理完调用request再请求一个数据
this.subscription.request(1);
// 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
// this.subscription.cancel();
}
@Override
public void onError(Throwable throwable) {
// 出现了异常(例如处理数据的时候产生了异常)
throwable.printStackTrace();
// 我们可以告诉发布者, 后面不接受数据了
this.subscription.cancel();
}
@Override
public void onComplete() {
// 全部数据处理完了(发布者关闭了)
System.out.println("处理完了!");
}
};
// 这里就是jdk8的stream
Flux.fromArray(strs).map(s -> Integer.parseInt(s))
// 最终操作
// 这里就是jdk9的reactive stream
.subscribe(subscriber);
}
}
二、初识Reactor
1、Flux和Mono的基本介绍
Reactor中有两个核心类,Mono和Flux。Flux和Mono是Reactor提供的最基础的构建块,⽽这两种响应式类型所提供的操作符则是组合使⽤它们以构建数据流动管线的黏合剂。
这两个类实现接口Publisher,提供丰富操作符。Flux对象实现发布者,返回N个元素Mono实现发布者,返回0或者1个元素。
Flux和Mono都是数据流的发布者,使用Flux和Mono都可以发出三种数据信号:元素值、错误信号、完成信号,错误信号和完成信号都代表终止信号,终止信号用于告诉订阅者数据流结束了,错误信号终止数据流同时把错误信息传递给订阅者。
Flux和Mono共有500多个操作,这些操作都可以⼤致归类为:创建操作;组合操作;转换操作;逻辑操作。
注意!Mono和Flux的很多操作是相同的,只不过对应的数据数量不同,所以本文更多的操作都是基于Flux的,Mono也同理。
2、引入Reactor依赖
需要引入reactor-core核心包和测试包。
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.x.x</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.x.x</version>
<scope>test</scope>
</dependency>
3、响应式类型的创建
Reactor提供了多种创建Flux和Mono的操作。
(1)简单创建
// 使⽤Flux或Mono上的静态 just()⽅法来创建⼀个响应式类型
Mono.just(1);
// 空元素
Mono<Object> objectMono = Mono.justOrEmpty(null);
Flux<String> fruitFlux = Flux
.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
// 调用just或其他方法只是声明数据流,数据流并没有发出,只有进行订阅之后才会触发数据流,不订阅什么都不会发生的。
// 添加一个订阅者,subscribe的方法参数相当于是一个Consumer
fruitFlux.subscribe(
f -> System.out.println("Here's some fruit: " + f)
);
// 根据集合创建
String[] fruits = new String[] {
"Apple", "Orange", "Grape", "Banana", "Strawberry" };
Flux<String> fruitFlux2 = Flux.fromArray(fruits);
List<String> list = Arrays.asList(fruits);
Flux.fromIterable(list); // 集合
Stream<String> stream = list.stream();
Flux.fromStream(stream); // stream流
// 根据区间创建1-5
Flux<Integer> intervalFlux =
Flux.range(1, 5);
intervalFlux.subscribe(
f -> System.out.println("data is :" + f)
);
// 每秒发布⼀个值的Flux,通过interval()⽅法创建的Flux会从0开始发布值,并且后续的条⽬依次递增。
// 因为interval()⽅法没有指定最⼤值,所以它可能会永远运⾏。我们也可以使⽤take()⽅法将结果限制为前5个条⽬。
Flux<Long> intervalFlux2 =
Flux.interval(Duration.ofSeconds(1))
.take(5);
intervalFlux2.subscribe(
f -> System.out.println("data2 is :" + f)
);
// 阻塞,等待结果
Thread.sleep(100000);
(2)使用from工厂创建
Flux.from(new Publisher<String>() {
@Override
public void subscribe(Subscriber<? super String> s) {
for (int i = 0; i < 10; i++) {
s.onNext("hello" + i);
}
s.onComplete();
}
}).subscribe(System.out::println,
System.err::println,
() -> System.out.println("complete"));
(3)使用defer工厂创建(在订阅时决定其行为)
Mono.defer(() -> {
// 可以写自定义逻辑
return Mono.just("success");
}).subscribe(System.out::println);
(4)使用push工厂创建
push 工厂方法能通过适配一个单线程生产者来编程创建 Flux 实例。
此方法对于适配异步、单线程
、多值 AP!非常有用,而无须关注背压和取消, push 方法本身包含背压和取消。
Flux.push(new Consumer<FluxSink<String>>() {
@Override
public void accept(FluxSink<String> fluxSink) {
// 从数据库中获取数据,使用fluxSink追加到响应式流中,将命令处理方式转化为响应式处理方式
fluxSink.next("hello1");
fluxSink.next("hello2");
// fluxSink.complete(); // 生成onComplete信号
}
}).subscribe(System.out::println);
(5)使用create工厂创建
create可以从不同线程发出事件
(6)generate、disposable、usingWhen工厂创建
4、响应式类型的组合
(1)使用mergeWith合并响应式流
Flux<String> characterFlux = Flux
.just("Garfield", "Kojak", "Barbossa")
.delayElements(Duration.ofMillis(500)); // 每500毫秒发布⼀个数据
Flux<String> foodFlux = Flux
.just("Lasagna", "Lollipops", "Apples")
.delaySubscription(Duration.ofMillis(250)) // 订阅后250毫秒后开始发布数据
.delayElements(Duration.ofMillis(500)); // 每500毫秒发布⼀个数据
// 使⽤mergeWith()⽅法,将两个Flux合并,合并过后的Flux数据项发布顺序与源Flux的发布时间⼀致
// Garfield Lasagna Kojak Lollipops Barbossa Apples
Flux<String> mergedFlux = characterFlux.mergeWith(foodFlux);
mergedFlux.subscribe(System.out::println);
// 阻塞,等待结果
Thread.sleep(100000);
我们发现,使用mergeWith合并过的两个FLux,并没有严格意义上的先后之分,谁产生了数据就接着消费,与同一个无异。
(2)使用zip压缩合并响应式流
Flux<String> characterFlux = Flux
.just("Garfield", "Kojak", "Barbossa");
Flux<String> foodFlux = Flux
.just("Lasagna", "Lollipops", "Apples");
// 当两个Flux对象压缩在⼀起的时候,它将会产⽣⼀个新的发布元组的Flux,其中每个元组中都包含了来⾃每个源Flux的数据项
// 这个合并后的Flux发出的每个条⽬都是⼀个Tuple2(⼀个容纳两个其他对象的容器对象)的实例,其中包含了来⾃每个源Flux的数据项,并保持着它们发布的顺序。
Flux<Tuple2<String, String>> zippedFlux =
Flux.zip(characterFlux, foodFlux);
zippedFlux.subscribe(t -> {
System.out.println(t.getT1() + "|" + t.getT2());
});
/**
* 执行结果:
* Garfield|Lasagna
* Kojak|Lollipops
* Barbossa|Apples
*/
(3)使用zip压缩合并为自定义对象的响应式流
如果你不想使⽤Tuple2,⽽想要使⽤其他类型,就可以为zip()⽅法提供⼀个合并函数来⽣成你想要的任何对象,合并函数会传⼊这两个数据项。
zip操作的另⼀种形式(从每个传⼊Flux中各取⼀个元素,然后创建消息对象,并产⽣这些消息组成的Flux)
Flux<String> characterFlux = Flux
.just("Garfield", "Kojak", "Barbossa");
Flux<String> foodFlux = Flux
.just("Lasagna", "Lollipops", "Apples");
// 压缩成自定义对象
Flux<String> zippedFlux =
Flux.zip(characterFlux, foodFlux, (c, f) -> c + " eats " + f);
zippedFlux.subscribe(System.out:: println);
/**
* 执行结果:
* Garfield eats Lasagna
* Kojak eats Lollipops
* Barbossa eats Apples
*/
(4)选择第⼀个反应式类型进⾏发布
假设我们有两个Flux对象,此时我们不想将它们合并在⼀起,⽽是想要创建⼀个新的Flux,让这个新的Flux从第⼀个产⽣值的Flux中发布值。first()操作会在两个Flux对象中选择第⼀个发布值的Flux,并再次发布它的值。
Flux<String> slowFlux = Flux.just("tortoise", "snail", "sloth")
.delaySubscription(Duration.ofMillis(100)); // 延迟100ms
Flux<String> fastFlux = Flux.just("hare", "cheetah", "squirrel");
// 选择第⼀个反应式类型进⾏发布
Flux<String> firstFlux = Flux.first(slowFlux, fastFlux);
firstFlux.subscribe(System.out::println);
// 阻塞,等待结果
Thread.sleep(100000);
/**
* 执行结果:
* hare
* cheetah
* squirrel
*/
5、转换和过滤反应式流
在数据流经⼀个流时,我们通常需要过滤掉某些值并对其他的值进⾏处理。
(1)skip操作跳过指定数⽬的消息
skip操作跳过指定数⽬的消息并将剩下的消息继续在结果Flux上进⾏传递
// 跳过3个,并创建一个新的Flux
Flux<String> skipFlux = Flux.just(
"one", "two", "skip a few", "ninety nine", "one hundred")
.skip(3);
skipFlux.subscribe(System.out::println);
/**
* 执行结果
* ninety nine
* one hundred
*/
(2)skip()操作的另⼀种形式
在⼀段时间之内跳过所有的第⼀批数据。
// 这是skip()操作的另⼀种形式,将会产⽣⼀个新Flux,在发布来⾃源Flux的数据项之前等待指定的⼀段时间
Flux<String> skipFlux = Flux.just(
"one", "two", "skip a few", "ninety nine", "one hundred")
.delayElements(Duration.ofSeconds(1)) // 每1秒一个
.skip(Duration.ofSeconds(4)); // 4秒前的都跳过
skipFlux.subscribe(System.out::println);
// 阻塞,等待结果
Thread.sleep(100000);
/**
* 执行结果:
* ninety nine
* one hundred
*/
(3)take操作只发布第⼀批指定数量的数据项
根据对skip操作的描述来看,take可以认为是与skip相反的操作。skip操作会跳过前⾯⼏个数据项,⽽take操作只发布第⼀批指定数量的数据项,然后将取消订阅。
// take操作只发布传⼊Flux中前⾯指定数⽬的数据项,然后将取消订阅
Flux<String> nationalParkFlux = Flux.just(
"Yellowstone", "Yosemite", "Grand Canyon",
"Zion", "Grand Teton")
.take(3);
nationalParkFlux.subscribe(System.out::println);
/**
* 执行结果:
* Yellowstone
* Yosemite
* Grand Canyon
*/
(4)take操作的另一种形式
take()⽅法也有另⼀种替代形式,基于间隔时间⽽不是数据项个数(在指定的时间过期之前,⼀直将消息传递给结果Flux)。它将接受并发布与源Flux⼀样多的数据项,直到某段时间结束,之后Flux将会完成。
// 在订阅之后的前3.5秒发布数据条⽬。
Flux<String> nationalParkFlux = Flux.just(
"Yellowstone", "Yosemite", "Grand Canyon",
"Zion", "Grand Teton")
.delayElements(Duration.ofSeconds(1))
.take(Duration.ofMillis(3500));
nationalParkFlux.subscribe(System.out::println);
// 阻塞,等待结果
Thread.sleep(100000);
/**
* 执行结果:
* Yellowstone
* Yosemite
* Grand Canyon
*/
(5)filter操作自定义过滤条件
filter操作允许我们根据任何条件进⾏选择性地发布。
Flux<String> nationalParkFlux = Flux.just(
"Yellowstone", "Yosemite", "Grand Canyon",
"Zion", "Grand Teton")
.filter(np -> !np.contains(" ")); // 过滤携带空格的
nationalParkFlux.subscribe(System.out::println);
/**
* 执行结果
* Yellowstone
* Yosemite
* Zion
*/
(6)distinct操作去重
Flux<String> animalFlux = Flux.just(
"dog", "cat", "bird", "dog", "bird", "anteater")
.distinct();
// 去重
animalFlux.subscribe(System.out::println);
/**
* 执行结果:
* dog
* cat
* bird
* anteater
*/
(7)map操作映射新元素
map将元素映射为新的元素,并创建一个新的Flux。
// map将元素映射为新的元素,并创建一个新的Flux
Flux<Integer> integerFlux = Flux
.just("Michael Jordan", "Scottie Pippen", "Steve Kerr")
.map(n -> {
String[] split = n.split("\\s");
return split.length; // 将String转为Integer
});
integerFlux.subscribe(System.out::println);
/**
* 执行结果:
* 2
* 2
* 2
*/
其中重要的⼀点是:在每个数据项被源Flux发布时,map操作是同步执⾏的,如果你想要异步地转换过程,那么你应该考虑使⽤flatMap操作。
(8)flatMap将流转成新的流
flatMap并不像map操作那样简单地将⼀个对象转换到另⼀个对象,⽽是将对象转换为新的Mono或Flux。结果形成的Mono或Flux会扁平化为新的Flux。当与subscribeOn()⽅法结合使⽤时,flatMap操作可以释放Reactor反应式的异步能⼒。
// 使⽤flatMap()⽅法和subscribeOn()⽅法
Flux<Integer> integerFlux = Flux
.just("Michael", "Scottie Pippen", "Steve Kerr Ob")
.flatMap(n -> Mono.just(n)
.map(p -> {
String[] split = p.split("\\s");
return split.length; // 将String转为Integer
})
.subscribeOn(Schedulers.parallel()) // 定义异步
);
integerFlux.subscribe(System.out::println);
// 阻塞,等待结果
Thread.sleep(100000);
(9)buffer操作现将数据流拆分为小块
buffer操作会产⽣⼀个新的包含列表Flux(具备最⼤⻓度限制的列表,包含从传⼊的Flux中收集来的数据)
// buffer操作会产⽣⼀个新的包含列表Flux(具备最⼤⻓度限制的列表,包含从传⼊的Flux中收集来的数据)
Flux<String> fruitFlux = Flux.just(
"apple", "orange", "banana", "kiwi", "strawberry");
// 创建⼀个新的包含List 集合的Flux,其中每个List只有不超过指定数量的元素
Flux<List<String>> bufferedFlux = fruitFlux.buffer(3); // 数据切分为小块,每3个一块
bufferedFlux.subscribe(System.out::println);
/**
* 执行结果:
* [apple, orange, banana]
* [kiwi, strawberry]
*/
// 可以分片后并行执行
bufferedFlux.flatMap(x ->
Flux.fromIterable(x)
.map(y -> y.toUpperCase())
.subscribeOn(Schedulers.parallel())
).subscribe(l -> {
System.out.println(Thread.currentThread().getName() + "线程执行:" + l);
});
/**
* 执行结果(因为并行执行,结果可能不一致):
* parallel-1线程执行:APPLE
* parallel-1线程执行:ORANGE
* parallel-1线程执行:BANANA
* parallel-2线程执行:KIWI
* parallel-2线程执行:STRAWBERRY
*/
// 阻塞,等待结果
Thread.sleep(100000);
使⽤不带参数的buffer()⽅法可以将Flux发布的所有数据项都收集到⼀个List中:
Flux<List<String>> bufferedFlux = fruitFlux.buffer();
(10)collectList操作也可以将所有数据收集到一个List
collectList操作将产⽣⼀个包含传⼊Flux发布的所有消息的Mono。
Flux<String> fruitFlux = Flux.just(
"apple", "orange", "banana", "kiwi", "strawberry");
// 生成一个Mono,里面包含一个List
Mono<List<String>> fruitListMono = fruitFlux.collectList();
(11)collectMap 操作产生⼀个发布Map的Mono
collectMap操作将会产⽣⼀个Mono(包含了由传⼊Flux所发出的消息产⽣的Map,这个Map的key是从传⼊消息的某些特征衍⽣⽽来的)
Flux<String> animalFlux = Flux.just(
"aardvark", "elephant", "koala", "eagle", "kangaroo");
Mono<Map<Character, String>> animalMapMono =
animalFlux.collectMap(a -> a.charAt(0)); // 将第一个字符作为Map的key
animalMapMono.subscribe(System.out::println);
/**
* 执行结果:
* {a=aardvark, e=eagle, k=kangaroo}
*/
// 阻塞,等待结果
Thread.sleep(100000);
key相同的,会被覆盖。
(12)collectMultimap产生自定义Map
Flux.just(1, 2, 3, 4, 5)
.collectMultimap(
item -> "key:" + item,
item -> {
List<String> values = new ArrayList<>();
for (int i = 0; i < item; i++) {
values.add("value:" + i);
}
return values;
},
() -> { // 扩充
Map map = new HashMap();
map.put("other", "other");
return map;
})
.subscribe(System.out::println);
(13)repeat重复操作符
Flux.just(1, 2, 3)
.repeat(3) // 实际打印四次,一次原始的,三次重复的
.subscribe(System.out::println);
6、在反应式类型上执行逻辑操作
(1)⽤all()⽅法来确保Flux中的所有消息都满⾜某些条件
Flux<String> animalFlux = Flux.just(
"aardvark", "elephant", "koala", "eagle", "kangaroo");
Mono<Boolean> hasAMono = animalFlux.all(a -> a.contains("a"));
都满足条件会返回true,否则返回false。
(2)⽤any()⽅法来确保Flux中⾄少有⼀个消息满⾜某些条件
Flux<String> animalFlux = Flux.just(
"aardvark", "elephant", "koala", "eagle", "kangaroo");
Mono<Boolean> hasAMono = animalFlux.any(a -> a.contains("t"));
至少有一个满足条件,就为true,都不满足就为false。
7、在响应式类型上使用Subscriber订阅
(1)使用Subscriber消费消息
Flux<String> stringFlux = Flux.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
stringFlux.subscribe(new Subscriber<String>() {
// 保存订阅关系, 需要用它来给发布者响应
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
System.out.println("订阅者开始订阅");
this.subscription = subscription;
// 请求一个数据
this.subscription.request(1);
}
@Override
public void onNext(String item) {
System.out.println("订阅者开始处理数据" + item);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 处理完调用request再请求一个数据
this.subscription.request(1);
// 或者 已经达到了目标, 调用cancel告诉发布者不再接受数据了
// this.subscription.cancel();
}
@Override
public void onError(Throwable t) {
// 出现了异常(例如处理数据的时候产生了异常)
t.printStackTrace();
// 我们可以告诉发布者, 后面不接受数据了
this.subscription.cancel();
}
@Override
public void onComplete() {
// 全部数据处理完了(发布者关闭了)
System.out.println("订阅者处理完了!");
}
});
/**
* 执行结果:
* 订阅者开始订阅
* 订阅者开始处理数据Apple
* 订阅者开始处理数据Orange
* 订阅者开始处理数据Grape
* 订阅者开始处理数据Banana
* 订阅者开始处理数据Strawberry
* 订阅者处理完了!
*/
// 阻塞
Thread.sleep(10000);
(2)使用Flux的doOnNext处理数据
Flux的doOnNext,会添加当Flux发出一个项目时触发的行为(副作用)。
Flux<String> stringFlux = Flux.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
stringFlux.doOnNext(t -> System.out.println("发布者处理数据:" + t))
.subscribe(t -> System.out.println("订阅者处理数据:" + t));
/**
* 执行结果:
* 发布者处理数据:Apple
* 订阅者处理数据:Apple
* 发布者处理数据:Orange
* 订阅者处理数据:Orange
* 发布者处理数据:Grape
* 订阅者处理数据:Grape
* 发布者处理数据:Banana
* 订阅者处理数据:Banana
* 发布者处理数据:Strawberry
* 订阅者处理数据:Strawberry
*/
// 阻塞
Thread.sleep(10000);
但是!以下写法是不会触发发布者的doOnNext事件的:
Flux<String> stringFlux = Flux.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
stringFlux.doOnNext(t -> System.out.println("发布者处理数据:" + t));
stringFlux.subscribe(t -> System.out.println("订阅者处理数据:" + t));
只有链式调用,才会触发发布者的doOnNext事件。
doOnNext可以写多个,顺序执行:
Flux<String> stringFlux = Flux.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
stringFlux.doOnNext(t -> System.out.println("发布者1处理数据:" + t))
.doOnNext(t -> System.out.println("发布者2处理数据:" + t))
.subscribe(t -> System.out.println("订阅者处理数据:" + t));
/**
* 执行结果:
* 发布者1处理数据:Apple
* 发布者2处理数据:Apple
* 订阅者处理数据:Apple
* 发布者1处理数据:Orange
* 发布者2处理数据:Orange
* 订阅者处理数据:Orange
* 发布者1处理数据:Grape
* 发布者2处理数据:Grape
* 订阅者处理数据:Grape
* 发布者1处理数据:Banana
* 发布者2处理数据:Banana
* 订阅者处理数据:Banana
* 发布者1处理数据:Strawberry
* 发布者2处理数据:Strawberry
* 订阅者处理数据:Strawberry
*/
8、使用then来处理完成数据返回
Flux<String> just = Flux.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
// 返回一个Mono ,在此Flux完成时完成。这将主动忽略序列,只重放完成或错误信号。
just.doOnNext(t -> System.out.println("发布者处理数据:" + t))
.then(Mono.defer(() -> {
return Mono.just("我完成了");
}))
.subscribe(t -> System.out.println("订阅者处理数据:" + t));
/**
* 执行结果:
* 发布者处理数据:Apple
* 发布者处理数据:Orange
* 发布者处理数据:Grape
* 发布者处理数据:Banana
* 发布者处理数据:Strawberry
* 订阅者处理数据:我完成了
*/
通常来说,发布者发布完之后,都需要调用then来处理数据,或调用thenEmpty返回一个空的Mono(Mono.empty())。
9、Reactor之Schedulers,publishOn 和 subscribeOn
(1)概述
Schedulers类似Executor,是Reactor的线程调度接口。提供以下几种线程执行环境:
当前线程(Schedulers.immediate())
;
可重用的单线程(Schedulers.single())
。注意,这个方法对所有调用者都提供同一个线程来使用, 直到该调度器被废弃。如果你想使用独占的线程,请使用Schedulers.newSingle();
弹性线程池(Schedulers.elastic())
。它根据需要创建一个线程池,重用空闲线程。线程池如果空闲时间过长 (默认为 60s)就会被废弃。对于 I/O 阻塞的场景比较适用。Schedulers.elastic()能够方便地给一个阻塞 的任务分配它自己的线程,从而不会妨碍其他任务和资源;
固定大小线程池(Schedulers.parallel())
,所创建线程池的大小与CPU个数等同;
自定义线程池(Schedulers.fromExecutorService(ExecutorService))
基于自定义的ExecutorService创建 Scheduler(虽然不太建议,不过你也可以使用Executor来创建)。
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。
最新版本中elastic被废弃了,重新提供了boundedElastic
。
// Schedulers#boundedElastic
public static Scheduler boundedElastic() {
return cache(CACHED_BOUNDED_ELASTIC, BOUNDED_ELASTIC, BOUNDED_ELASTIC_SUPPLIER);
}
static final Supplier<Scheduler> BOUNDED_ELASTIC_SUPPLIER =
() -> newBoundedElastic(DEFAULT_BOUNDED_ELASTIC_SIZE, DEFAULT_BOUNDED_ELASTIC_QUEUESIZE,
BOUNDED_ELASTIC, BoundedElasticScheduler.DEFAULT_TTL_SECONDS, true);
DEFAULT_BOUNDED_ELASTIC_SIZE表示全局bounddElastic()调度器的最大线程数,DEFAULT_BOUNDED_ELASTIC_SIZE由属性reactor.schedulers.defaultBoundedElasticSize设置,若未设置则初始化为10倍处理器数。
DEFAULT_BOUNDED_ELASTIC_QUEUESIZE表示全局bounddElastic()调度器的无法创建更多线程时要排队的最大任务数。DEFAULT_BOUNDED_ELASTIC_QUEUESIZE由属性reactor.schedulers.defaultBoundedElasticQueueSize设置,若未设置则初始化为100000。
(2)publishOn 和 subscribeOn
publishOn 和 subscribeOn都是在指定的Scheduler
中运行。当某些操作执行慢,阻碍运行速度时可以在指定的Scheduler中执行。
@Test
public void testPublishOn() {
Flux.just("tom")
.map(s -> {
System.out.println("[map] Thread name: " + Thread.currentThread().getName());
return s.concat("@mail.com");
})
.publishOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-publishOn"))
.filter(s -> {
System.out.println("[filter] Thread name: " + Thread.currentThread().getName());
return s.startsWith("t");
})
.doOnNext((t) -> {
System.out.println("[ doOnNext ] Thread name:" + Thread.currentThread().getName());
})
.subscribeOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-subscribeOn"))
.subscribe(s -> {
System.out.println("[subscribe] Thread name: " + Thread.currentThread().getName());
System.out.println(s);
});
}
结果:
[map] Thread name: thread-subscribeOn-1
[filter] Thread name: thread-publishOn-2
[ doOnNext ] Thread name:thread-publishOn-2
[subscribe] Thread name: thread-publishOn-2
tom@mail.com
可以看到map操作在subscribeOn设置的Schedulers中运行,从publishOn以后都是在publishOn设置的Schedulers中运行,即使是subscribeOn操作后面的操作。从上面可知subscribeOn从开头开始影响操作所在的线程,从publishOn操作之后所有的操作都在publishOn设置的Schedulers中运行。
@Test
public void testPublishOn1() {
Flux.just("tom")
.map(s -> {
System.out.println("[map] Thread name: " + Thread.currentThread().getName());
return s.concat("@mail.com");
})
.publishOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-publishOn"))
.filter(s -> {
System.out.println("[filter] Thread name: " + Thread.currentThread().getName());
return s.startsWith("t");
})
.doOnNext((t) -> {
System.out.println("[ doOnNext ] Thread name:" + Thread.currentThread().getName());
})
.subscribeOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-subscribeOn"))
.doOnNext((t) -> {
System.out.println("[ doOnNext1 ] Thread name:" + Thread.currentThread().getName());
})
.subscribeOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-000"))
.doOnNext((t) -> {
System.out.println("[ doOnNext2 ] Thread name:" + Thread.currentThread().getName());
})
.subscribe(s -> {
System.out.println("[subscribe] Thread name: " + Thread.currentThread().getName());
System.out.println(s);
});
}
结果:
[map] Thread name: thread-subscribeOn-2
[filter] Thread name: thread-publishOn-3
[ doOnNext ] Thread name:thread-publishOn-3
[ doOnNext1 ] Thread name:thread-publishOn-3
[ doOnNext2 ] Thread name:thread-publishOn-3
[subscribe] Thread name: thread-publishOn-3
tom@mail.com
第二个subscribeOn不起作用。只有第一个subscribeOn才有作用。
@Test
public void testPublishOn3() {
Flux.just("tom")
.publishOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-publishOn"))
.doOnNext((t) -> {
System.out.println("[ doOnNext ] Thread name:" + Thread.currentThread().getName());
})
.publishOn(Schedulers.newBoundedElastic(Runtime.getRuntime().availableProcessors(), 1000,"thread-publishOn000"))
.doOnNext((t) -> {
System.out.println("[ doOnNext1 ] Thread name:" + Thread.currentThread().getName());
})
.subscribe(s -> {
System.out.println("[subscribe] Thread name: " + Thread.currentThread().getName());
System.out.println(s);
});
}
结果:
[ doOnNext ] Thread name:thread-publishOn-2
[ doOnNext1 ] Thread name:thread-publishOn000-1
[subscribe] Thread name: thread-publishOn000-1
tom
第二个publishOn会影响第一个publishOn。
10、将响应式流转化为阻塞结构
Project Reactor 库提供了一个 API,用于将响应式流转换为阻塞结构。
有以下选项来阻塞流并同步生成结果:
1、tolterable
方法将响应式 Flux 转换为阻塞 lterable
,
2、toStream
方法将响应式 Flux 转换为阻塞 Stream API。从 Reactor 3.2 开始,在底层使用tolterable 方法。
3、blockFirst
方法阻塞了当前线程,直到上游发出第一个值或完成流为止。
4、blockLast
方法阻塞当前线程,直到上游发出最后一个值或完成流为止。在 onError的情况下,它会在被阻塞的线程中抛出异常。
blockFirst
操作符和 blockLast 操作符具有开法重载,可用于设置线程阻塞的持续时间。这应该可以防止线程被无限阻塞。
11、背压处理
尽管响应式流规范要求将背压构建到生产者和消费者之间的通信中,但这仍然可能使消费者溢出。
一些消费者可能无意识地请求无界需求,然后无法处理生成的负载。
另一些消费者则可能对传入消息的速率有严格的限制。例如,数据库客户端每秒不能插入超过 1000条记录。在这种情况下,事件批处理技术可能有所帮助。
可以通过以下方式配置流以处理背压情况:
1.onBackPressureBuffer 操作符会请求无界需求并将返回的元素推送到下游。如果下游消费者无法跟上,那么元素将缓冲在队列中。2.onBackPressureDrop 操作符也请求无界需求(Integer.MAX_VALUE)并向下游推送数据。如果下游请求数量不足,那么元素会被丢弃。自定义处理程序可以用来处理已丢弃的元素。
3.onBackPressureLast操作符与 onBackPressureDrop 的工作方式类似。只是会记住最近收到的元素,并在需求出现时立即将其推向下游。
4.onBackPressureError 操作符在尝试向下游推送数据时请求无界需求。如果下游消费者无法跟上,则操作符会引发错误管理背压的另一种方法是使用速率限制技术。
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Flux.range(1, 1000)
.delayElements(Duration.ofMillis(10))
.onBackpressureBuffer(600)
.delayElements(Duration.ofMillis(100))
.subscribe(System.out::println,
ex -> {
System.out.println(ex);
latch.countDown();
},
() -> {
System.out.println("complete");
latch.countDown();
});
latch.await();
System.out.println("end");
}
}