使用 Reactor 进行反应式编程,阿里高级java工程师面试题

Flux.range(1, 10).filter(i -> i % 2 == 0).subscribe(System.out::println);

window

window 操作符的作用类似于 buffer,所不同的是 window 操作符是把当前流中的元素收集到另外的 Flux 序列中,因此返回值类型是 Flux。在代码清单 7 中,两行语句的输出结果分别是 5 个和 2 个 UnicastProcessor 字符。这是因为 window 操作符所产生的流中包含的是 UnicastProcessor 类的对象,而 UnicastProcessor 类的 toString 方法输出的就是 UnicastProcessor 字符。

清单 7. window 操作符使用示例

Flux.range(1, 100).window(20).subscribe(System.out::println);

Flux.intervalMillis(100).windowMillis(1001).take(2).toStream().forEach(System.out::println);

zipWith

zipWith 操作符把当前流中的元素与另外一个流中的元素按照一对一的方式进行合并。在合并时可以不做任何处理,由此得到的是一个元素类型为 Tuple2 的流;也可以通过一个 BiFunction 函数对合并的元素进行处理,所得到的流的元素类型为该函数的返回值。

在代码清单 8 中,两个流中包含的元素分别是 a,b 和 c,d。第一个 zipWith 操作符没有使用合并函数,因此结果流中的元素类型为 Tuple2;第二个 zipWith 操作通过合并函数把元素类型变为 String。

清单 8. zipWith 操作符使用示例

Flux.just(“a”, “b”)

.zipWith(Flux.just(“c”, “d”))

.subscribe(System.out::println);

Flux.just(“a”, “b”)

.zipWith(Flux.just(“c”, “d”), (s1, s2) -> String.format(“%s-%s”, s1, s2))

.subscribe(System.out::println);

take

take 系列操作符用来从当前流中提取元素。提取的方式可以有很多种。

  • take(long n),take(Duration timespan)和 takeMillis(long timespan):按照指定的数量或时间间隔来提取。

  • takeLast(long n):提取流中的最后 N 个元素。

  • takeUntil(Predicate<? super T> predicate):提取元素直到 Predicate 返回 true。

  • takeWhile(Predicate<? super T> continuePredicate): 当 Predicate 返回 true 时才进行提取。

  • takeUntilOther(Publisher<?> other):提取元素直到另外一个流开始产生元素。

在代码清单 9 中,第一行语句输出的是数字 1 到 10;第二行语句输出的是数字 991 到 1000;第三行语句输出的是数字 1 到 9;第四行语句输出的是数字 1 到 10,使得 Predicate 返回 true 的元素也是包含在内的。

清单 9. take 系列操作符使用示例

Flux.range(1, 1000).take(10).subscribe(System.out::println);

Flux.range(1, 1000).takeLast(10).subscribe(System.out::println);

Flux.range(1, 1000).takeWhile(i -> i < 10).subscribe(System.out::println);

Flux.range(1, 1000).takeUntil(i -> i == 10).subscribe(System.out::println);

reduce 和 reduceWith

reduce 和 reduceWith 操作符对流中包含的所有元素进行累积操作,得到一个包含计算结果的 Mono 序列。累积操作是通过一个 BiFunction 来表示的。在操作时可以指定一个初始值。如果没有初始值,则序列的第一个元素作为初始值。

在代码清单 10 中,第一行语句对流中的元素进行相加操作,结果为 5050;第二行语句同样也是进行相加操作,不过通过一个 Supplier 给出了初始值为 100,所以结果为 5150。

清单 10. reduce 和 reduceWith 操作符使用示例

Flux.range(1, 100).reduce((x, y) -> x + y).subscribe(System.out::println);

Flux.range(1, 100).reduceWith(() -> 100, (x, y) -> x + y).subscribe(System.out::println);

merge 和 mergeSequential

merge 和 mergeSequential 操作符用来把多个流合并成一个 Flux 序列。不同之处在于 merge 按照所有流中元素的实际产生顺序来合并,而 mergeSequential 则按照所有流被订阅的顺序,以流为单位进行合并。

