Java中的反应式编程

Posted 雨夜随笔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的反应式编程相关的知识,希望对你有一定的参考价值。

反应式编程(Reactive Programming,又称为响应式编程。本文统称为反应式编程)这种新的编程范式正在逐渐走进开发人员的视野中,也越来越受到开发人员的欢迎。不同于传统的编程方式,反应式编程提供了很多新颖的设计,并为真正的并发编程提供了便利。

反应式编程


反应式编程来源于数据流和变化的传播,意味着由底层的执行模型负责通过数据流来自动传播变化。比如求值一个简单的表达式 c=a+b,当 a 或者 b 的值发生变化时,传统的编程范式需要对 a+b 进行重新计算来得到 c 的值。如果使用反应式编程,当 a 或者 b 的值发生变化时,c 的值会自动更新。


反应式编程最早由 .NET 平台上的 Reactive Extensions (Rx) 库来实现。后来迁移到 Java 平台之后就产生了著名的 RxJava 库,并产生了很多其他编程语言上的对应实现。在这些实现的基础上产生了后来的反应式流(Reactive Streams)规范。该规范定义了反应式流的相关接口,并将集成到 Java 9 中。


在传统的编程范式中,我们一般通过迭代器(Iterator)模式来遍历一个序列。这种遍历方式是由调用者来控制节奏的,采用的是拉的方式。每次由调用者通过 next()方法来获取序列中的下一个值。使用反应式流时采用的则是推的方式,即常见的发布者-订阅者模式。当发布者有新的数据产生时,这些数据会被推送到订阅者来进行处理。在反应式流上可以添加各种不同的操作来对数据进行处理,形成数据处理链。这个以声明式的方式添加的处理链只在订阅者进行订阅操作时才会真正执行。


反应式编程在设计上遵从了如下的规范:

  • 反应式流必须是无阻塞的

  • 反应式流必须是一个数据流

  • 必须可以异步执行

  • 必须可以处理负压(参考下面的说明)



负压




反应式流中第一个重要概念是负压(backpressure)。在基本的消息推送模式中,当消息发布者产生数据的速度过快时,会使得消息订阅者的处理速度无法跟上产生的速度,从而给订阅者造成很大的压力。当压力过大时,有可能造成订阅者本身的奔溃,所产生的级联效应甚至可能造成整个系统的瘫痪。


负压的作用在于提供一种从订阅者到生产者的反馈渠道。订阅者可以通过 request()方法来声明其一次所能处理的消息数量,而生产者就只会产生相应数量的消息,直到下一次 request()方法调用。这实际上变成了推拉结合的模式。


Reactor


前面提到的 RxJava 库是 JVM 上反应式编程的先驱,也是反应式流规范的基础。RxJava 2 在 RxJava 的基础上做了很多的更新。不过 RxJava 库也有其不足的地方。RxJava 产生于反应式流规范之前,虽然可以和反应式流的接口进行转换,但是由于底层实现的原因,使用起来并不是很直观。RxJava 2 在设计和实现时考虑到了与规范的整合,不过为了保持与 RxJava 的兼容性,很多地方在使用时也并不直观。Reactor 则是完全基于反应式流规范设计和实现的库,没有 RxJava 那样的历史包袱,在使用上更加的直观易懂。Reactor 也是 Spring 5 中反应式编程的基础。学习和掌握 Reactor 可以更好地理解 Spring 5 中的相关概念。



基本数据类型




Reactor为了能够产生上面所说的反应式流(数据流),提供了两种基本的数据类型来实现,分别是Flux和Mono。


Flux


Flux是可以产生0到N个元素的异步数据流。如下图所示,Flux可以弹出数据进行操作,处理的结果也会组成一个新的数据流。最终被正常或者异常终止。



Mono


Mono是可以产生0到1个元素的异步数据流。如下图所示,和Flux数据流一样,Mono数据流中的数据可以被处理,也会正常或异常终止。




常见问题




  • 为什么还需要Mono,只有Flux不就可以了么?


事实上是可以的,但是很多操作不太方便,我们以常规编程来举例:



public List<T> findAll()     

public List<T> findFirst()  

public T findFirst()


我们可以发现第二种用法很奇怪,因为我们知道返回的元素肯定是一个或者没有。所以我们一般采用第三种用法。反应式编程也是同样的道理,我们是可以只使用Flux,但是很多操作我们知道只返回一个或者没有,而且反应式编程也为Mono提供了很多独有的用法。所以我们使用Mono是为了方便我们的编程。


  • 为什么需要反应式编程?


这个问题是很多人了解反应式编程会遇到的问题,因为在很多人看来反应式编程能做的事情常规编程也能做到。为什么还要反应式编程?


我们要理解反应式编程是为了解决什么问题,知道了这个核心我们就可以理解其设计思路。反应式编程的目的是为了方便创建异步编程。异步的概念就是请求的结果并不会立即返回,而是会通过回调或者推的方式来实现。而反应式编程就是采取了推的方式。而反应式编程为了方便这个"推"的效果,采取了数据流的设计,也就是所有的代码都是在定义数据的流向,当你的代码完成后,数据的流向也就确定了。那么当数据产生后,就会按照制定的流程流向订阅者。


反应式编程从设计上摒弃了原有的历史负担,为异步编程提供了很多开箱即用的功能。随着异步并行编程的复杂度增加,原有的编程方式变得愈加复杂而且难以理解和维护。而反应式编程由于采用声明式编程,代码的复杂度得到大幅的降低,无论从阅读还是维护反应式编程都具有很大的优势。


  • 反应式编程如何理解


很多学习了反应式编程后,也理解了反应式编程的概念,但是在自己动手写代码的时候却往往不尽人意,不是写成了传统编程的方式,就是出现各种各样的问题。比如数据收不到或者提前结束等异常情况。


这里就是我们对反应式编程的核心没有掌握,像上面所说的,我们在写反应式编程的时候不要像传统编程那样,过于注重其中的处理逻辑。而是要把目光聚焦在如何制定数据的流向,我们的代码目的是编织好数据的流向。一旦确立好具体的数据流,那么数据就会按照我们理解的方式进行。


总结


我们这里简单了解了反应式编程和Java目前的最佳实践Reactor。本文不作具体的教程解析,这些将在后续的文章中一一描述。我们首先要有这个概念,这样无论以后遇到或者用到这方面的知识,能够大致知道反应式编程是什么就是本文的目的。



以上是关于Java中的反应式编程的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——JS中的面向对象编程

VSCode自定义代码片段9——JS中的面向对象编程

Spring5-Reactor函数式编程

Spring5-Reactor函数式编程

套接字编程中的Java拖放问题

Day718. 反应式编程 -Java8后最重要新特性