Reactor响应式编程系列(三)- Operator操作符
一. 前言
在讲每个Operator的具体作用之前,我觉得有个易错点需要提前告知大家,请看个官网案例:
// 目的是让两个字符串全部转化成 *
Flux<String> f1 = Flux.just("hello", "world");
f1.map(s -> s.replaceAll(".", "*"));
f1.subscribe(System.out::println);
结果如下:
可见,最终输出的结果并不是我们预想到的,也就是我们的操作符好像没有起作用。为什么会产生这样的原因呢?
答案:
Reactor的操作符引用了装饰器模式的设计思想,因此它们每一个操作符返回后的对象是一个不同的发布者实例。 而这个实例只是对上游序列进行了包装,并在原本的基础上增加一些处理行为。
因此我们在Reactor编程的时候,不能够犯上面的错误,正确的写法最好是这样:
// 链式编程,若不习惯这样,也可以将上述代码改为===> f1 = f1.map(s -> s.replaceAll(".", "*"));
Flux.just("hello", "world")
.map(s -> s.replaceAll(".", "*"))
.subscribe(System.out::println);
这种写法最后的输出才是我们预期的结果:
接下来就讲几个常用的操作符的使用(很难把所有的方法(包括重载方法)都讲全,如果大家有兴趣,可以查看官网:Reactor操作符官方文档)
二. Operator操作符
buffer()
作用:
- buffer是将当前流中的元素收集到集合当中,并将集合对象作为流的新元素。
@org.junit.Test
/**
* 1.buffer是将当前流中的元素收集到集合当中,并将集合对象作为流的新元素。
* 2.在进行元素收集的时候可以指定几个条件:
* 一个对象所包含元素的最大数量
* 收集的时间间隔
*/
public void testBuffer() {
// 结果:输出5组数据,每一组数据包含20个元素
Flux.range(1, 100).buffer(20).subscribe(System.out::println);
System.out.println("====================================================*====================================================");
// 结果:输出五组数据,每一组包含2个元素,只有当前元素为偶数的时候才会停止当前元素的收集,接下来的元素另起组
/**
* [1, 2] [3, 4] [5, 6] [7, 8] [9, 10]
*/
Flux.range(1, 10).bufferUntil(i -> i % 2 == 0).subscribe(System.out::println);
System.out.println("====================================================*====================================================");
// 结果:输出五组数据,每一组1个元素,都是偶数
/**
* [2] [4] [6] [8] [10]
*/
Flux.range(1, 10).bufferWhile(i -> i % 2 == 0).subscribe(System.out::println);
}
实际输出结果如下:
此外,buffer()
方法还有另外两个类似方法,其参数:表示每个集合中的元素所要满足的条件的 Predicate
对象(也可以理解为表达式)
bufferUntil()
:会一直收集直到Predicate
返回为 true。此时当前元素可以选择添加到当前集合或下一个集合中。bufferWhile()
:只有当Predicate
返回 true 时才会收集。一旦值为 false,会立即开始下一次收集。
filter()
作用:
- 对流中包含的元素进行过滤,只留下满足
Predicate
指定条件的元素。
Flux.range(1, 10).filter(i -> i % 2 == 0).subscribe(System.out::println);
结果如下:
window()
作用:
- 把当前流中的元素收集到另外的 Flux 序列中。
// 可以观察到,这里的返回类型是Flux<Flux<Integer>>,也就是父Flux序列中的每一个元素也是一个Flux序列
Flux<Flux<Integer>> window = Flux.range(1, 100).window(20);
window.subscribe(System.out::println);
而这里的结果输出如下:
为什么会这样呢?输出的结果是UnicastProcessor
。其实,在调用window()方法的时候,其调用过程大致如下:
- 序列得到订阅,则开始首次发送元素,这时候会创建一个新的源序列
UnicastProcessor
。 - 当
UnicastProcessor
接受的元素数量到达20个时,执行对应的onComplete()
方法。 - 那么当第21个元素发送的时候,又会创建一个新的源序列
UnicastProcessor
,并向下游传递,以此类推。
注意: window和buffer的区别:(首先有个前提,我们数据都是从上游往下游进行传输的)
- window是先创建一个
UnicastProcessor
序列,然后直接向下游传递。 - buffer是先收集满20个元素,再向下游进行传递。
zipwith()
作用:
- 把当前流中的元素与另外一个流中的元素按照一对一的方式进行合并。
注意:
- 在合并时可以不做任何处理,由此得到的是一个元素类型为 Tuple2 的流。
- 若某个序列中的元素数量偏多或者偏少,那么多余的结果并不会输出。
- 可以使用函数对元素进行处理,如合并操作。
// 结果:[a,c] [b,d]
Flux.just("a", "b").zipWith(Flux.just("c", "d","e","f")).subscribe(System.out::println);
// 结果:a-c b-d
Flux.just("a", "b").zipWith(Flux.just("c", "d"), (s1, s2) -> String.format("%s-%s", s1, s2)).subscribe(System.out::println);
输出结果图:
take()
作用:
- 提取元素。
/**
* take操作
* takeLast(long n):提取流中的最后 N 个元素。
* takeUntil(Predicate<? super T> predicate):提取元素直到 Predicate 返回 true。
* takeWhile(Predicate<? super T> continuePredicate): 当 Predicate 返回 true 时才进行提取。
* takeUntilOther(Publisher<?> other):提取元素直到另外一个流开始产生元素。
**/
Flux.range(1, 100).take(3).subscribe(System.out::println);
System.out.println("===================================================================================");
Flux.range(1, 100).takeLast(3).subscribe(System.out::println);
System.out.println("===================================================================================");
Flux.range(1, 100).takeWhile(i -> i < 3).subscribe(System.out::println);
System.out.println("===================================================================================");
Flux.range(1, 100).takeUntil(i -> i == 3).subscribe(System.out::println);
System.out.println("===================================================================================");
输出结果如下:
reduce()
作用:
- 对流中包含的所有元素进行累积操作,得到一个包含计算结果的 Mono 序列。
Mono<Integer> reduce = Flux.range(1, 100).reduce(Integer::sum);
reduce.subscribe(System.out::println);
System.out.println("===================================");
Flux.range(1, 100).reduceWith(() -> 100, Integer::sum).subscribe(System.out::println);
结果如下:
注意:
- 在操作时可以指定一个初始值。如果没有初始值,则序列的第一个元素作为初始值。
Integer::sum
等同于a+b。- 累积操作并不单单代表累加、累积的任意一种,而指的是对所有的元素做一个统一的操作,可能是累积、累加等一系列操作。
merge()
作用:
- 用来把多个流合并成一个 Flux 序列。
@Test
public void test() throws InterruptedException {
Flux.merge(Flux.interval(Duration.ZERO, Duration.ofMillis(100)).take(9),
Flux.interval(Duration.ofMillis(50), Duration.ofMillis(100)).take(2),
Flux.interval(Duration.ofMillis(100), Duration.ofMillis(100)).take(3))
.subscribe((t) -> System.out.println("Flux.merge :" + t + ",线程:" + Thread.currentThread().getName()));
// 让主线程睡眠2s,保证上面的输出能够完整
Thread.sleep(2000);
}
输出结果如下:
拓展:Flux.interval(xxx1,xxx2)
:时间类操作符,按照指定的参数来创建流。
- xxx1:第一次执行的延迟时间。
- xxx2:每隔多少秒发送一次事件,发送的内容是Long类型整数,从0开始。
注意:
- merge 会按照所有流中元素的实际产生顺序来合并,所以我在代码中定义了每个流中元素产生的时间间隔。
merge()
方法的兄弟方法:mergeSequential()
,按照所有流被订阅的顺序,以流为单位进行合并。- 代码中对
Flux.interval()
方法生成的流,采用了take()操作来制定获取的元素,若不采用,那么该流是一个无限序列。
flatMap()
作用:
- 把流中的每个元素转换成一个流,再把所有流中的元素进行合并。
同样有兄弟方法flatMapSequential()
,两者区别则类似于上述的merge。
Flux<String> stringFlux1 = Flux.just("a", "b", "c", "d", "e", "f", "g", "h", "i");
// [a,b],[c,d],[e,f],[g,h],[i]
Flux<Flux<String>> stringFlux2 = stringFlux1.window(2);
stringFlux2.flatMap(flux1 -> flux1.map(String::toUpperCase)).subscribe(System.out::print);
输出结果如下:
concatMap()
作用:
- concatMap 操作同样是根据源中的每个元素来获取一个于源,再把所有子源进行合并。
Flux.just(5, 10)
// 将源中的每个元素来获取一个子源,那么这里的x指的是5或者10
// 然后以元素10位例:x=10,通过Flux.interval来生成序列,并取出10-3=7个元素
.concatMap(x -> Flux.interval(Duration.ofMillis(x * 10), Duration.ofMillis(100)).map(i -> x + ": " + i).take(x - 3))
.toStream()
.forEach(System.out::println);
结果如下(元素5对应2个元素,元素10对应7个元素):
注意: concatMap和flatMap有什么不同?
- 顺序:concatMap 操作会根据初始源中的元素顺序依次将获取到的子源进行合并。而flatMap是不会的。
- 订阅:concatMap 操作对所获取到的子源的订阅是动态进行的,而flatMap则是在合并开始之前就订阅了由源下发的所有子源。
groupBy()
作用:
- 通过一个策略 key 将一个 Flux分割为多个组。
Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9)
.groupBy(i -> i % 2 == 0 ? "even" : "odd")
.concatMap(i -> i.defaultIfEmpty(-1).map(String::valueOf).startWith(i.key()))
.subscribe(System.out::println);
输出结果:
注意:
- 几个组之间的元素不会存在交集,也就是一个元素只属于其中1个组。
- 组永远不会为空,因为如果进行分组的时候发现没有对应的组,则进行创建操作。
combineLateset()
作用:
- 把所有源中最新产生的元素合并成一个新的元素并下发。
Flux.combineLatest(Flux.interval(Duration.ofSeconds(2)).map(x -> "源1产生数据 " + x).take(5),
Flux.interval(Duration.ofSeconds(1)).map(x -> "源2产生数据 " + x).take(5),
(a, b) -> a + ":" + b)
.toStream()
.forEach(System.out::println);
输出结果如下:
自己跑一下你会发现,只要有一个源产生元素,控制台就会输出一条数据。
注意:
- 只要源中任何一个子源产生了新的元素,合并操作就会执行一次,然后下发新元素。
背压操作符
背压操作符有这么几种,直接调即可:
onBackpressureBuffer()
onBackpressureDrop()
onBackpressureError()
onBackpressureLatest()
三. 打包操作
transform()
在编程中,可能有一些代码是一些公共方法,即可能被多次调用。一般这种情况我们可以把它提取出来,对于Reactor而言,就可以使用transform()
方法调用外部定义好的Function
函数。
案例如下:
// 1.定义一个公用的Function
Function<Flux<String>, Flux<String>> filterAndMap =
f -> f.filter(color -> !color.startsWith("str"))
.map(String::toUpperCase);
// 2.定义Flux序列
Flux.fromIterable(Arrays.asList("blue", "green", "orange", "purple", "yellow", "str1", "str2"))
// 调用外部的Function
.transform(filterAndMap)
.subscribe(System.out::println);
输出结果如下:
compose()
// 1.定义一个公用的Function
AtomicInteger i = new AtomicInteger();
Function<Flux<String>, Flux<String>> function = f -> {
if (i.incrementAndGet() == 1) {
return f.filter(color -> !color.equals("str1"))
.map(String::toUpperCase);
}
return f.filter(color -> !color.equals("str2"))
.map(String::toUpperCase);
};
Flux<String> flux =
Flux.fromIterable(Arrays.asList("str1", "str2", "str3", "str4"))
// 调用外部定义好的function
.compose(function);
flux.subscribe(d -> System.out.println("订阅者1:获得数据" + d));
System.out.println("==============================================");
flux.subscribe(d -> System.out.println("订阅者2:获得数据" + d));
输出结果如下:
如果将compose()改为transform(),结果如下:
可以观察下和transform()
这个方法有什么不同:
- 我们在代码里定义了一个累加器,并且每次调用其值都会+1。
- 对于
compose()
方法,可以看到,两个订阅者最终获得的数据有偏差,而对于transform()
方法,两个订阅者获得的数据是一致的。
结论如下:
compose()
中打包的函数可以是有状态的,本案例中对应我们的AtomicInteger
对象。transform()
打包的函数是无状态的。
最后,如果这边文章对你有帮助,还望来个一键三连哈哈~