代码清单 11 中分别使用了 merge 和 mergeSequential 操作符。进行合并的流都是每隔 100 毫秒产生一个元素,不过第二个流中的每个元素的产生都比第一个流要延迟 50 毫秒。在使用 merge 的结果流中,来自两个流的元素是按照时间顺序交织在一起;而使用 mergeSequential 的结果流则是首先产生第一个流中的全部元素,再产生第二个流中的全部元素。

清单 11. merge 和 mergeSequential 操作符使用示例

Flux.merge(Flux.intervalMillis(0, 100).take(5), Flux.intervalMillis(50, 100).take(5))

.toStream()

.forEach(System.out::println);

Flux.mergeSequential(Flux.intervalMillis(0, 100).take(5), Flux.intervalMillis(50, 100).take(5))

.toStream()

.forEach(System.out::println);

flatMap 和 flatMapSequential

flatMap 和 flatMapSequential 操作符把流中的每个元素转换成一个流,再把所有流中的元素进行合并。flatMapSequential 和 flatMap 之间的区别与 mergeSequential 和 merge 之间的区别是一样的。

在代码清单 12 中,流中的元素被转换成每隔 100 毫秒产生的数量不同的流,再进行合并。由于第一个流中包含的元素数量较少,所以在结果流中一开始是两个流的元素交织在一起,然后就只有第二个流中的元素。

清单 12. flatMap 操作符使用示例

Flux.just(5, 10)

.flatMap(x -> Flux.intervalMillis(x * 10, 100).take(x))

.toStream()

.forEach(System.out::println);

concatMap

concatMap 操作符的作用也是把流中的每个元素转换成一个流,再把所有流进行合并。与 flatMap 不同的是,concatMap 会根据原始流中的元素顺序依次把转换之后的流进行合并;与 flatMapSequential 不同的是,concatMap 对转换之后的流的订阅是动态进行的,而 flatMapSequential 在合并之前就已经订阅了所有的流。

代码清单 13 与代码清单 12 类似,只不过把 flatMap 换成了 concatMap,结果流中依次包含了第一个流和第二个流中的全部元素。

清单 13. concatMap 操作符使用示例

Flux.just(5, 10)

.concatMap(x -> Flux.intervalMillis(x * 10, 100).take(x))

.toStream()

.forEach(System.out::println);

combineLatest

combineLatest 操作符把所有流中的最新产生的元素合并成一个新的元素,作为返回结果流中的元素。只要其中任何一个流中产生了新的元素,合并操作就会被执行一次,结果流中就会产生新的元素。在 代码清单 14 中,流中最新产生的元素会被收集到一个数组中,通过 Arrays.toString 方法来把数组转换成 String。

清单 14. combineLatest 操作符使用示例

Flux.combineLatest(

Arrays::toString,

Flux.intervalMillis(100).take(5),

Flux.intervalMillis(50, 100).take(5)

).toStream().forEach(System.out::println);

消息处理


当需要处理 Flux 或 Mono 中的消息时,如之前的代码清单所示,可以通过 subscribe 方法来添加相应的订阅逻辑。在调用 subscribe 方法时可以指定需要处理的消息类型。可以只处理其中包含的正常消息,也可以同时处理错误消息和完成消息。代码清单 15 中通过 subscribe()方法同时处理了正常消息和错误消息。

清单 15. 通过 subscribe()方法处理正常和错误消息

Flux.just(1, 2)

.concatWith(Mono.error(new IllegalStateException()))

.subscribe(System.out::println, System.err::println);

正常的消息处理相对简单。当出现错误时,有多种不同的处理策略。第一种策略是通过 onErrorReturn()方法返回一个默认值。在代码清单 16 中,当出现错误时,流会产生默认值 0.

清单 16. 出现错误时返回默认值

Flux.just(1, 2)

.concatWith(Mono.error(new IllegalStateException()))

.onErrorReturn(0)

.subscribe(System.out::println);

第二种策略是通过 switchOnError()方法来使用另外的流来产生元素。在代码清单 17 中,当出现错误时,将产生 Mono.just(0)对应的流,也就是数字 0。

清单 17. 出现错误时使用另外的流

Flux.just(1, 2)

.concatWith(Mono.error(new IllegalStateException()))

.switchOnError(Mono.just(0))

.subscribe(System.out::println);

