反应式Web架构SpringWebFlux详解(中)

原文链接

欢迎大家对于本站的访问 - AsterCasc

前言

在上篇反应式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的这些参数都是可以被省略的,FluxMono有处理相关函数的重载,下面给出订阅的示例代码

@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,谓词判断anyall,输入迭代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,索引过滤taketakeLasttakeUntilskipskipLast,示例如下

@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密集的工作时,最好切换到其他线程处理,一般使用publishOnsubscribeOn,示例如下

@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();
	}
}

用于判断当前序列订阅后是否可以按照指定序列输出

参考资料

Spring WebFlux Overview

Reactor Reference

RxJava Wiki

原文链接

欢迎大家对于本站的访问 - AsterCasc

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值