原文链接
前言
在上篇反应式Web架构SpringWebFlux详解(上)中,我们主要了解了反应式架构的基本原理以及和传统命令式比较和选择,本篇常用情景下展示WebFlux
架构的具体相关代码以及测试方法,下篇介绍Reactive
的一些进阶用法
有关于和数据库的相关交互参考本站SpringReactive下的数据库交互,这里不再介绍反应式框架数据库交互相关内容
常用语法和使用
Mono&Flux
Mono
用于创建01的反应序列,而Flux
用于创建0N的反应序列,创建序列后,这些序列为发布者即Publisher<T>
。我们亦可以从源码中观察到,这两个类由CorePublisher<T>
实现,而CorePublisher<T>
实现Publisher<T>
public abstract class Mono<T> implements CorePublisher<T> {
//...
}
public interface CorePublisher<T> extends Publisher<T> {
void subscribe(CoreSubscriber<? super T> subscriber);
}
public interface Publisher<T> {
void subscribe(Subscriber<? super T> var1);
}
当然也有其他发布者类,但是核心基本都是Mono
或者是Flux
的变种
public abstract class CloudFlux<T> extends Flux<T> {
public CloudFlux() {
}
@SafeVarargs
public static <I> Flux<I> firstNonEmpty(Publisher<? extends I>... sources) {
return onAssembly(new FluxFirstNonEmptyEmitting(sources));
}
public static <I> Flux<I> firstNonEmpty(Iterable<? extends Publisher<? extends I>> sources) {
return onAssembly(new FluxFirstNonEmptyEmitting(sources));
}
final class FluxArray<T> extends Flux<T> implements Fuseable, SourceProducer<T> {
//...
}
创建和订阅序列
由前篇可知,我们所有的发布者需要工作,必须要有订阅者,所以一般的反应式的流程如下
创建
public record DemoOutput(String name, Integer age, String desc) {}
Mono
@Service
public class DemoServiceImpl implements IDemoService {
@Override
public Mono<DemoOutput> monoTest() {
// 1. 创建空序列
Mono<DemoOutput> mono1 = Mono.empty();
// 2. 直接创建序列
var mono2 = Mono.just(new DemoOutput("张三", 18, "法外狂徒"));
// todo subscribe
return null;
}
}
Flux
@Service
public class DemoServiceImpl implements IDemoService {
@Override
public Flux<DemoOutput> fluxTest() {
// 1. 创建空序列
Flux<DemoOutput> flux1 = Flux.empty();
// 2. 直接创建
var flux2 = Flux.just(new DemoOutput("响当当", 20, "少林妖女。少林派?当当的玩具罢了"),
new DemoOutput("响叮叮", 20, "百花谷主夫人。不管谁是谷主她都是夫人"));
// 3. 迭代创建
var flux3 = Flux.fromIterable(List.of(new DemoOutput("响当当", 20, "少林妖女。少林派?当当的玩具罢了"),
new DemoOutput("响叮叮", 20, "百花谷主夫人。不管谁是谷主她都是夫人")));
// todo subscribe
return null;
}
}
当然,我们这可以可以直接return
创建的序列,将订阅交给封装的http
请求
这里创建序列的方法有很多,更多可以参考Flux
以及Mono
的其他静态方法,以及Publisher<T>
其他的实现
编程序列创建
以上创建都是基于稳定的参数创建序列,那么如果条件不稳定时,如何控制条件/循环编程式地创建序列呢,比如我们需要根据传入对象类型生成指定的递增序列,以下是示例代码
@Service
public class DemoServiceImpl implements IDemoService {
public void programmaticallyCreatingDemo() {
//Object obj = 1;Object obj = 1.0;Object obj = "1";
Flux.generate(
() -> 0,
(status, sink) -> {
sink.next(
switch (obj) {
case Integer in -> in + status;
case Double dou -> dou + status;
case String str -> new StringBuffer(str).append(status);
default -> 0;
}
);
if (status++ > 3) {
sink.complete();
}
return status;
})
.subscribe(System.out::println);
}
}
在使用generate
创建时,是使用同步方式一对一地传递,那么如果我们希望异步创建的话,比如多个线程向特定流传递序列,一般使用create
创建。可以观察到create
的创建参数不再是SynchronousSink<T>
而是FluxSink<T>
,说明它可以异步注入序列的,示例如下
public class MyFluxSink implements Consumer<FluxSink<Integer>> {
private FluxSink<Integer> mySink;
@Override
public void accept(FluxSink<Integer> integerFluxSink) {
this.mySink = integerFluxSink;
}
public void publish(Integer data) {
this.mySink.next(data);
}
}
我们需要一个Comsumer<FluxSink<T>>
的实现类,用于多线程异步注入序列。mySink
存储已经注册到Flux.create
的对象示例,方便后续调用对象注入
@Service
public class DemoServiceImpl implements IDemoService {
public void programmaticallyCreatingDemo() {
//创建注入序列消费对象
var myFluxSink = new MyFluxSink();
//注册对象到指定序列
Flux.create(myFluxSink).subscribe(System.out::println);
//多线程写入
var service = Executors.newCachedThreadPool();
for (int count : List.of(11, 12, 13, 14, 15)) {
service.execute(() -> myFluxSink.publish(count));
}
service.shutdown();
//11 14 12 13 15
}
}
订阅
订阅一般函数是使用
public final Disposable subscribe(
@Nullable Consumer<? super T> consumer, //消费者处理
@Nullable Consumer<? super Throwable> errorConsumer, //发生异常或者错误的消费处理
@Nullable Runnable completeConsumer, //处理完成后执行
@Nullable Context initialContext
) {
//...
}
关于initialContext
本来应该是在下一篇文章中聊的,这里既然用到了就提一嘴。一般命令式编程的结构是将请求绑定在线程上,那么该请求的生命周期中需要用到数据我们就可以ThreadLocal
来处理,因为这里请求生命周期中留存数据和线程的上下文是重合的。但是对于反应式来说,一个线程是很可能会服务多个请求的,此时就没办法使用ThreadLocal
来处理了
从reactor 3.1.0
之后,我们可以使用Context
来处理这种情况,可以认为是反应序列处理中的ThreadLocal
,而initialContext
参数可以间加入一些初始的上下文数据,简单用例比如
flux.subscribe(
System.out::println,
System.out::println,
() -> {},
Context.of("key", "value")
);
当然,subscribe
的这些参数都是可以被省略的,Flux
和Mono
有处理相关函数的重载,下面给出订阅的示例代码
@Service
public class DemoServiceImpl implements IDemoService {
@Override
public void subscribeTest() {
Flux.range(0, 5)
.flatMap(
num -> Mono.deferContextual(ctxView ->
Mono.just(num + Integer.parseInt(ctxView.get("key"))))
)
.map(num -> {
if (num < 102) {
return num;
} else {
return num - 100;
// throw new RuntimeException("Exception ge 102");
}
}).subscribe(
System.out::println,
ex -> System.out.println(ex.getMessage()),
() -> System.out.println("Finish"),
Context.of("key", "100")
);
}
}
此时的输出为
100
101
2
3
4
Finish
当改为抛出异常时,输出为
100
101
Exception ge 102
当然你这里也可以不使用subscribe
进行订阅的回调处理,可以选择继承BaseSubscriber
进行订阅处理,这样会让你的代码更加符合设计模式,传入的不再是实现而是抽象(依赖倒置原则),使用上也很简单,比如自定义的处理的继承子类名为DemoBaseSubscriber
var testSub = new DemoBaseSubscriber<Integer>();
var testFlux = Flux.range(0, 5);
testFlux.subscribe(testSub);
序列处理函数
转换序列
map
&flatMap
,虽然都是做的数据对应转换,但是map
返回的是? extends V
数据,而flatMap
返回的是? extends Publisher<? extends R>
订阅,也就是说flatMap
拥有重新生成序列的能力,以拆分为例,示例如下
@Service
public class DemoServiceImpl implements IDemoService {
public void convertDemo() {
var mapDemo = Flux.just("some", "data")
.map(String::toUpperCase)
.subscribe(System.out::println);
//SOME
//DATA
var flatmapDemo = Flux.just("some", "data")
.flatMap(str -> Flux.just(str.toUpperCase().split("")))
.subscribe(chara -> System.out.print(chara + " "));
//S O M E D A T A
}
}
添加元素,startWith
&concatWithValues
@Service
public class DemoServiceImpl implements IDemoService {
public void convertDemo() {
var addDemo = Flux.range(0, 5)
.startWith(-3, -2, -1)
.concatWithValues(5, 6, 7)
.subscribe(num -> System.out.print(num + " "));
//-3 -2 -1 0 1 2 3 4 5 6 7
}
}
聚合,collectList
&collectSortedList
&collectMap
&collectSortMap
聚合Flux<T>
为Mono<List<T>>
或者Mono<Map<T>>
,数量count
,谓词判断any
,all
,输入迭代reduce
,这里仅展示reduce
的示例,其他都比较简单,reduce
的输入为BiFunction<T, T, T> aggregator
,是一个二元同类型参数返回值的函数,一般用作对象聚合,操作步骤为将首个元素和第二个元执行聚合方法合生成的同种元素再和第三个执行聚合方法,依序迭代
@Service
public class DemoServiceImpl implements IDemoService {
public void convertDemo() {
var reduceDemo = Flux.range(0, 5)
.reduce((x,y) -> x + y)
.subscribe(System.out::println);
//10
}
}
过滤序列
常用操作有:谓词过滤filter
,类型过滤ofType
,索引过滤take
、takeLast
、takeUntil
、skip
、skipLast
,示例如下
@Service
public class DemoServiceImpl implements IDemoService {
public void filterDemo() {
Flux.just(0, 1, 2, 3, "some", 3.5, 4, 5)
.filter(obj -> obj instanceof Integer num && num > 2)
.doOnComplete(System.out::println)
.subscribe(obj -> System.out.print(obj + " "));
//3 4 5
Flux.just(0, 1, 2, 3, "some", 3.5, 4, 5)
.take(7)
.takeLast(5)
.doOnComplete(System.out::println)
.subscribe(obj -> System.out.print(obj + " "));
//2 3 some 3.5 4
Flux.just(0, 1, 2, 3, "some", 3.5, 4, 5)
.takeUntil(obj -> obj instanceof Double)
.doOnComplete(System.out::println)
.subscribe(obj -> System.out.print(obj + " "));
//0 1 2 3 some 3.5
Flux.just(0, 1, 2, 3, "some", 3.5, 4, 5)
.takeLast(2)
.doOnComplete(System.out::println)
.subscribe(obj -> System.out.print(obj + " "));
//4 5
Flux.just(0, 1, 2, 3, "some", 3.5, 4, 5)
.skip(2)
.skipLast(2)
.doOnComplete(System.out::println)
.subscribe(obj -> System.out.print(obj + " "));
//2 3 some 3.5
}
}
分割序列
分割序列主要针对Flux
,一般使用普通窗口或者谓词窗口,窗口window
,谓词窗口windowUntil
,或者直接使用gourpBy
进行属性分组,示例代码如下
@Service
public class DemoServiceImpl implements IDemoService {
public void SplittingDemo() {
Flux.range(0, 20)
.window(10)
.map(
integerFlux -> {
integerFlux.doFirst(() -> System.out.println("inter start"))
.map(integer -> integer + 100)
.doOnComplete(() -> System.out.println("inter finish"))
.subscribe(num -> System.out.print(num + " "));
return integerFlux;
}
)
.doOnSubscribe(onSubscribe -> System.out.println("subscribe outer finish"))
.doFinally(onFinally -> System.out.println("outer finally"))
.subscribe();
//subscribe outer finish
//inter start
//100 101 102 103 104 105 106 107 108 109 inter finish
//inter start
//110 111 112 113 114 115 116 117 118 119 inter finish
//outer finally
Flux.just(
new DemoOutput("响当当", 20, "少林妖女。少林派?当当的玩具罢了"),
new DemoOutput("响当当", 20, "“放心,我会好好照顾漆雕蝉的”"),
new DemoOutput("响叮叮", 20, "百花谷主夫人。不管谁是谷主她都是夫人"),
new DemoOutput("响叮叮", 20, "“欸?你这太吾村的穷鬼就不要来参加婚礼了”")
).groupBy(DemoOutput::name)
.map(
groupFlux -> {
groupFlux.doFirst(() -> System.out.println("inter start " + groupFlux.key()))
.subscribe(demoOutput -> System.out.println(demoOutput.desc()));
return groupFlux;
}
).doOnSubscribe(onSubscribe -> System.out.println("subscribe outer finish"))
.doFinally(onFinally -> System.out.println("outer finally"))
.subscribe();
//subscribe outer finish
//inter start 响当当
//少林妖女。少林派?当当的玩具罢了
//“放心,我会好好照顾漆雕蝉的”
//inter start 响叮叮
//百花谷主夫人。不管谁是谷主她都是夫人
//“欸?你这太吾村的穷鬼就不要来参加婚礼了”
//outer finally
}
}
序列附加操作
附加操作不会改变原本的序列结构,一般用于信号感知和发送,序列附加操作常用的一般有:单元素处理前操作doOnNext
,序列完成时操作doOnComplete
,序列内部错误操作doOnError
,序列取消操作doOnCancel
,序列开始操作doFirst
,序列成功订阅操作doOnSubscribe
,任何终止操作(完成、错误、取消)doFinally
,示例如下
@Service
public class DemoServiceImpl implements IDemoService {
public void additionalDemo() {
var fluxDemo = Flux.range(0, 3)
.doOnNext(num -> System.out.println("current num is " + num))
.map(num -> ++num)
.doOnSubscribe(onSubscribe -> System.out.println("subscribe finish"))
.doOnComplete(() -> System.out.println("completed"))
.doFirst(() -> System.out.println("start"))
.subscribe(System.out::println);
//start
//subscribe finish
//current num is 0
//1
//current num is 1
//2
//current num is 2
//3
//completed
}
}
序列线程操作
我们在序列处理中使用了阻塞的函数的时候,比如Thread::sleep
,此时编辑器一般会提示你Possibly blocking call in non-blocking context could lead to thread starvation
,我们最好需要将阻塞的操作放入其他线程中处理,否则其他资源会处于饥饿状态。所以在序列处理中,如果我们需要进行阻塞性或者CPU
密集的工作时,最好切换到其他线程处理,一般使用publishOn
和subscribeOn
,示例如下
@Service
public class DemoServiceImpl implements IDemoService {
public void threadDemo() {
System.out.println(Thread.currentThread().getName());
var flux = Flux.range(1, 3)
.map(i -> {
System.out.println(Thread.currentThread().getName());
return 10 + i;
})
// .publishOn(Schedulers.boundedElastic())
.map(i -> {
try {
Thread.sleep(1000);
} catch (Exception ignore) {
}
System.out.println(Thread.currentThread().getName());
return "value " + i;
});
var service = Executors.newCachedThreadPool();
service.execute(() -> {
System.out.println("Main Thread: " + Thread.currentThread().getName());
flux.subscribe( data -> System.out.println("Finish: " + data));
});
service.shutdown();
}
}
此时的输出为:
http-nio-8005-exec-1
Main Thread: pool-3-thread-1
pool-3-thread-1
pool-3-thread-1
Finish: value 11
pool-3-thread-1
pool-3-thread-1
Finish: value 12
pool-3-thread-1
pool-3-thread-1
Finish: value 13
如果开放publishOn
,此时的输出为:
http-nio-8005-exec-1
Main Thread: pool-5-thread-1
pool-5-thread-1
pool-5-thread-1
pool-5-thread-1
boundedElastic-1
Finish: value 11
boundedElastic-1
Finish: value 12
boundedElastic-1
Finish: value 13
pool-5-thread-1
被释放,就不会由于当前阻塞操作导致资源饥饿的情况发生,如果将publishOn
改为subscribeOn
,那么意味着从订阅开始就会改变线程,如果后续不使用publishiOn
进行再次切换,那么整个序列处理都为位于该线程中,此时输出为:
http-nio-8005-exec-1
Main Thread: pool-5-thread-1
boundedElastic-1
boundedElastic-1
Finish: value 11
boundedElastic-1
boundedElastic-1
Finish: value 12
boundedElastic-1
boundedElastic-1
Finish: value 13
更多
更多函数参考Reactor Reference Which Operator
自动化测试
和Assert
使用类似,引入测试包:
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
class UshioApplicationTests {
@Test
void contextLoads() {
var testFlux = Flux.just("data1", "data2", 1)
.concatWith(Mono.error(new RuntimeException("run")));
StepVerifier.create(testFlux)
.expectNext("data1")
.expectNext("data2")
.expectNext(1)
.expectErrorMessage("run")
.verify();
}
}
用于判断当前序列订阅后是否可以按照指定序列输出