第三种策略是通过 onErrorResumeWith()方法来根据不同的异常类型来选择要使用的产生元素的流。在代码清单 18 中,根据异常类型来返回不同的流作为出现错误时的数据来源。因为异常的类型为 IllegalArgumentException,所产生的元素为-1。

清单 18. 出现错误时根据异常类型来选择流

Flux.just(1, 2)

.concatWith(Mono.error(new IllegalArgumentException()))

.onErrorResumeWith(e -> {

if (e instanceof IllegalStateException) {

return Mono.just(0);

} else if (e instanceof IllegalArgumentException) {

return Mono.just(-1);

}

return Mono.empty();

})

.subscribe(System.out::println);

当出现错误时,还可以通过 retry 操作符来进行重试。重试的动作是通过重新订阅序列来实现的。在使用 retry 操作符时可以指定重试的次数。代码清单 19 中指定了重试次数为 1,所输出的结果是 1,2,1,2 和错误信息。

清单 19. 使用 retry 操作符进行重试

Flux.just(1, 2)

.concatWith(Mono.error(new IllegalStateException()))

.retry(1)

.subscribe(System.out::println);

调度器


前面介绍了反应式流和在其上可以进行的各种操作,通过调度器(Scheduler)可以指定这些操作执行的方式和所在的线程。有下面几种不同的调度器实现。

  • 当前线程,通过 Schedulers.immediate()方法来创建。

  • 单一的可复用的线程,通过 Schedulers.single()方法来创建。

  • 使用弹性的线程池,通过 Schedulers.elastic()方法来创建。线程池中的线程是可以复用的。当所需要时,新的线程会被创建。如果一个线程闲置太长时间,则会被销毁。该调度器适用于 I/O 操作相关的流的处理。

  • 使用对并行操作优化的线程池,通过 Schedulers.parallel()方法来创建。其中的线程数量取决于 CPU 的核的数量。该调度器适用于计算密集型的流的处理。

  • 使用支持任务调度的调度器,通过 Schedulers.timer()方法来创建。

  • 从已有的 ExecutorService 对象中创建调度器,通过 Schedulers.fromExecutorService()方法来创建。

某些操作符默认就已经使用了特定类型的调度器。比如 intervalMillis()方法创建的流就使用了由 Schedulers.timer()创建的调度器。通过 publishOn()和 subscribeOn()方法可以切换执行操作的调度器。其中 publishOn()方法切换的是操作符的执行方式,而 subscribeOn()方法切换的是产生流中元素时的执行方式。

在代码清单 20 中,使用 create()方法创建一个新的 Flux 对象,其中包含唯一的元素是当前线程的名称。接着是两对 publishOn()和 map()方法,其作用是先切换执行时的调度器,再把当前的线程名称作为前缀添加。最后通过 subscribeOn()方法来改变流产生时的执行方式。运行之后的结果是[elastic-2] [single-1] parallel-1。最内层的线程名字 parallel-1 来自产生流中元素时使用的 Schedulers.parallel()调度器,中间的线程名称 single-1 来自第一个 map 操作之前的 Schedulers.single()调度器,最外层的线程名字 elastic-2 来自第二个 map 操作之前的 Schedulers.elastic()调度器。

清单 20. 使用调度器切换操作符执行方式

Flux.create(sink -> {

sink.next(Thread.currentThread().getName());

sink.complete();

})

.publishOn(Schedulers.single())

.map(x -> String.format(“[%s] %s”, Thread.currentThread().getName(), x))

.publishOn(Schedulers.elastic())

.map(x -> String.format(“[%s] %s”, Thread.currentThread().getName(), x))

.subscribeOn(Schedulers.parallel())

.toStream()

.forEach(System.out::println);

测试


在对使用 Reactor 的代码进行测试时,需要用到 io.projectreactor.addons:reactor-test 库。

使用 StepVerifier

进行测试时的一个典型的场景是对于一个序列,验证其中所包含的元素是否符合预期。StepVerifier 的作用是可以对序列中包含的元素进行逐一验证。在代码清单 21 中,需要验证的流中包含 a 和 b 两个元素。通过 StepVerifier.create()方法对一个流进行包装之后再进行验证。expectNext()方法用来声明测试时所期待的流中的下一个元素的值,而 verifyComplete()方法则验证流是否正常结束。类似的方法还有 verifyError()来验证流由于错误而终止。

清单 21. 使用 StepVerifier 验证流中的元素

StepVerifier.create(Flux.just(“a”, “b”))

.expectNext(“a”)

.expectNext(“b”)

.verifyComplete();

操作测试时间

有些序列的生成是有时间要求的,比如每隔 1 分钟才产生一个新的元素。在进行测试中,不可能花费实际的时间来等待每个元素的生成。此时需要用到 StepVerifier 提供的虚拟时间功能。通过 StepVerifier.withVirtualTime()方法可以创建出使用虚拟时钟的 StepVerifier。通过 thenAwait(Duration)方法可以让虚拟时钟前进。

在代码清单 22 中,需要验证的流中包含两个产生间隔为一天的元素,并且第一个元素的产生延迟是 4 个小时。在通过 StepVerifier.withVirtualTime()方法包装流之后,expectNoEvent()方法用来验证在 4 个小时之内没有任何消息产生,然后验证第一个元素 0 产生;接着 thenAwait()方法来让虚拟时钟前进一天,然后验证第二个元素 1 产生;最后验证流正常结束。

清单 22. 操作测试时间

StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofHours(4), Duration.ofDays(1)).take(2))

.expectSubscription()

.expectNoEvent(Duration.ofHours(4))

.expectNext(0L)

.thenAwait(Duration.ofDays(1))

.expectNext(1L)

.verifyComplete();

使用 TestPublisher

TestPublisher 的作用在于可以控制流中元素的产生,甚至是违反反应流规范的情况。在代码清单 23 中,通过 create()方法创建一个新的 TestPublisher 对象,然后使用 next()方法来产生元素,使用 complete()方法来结束流。TestPublisher 主要用来测试开发人员自己创建的操作符。

清单 23. 使用 TestPublisher 创建测试所用的流

final TestPublisher testPublisher = TestPublisher.create();

testPublisher.next(“a”);

testPublisher.next(“b”);

testPublisher.complete();

StepVerifier.create(testPublisher)

.expectNext(“a”)

.expectNext(“b”)

.expectComplete();

调试


由于反应式编程范式与传统编程范式的差异性,使用 Reactor 编写的代码在出现问题时比较难进行调试。为了更好的帮助开发人员进行调试,Reactor 提供了相应的辅助功能。

启用调试模式

当需要获取更多与流相关的执行信息时,可以在程序开始的地方添加代码清单 24 中的代码来启用调试模式。在调试模式启用之后,所有的操作符在执行时都会保存额外的与执行链相关的信息。当出现错误时,这些信息会被作为异常堆栈信息的一部分输出。通过这些信息可以分析出具体是在哪个操作符的执行中出现了问题。

清单 24. 启用调试模式

Hooks.onOperator(providedHook -> providedHook.operatorStacktrace());

不过当调试模式启用之后,记录这些额外的信息是有代价的。一般只有在出现了错误之后,再考虑启用调试模式。但是当为了找到问题而启用了调试模式之后,之前的错误不一定能很容易重现出来。为了减少可能的开销,可以限制只对特定类型的操作符启用调试模式。

使用检查点

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

面试题文档来啦,内容很多,485页!

由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。

1111道Java工程师必问面试题

MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:

Elasticsearch 24 题 +Memcached + Redis 40题:

Spring 26 题+ 微服务 27题+ Linux 45题:

Java面试题合集:

%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-cUlKdaB4-1712004690871)]

最后

面试题文档来啦,内容很多,485页!

由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。

1111道Java工程师必问面试题

[外链图片转存中…(img-MoyxhEsu-1712004690871)]

MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:

[外链图片转存中…(img-kZ30vh9o-1712004690871)]

Elasticsearch 24 题 +Memcached + Redis 40题:

[外链图片转存中…(img-m7TaU3MZ-1712004690872)]

Spring 26 题+ 微服务 27题+ Linux 45题:

[外链图片转存中…(img-Rilg03eR-1712004690872)]

Java面试题合集:

[外链图片转存中…(img-zYqcTmwI-1712004690872)]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值