3.4.一切都是Handler
Handler是Vert.x中最常见的一种行为结构,也是Vert.x中为什么最小JDK支持为8.0的主要原因——没错,它使用了JDK 8.0引入的lambda语法,您在官方文档中也许看到最多的代码就如下:
从此处开始,您就打开了Handler的大门,它的格式类似JS中常用的回调函数,也是前文提到的Vert.x中启动周期和请求周期的一个分水岭,在上述示例中,内部打印语句并不会立即执行,您可以理解vertx.setPeriodic
在部署代码,部署好之后每隔一秒lambda函数内的打印语句会执行一次,这种思路充斥着整个Vert.x官方教程,如果单纯依靠这种写法也容易陷入代码中的回调地狱,本章节我会带领大家一起感受Handler的各种玩法。
使用Handler之前,我们先看看Vert.x中的基本定义,理解了基本定义后,回过头来看Handler的基本用法会更加得心应手,读者需要牢记几个思考基础:
Handler在主代码过程中通常只用于绑定(函数的延迟调用原理),并不会执行函数内部代码。
Handler的内部代码会在满足条件时执行,一般是触发式的(这点很重要,您的断点打在什么位置是最需要您理解的)。
Vert.x中大部分Handler是异步调用,读者需要区分延迟调用和异步调用,二者在某些地方是等价而在某些地方是不等价的,Vert.x中的异步调用通常会追加
AsyncResult
作数据的封装容器。
1. Handler接口
Handler的主接口定义如下:
它是一个函数式接口(@FunctionalInterface
修饰),基本接口十分简单,此处我们讨论下这个方法背后的设计。在整个Vert.x中,实现了这个接口的类不少,您可以将它理解成Vert.x框架中行为的神经系统,由于这个方法的返回值是void
,意味着它可以支持异步也可支持同步,Vert.x内部的核心系统基础实际是回调模式,您可以在该方法的实现中直接书写类似:
遵循Vert.x的黄金法则,如果您要绑定同步的Handler,则尽可能找到类似blockingHandler
的方法来绑定,该绑定内部由Vert.x执行调度,不会让您的程序出现阻塞;您若直接在handler
方法中绑定同步代码,很有可能导致线程阻塞问题;若您的执行代码已经处理过同步转异步的操作,则可无视上述法则——禁止直接绑定同步代码依然是在Vert.x开发过程中牢记的法则。
所以Handler接口的实现类大部分执行方式如下图(参考上边例子):
上图中Route对象就是编程过程中调用handler
的对象,而此时执行的绑定(并不执行Handler内部代码),即Vert.x中Web流程的启动周期;而下边触发事件时的虚线数据流才会执行Handler的内部代码,即Vert.x中Web流程的请求周期,两部分代码不是同时执行的,这一点需读者牢记。
比较有意思的是,如果调用blockingHandler
执行了同步绑定,这些看似在Route对象内部的代码最终都会执行Context
上下文对象中的同步调度块,而不在组件内部,整个Vert.x框架中的大部分组件都基于此原则——大部分调度代码最终由Context
统一完成。读者可以直接跟踪route.blockingHandler
的源代码如下:
细心的读者会发现,Vert.x整个框架中的组件在初始化时都包含了初始化组件的API函数,该函数的参数签名一般是(Vertx,JsonObject)
格式或(Vertx,Options)
格式,Vertx实例的引用就是通过它传入到组件内部,让所有组件共享的。阅读上述源代码之前,您需回顾一下两个基本概念:函数引用和函数调用:
函数引用:通常构造如:
var fn = xxx
,构造出来的fn
本身是一个函数,如Java8之后的java.util.function.Function
对象,在构造该函数时,您只是使用fn
变量获取了一个函数引用,而此时函数没有执行。函数调用:这个概念最简单,就是我们通常说的方法调用。
代码中的blockingCodeHandler
就是一个函数引用,它的类型就是本小节提到的Handler
,所以在下边这行代码之前,函数内部的绑定代码都不会运行:
如果您区分了函数引用和函数调用的概念,理解Handler就轻而易举了,最后说明一点:是否异步执行取决于函数调用时的上下文(代码块),而不是函数引用的上下文,在Vert.x环境中对比如下:
上述概念代码就是Handler的核心原理,至于blockingCodeHandler
引用函数的内部代码是同步还是异步取决于函数内部的实现代码,回到本小节最初提到的第三点:如果您已经拿到了一个函数引用,那么该函数就具备了延迟调用的功能,因为这个函数会在您触发它的时候执行(如执行handler.handle(xx)
),而触发之前环境中只是维持了该函数引用;若函数内代码本身是同步的,那么只是单纯的延迟调用,若函数内代码是异步的,那么它才是异步调用。之所以会将延迟调用和异步调用混淆,是因为二者具备极高的相似性,它们都是延迟得到执行结果,但从原理上分析,二者存在本质的区别。
2. AsyncResult接口
看完了Handler之后,我们继续屠城。
如果说Handler的设计是用来区分函数引用和函数调用,那么另外一个接口io.vertx.core.AsyncResult
就是用来区分同步调用和异步调用,该接口的定义如下:
理解接口之前先看一个图示:
上图描述了AsyncResult
接口中所有API的概念图,它是一个典型的Monad容器,容器内数据类型为T
,包含了两种状态:成功和失败,每种状态提供了判断函数以及读取数据的函数,失败时读取的数据为一个Java语言中的Throwable
对象;由于map
和otherwise
是绑定的函数,函数中实现同步或异步由开发人员自定。
参考下边的简单代码:
运行该代码您会得到如下信息输出:
上述代码中有三个例子:
第一个例子最简单,全程没有任何异常抛出,所以最终调用了
onSuccess
方法打印了最终结果,它的数据变化如:10 -> 15
,最终输出结果。第二个例子中,强制性在
addx
方法内抛出了一个异常,最终调用onFailure
方法捕捉异常发生后的情况。第三个例子中绑定了otherwize函数,并返回了同步结果
Integer
,最终调用onComplete
方法来捕捉结果。
开发人员在使用AsyncResult<T>
一定要注意此处的T
类型,在关注类型时,T
类型本身很简单的,比较复杂的是转换之后的类型。代码中的第三个例子在调用map
时绑定的是同步函数,也就意味着最终转换出来的结果类型已经是Integer
,此时如果您直接使用onSuccess
,那么都不用执行下边的检查代码:
此处根据map/otherwise
两个方法的定义来理解:
两个函数在执行过程中,不会去理睬返回值(map
中的T
,otherwise
中的U
)是一个异步结构还是一个同步结构,通常开发人员可以选择两种方式捕捉结果:
onSuccess/onFailure
配对,这种模式下函数参数就已经是内部结果T
或U
了。onComplete
,这种模式下您拿到的其实是原始结果,它并没有执行任何数据提取操作。
发现问题没?其实map
和otherwize
构造出来的实际是类似Java中lambda的map
效果,它是一个函数链,并不是字面理解的异步数据流(我们所有的例子此时都还是同步的),如果您把第一个例子中的onSuccess
改成onComplete
,您会发现res的数据结构形如AsyncResult<Future<Integer>>
,和期望的AsyncResult<Integer>
不同,这就是map
带来的效果——函数返回什么内容,那么它就解析成什么类型,并对此类型执行AsyncResult封装,至于该类型是同步还是异步,它不关心。map
和otherwize
构造了如下的一种数据结构:
所有的函数执行都会有成功和失败两种状态,而Java语言中的函数本身不支持多返回值,于是有了AsyncResult<T>
对函数执行结果的数据和状态进行封装,如此,函数即使出现异常,也只会生成一个失败状态的AsyncResult<T>
而不是以异常的方式抛出,这也遵循了函数式语言中Monad的特性,所以从这点意义上讲,AsyncResult<T>
本质就是函数式编程中最子元的Monad结构,它最大的改动就是让您的函数可以返回双态。
您也许会问,既如此,为何AsyncResult<T>
的字面名字会叫异步结果?主要原因是AsyncResult
在Vert.x中是以接口的形式定义,它的API形如Monad,而最终这个Monad的实现是同步还是异步取决于实现类,Vert.x中最常用的两个实现类是FutureImpl和PromiseImpl,这两货内部实现都是异步数据流,因此这个名字就实至名归了。
3. 再谈Future<T>/Promise<T>
Future<T>/Promise<T>
紧接前一章节提到的AsyncResult<T>
,本章我们再来温习两个可爱的小宝贝:Future和Promise,只是这次我们换个视角,从整体结构来解读Vert.x中这部分内容:
3.1. 整体结构
[I]标记是接口,[A]标记是抽象类。
从结构图上可知:
Future
的类型是前文提到的AsyncResult<T>
。Promise
的类型则是Handler<AsyncResult<T>>
。
Future<T>
接口定义中存在几个和Handler
直接相关的方法:
这三个方法最早是没有的,3.x中原始版本最常用的方法是setHandler
,该方法等价于onComplete
,从3.8
开始该方法被标记为废弃(@Deprecated
),4.x
之后就被移除了,这种设计是因为早期代码经常会是如下写法:
虽然这种写法会使得函数本身更趋近于全函数,但在开发过程中,我们的业务层面有时候只关心单边状态,如成功时如何,又或者失败时如何,如果一直使用setHandler
绑定函数,那么系统中会到处充斥着上述结构的代码,这样会显得冗余,所以取而代之提供了onSuccess
和onFailure
的绑定模式。
也就是说上述示例代码:
也可直接取消res.succeeded()
的判断,直接写成:
此处牵涉一个简单的编程思维问题:Handler是否有必要处处都检查?这个问题可根据实际情况而定,我们在编程过程中通常会有A调用B的模式,是否检查取决于函数的内部实现,如果内部实现是可信任的,即使用了类似前文提到的otherwise
方法绑定异常情况,那么就没有必要检查,而若内部实现会访问网络、数据库、文件系统等无法预知的资源,除了使用try-catch
执行Checked
异常转换,也可以让部分Runtime
的异常抛出,丢给Handler来检查(res.failed()
)。也许有人会说处处检查更完美,但完美的东西不一定实用,况且对代码本身的维护也是一种成本,所以根据分析,您就可以择优而从,因地制宜了。
3.2. 三态
前文一直提到函数执行要么成功,要么失败,此处的三态如何讲呢?其实还有一种就是Handler中同时包含了成功和失败,因为FutureBase
有三个直接子类,三者的统计如下:
由于FutureImpl类的访问域是default的,所以基本不会使用,但内部是如此设计,若您要理解Vert.x中的结构,不可越过它;在3.8.x
版本之前,Future类有一个直接构造方法Future.future()
,FutureImpl类就是为它量身打造的,而之后的版本进行了细分设计,该方法就被废弃了,取而代之的就是Promise.promise()
。开发人员在使用Future中常用的代码如:
上述两个方法会构造SucceededFuture实例和FailedFuture实例,这两个类的出现是细粒度设计的体现,二者一个职责是表示成功,另外一个职责就是表示失败,它们的区别如下:
二者构造函数不同
置留的空方法不同,由于这两个类都是单态,所以对另外一种状态会存在忽略不计的情况,若您在书写时使用了反向绑定(如SucceededFuture调用了onFailure绑定)可能什么也不会发生,这也是开发过程中容易被忽视的点。
FutureBase中有两个只有子类(protected
域)才能访问的主方法emitSuccess(T,Listener<T>)
和emitFailure(Throwable,Listener<T>)
,这两个方法是整个Future/Promise的核心逻辑,FutureBase的构造函数如下:
到此处,相信读者对下边几个方法已经有答案了:
Future什么时候是异步的,什么时候是同步的?
Future和Promise的本质是什么?
在Vert.x中,Future本身是同时支持同步和异步的,在同步模式中,它可以不和Context上下文环境产生任何关系,此种模式下可直接触发绑定的Handler实现同步转异步的操作(只是代码模式转成了异步,是否异步同样取决于绑定Handler的内部实现);若Future和Context上下文环境相关联,那么它就具备了先天性的异步特征。
Future和Promise不同的点是继承的接口,前者从AsyncResult<T>
继承,后者则从Handler<AsyncResult<T>>
继承。Future本质上是一种数据结构,您可以将它理解成异步容器,它将数据结果封装在容器内部,提供给函数链消费,而其本身就是标准的Monad结构;Promise本质上是一种行为,它在3.8.x
之前都是不存在的,它是从原版的Future中剥离出来的。——二者实则是异步数据和异步行为的一种分离设计,各司其职。
之所以一直对Future/Promise念念不忘,重复讲解,主因是二者不论在Vert.x内部还是Future风格的开发中随处可见,若您不掌握二者的用法,可能在开发过程中举步维艰。
4. 其他Handler
上述提到的Handler是整个Vert.x中的Handler主结构,Vert.x中多数API都使用了该结构,到这里,相信您再去阅读Vert.x的源码就更加轻车熟路了,我们跟着目前所见之初瞅瞅它,让读者对一切都是Handler有更深入的印象。
4.1. Vertx初始化
Vertx实例在初始化时支持两种模式:单机模式和集群模式,单机模式的启动是同步创建,集群模式则是异步实现,二者函数签名如:
于是就有了在初始化集群时的官方代码:
4.2. Verticle的部署
Vertx实例从部署Verticle开始就已经是异步部署了,部署组件没有同步模式,它的API形如:
官方有一段代码:
之所以说此处代码是小坑是因为在部署Verticle组件时,我们本身就会等待部署结果,无关部署完成后去执行此操作,Future<String>
的返回值并不代表代码本身不执行,而是随后会执行,执行完成待定。它提供了这样一种视角:一个函数返回Future
,并不代表它不会执行,而是它不会立即执行,若您不关心返回值,则无需设置对应的Handler,当然也不需要调用onSuccess/onFailure
执行绑定,等待结果即可。若您想要在执行完成后有对应操作,则必须设置Handler代码,Handler的另外一种本质就是类似JS中的回调,它采用了Callback风格的代码让您可以在异步操作执行完成之后再处理其他事情。图示如下:
图中的灰色区域是开发人员无法触碰的区域,主代码一旦调用了deployVerticle之后
,它的执行就不受主代码管控了,能等待它执行结果的地方就只有异步回调代码了,而主代码中的return
操作在此时不起任何作用,这和JS中的异步如出一辙,而灰色区域实则是开发过程中的盲区。
4.3. Verticle启动
我们一般写Verticle代码,只会去重写start()
方法,通常写法如:
而AbstractVerticle中包含了异步方法函数如下:
这里有个小细节:异步实现中只是单纯调用了start()
函数,如果该函数本身包含了异步行为,那么这个行为您可以在函数内部书写回调,Verticle组件本身无法捕捉回调内容,也就是说,Verticle的默认异步启动只是发起了异步请求,并没等待您的start()方法执行完成,若要让您的start()方法在执行完成后再标识Verticle部署完成(示例中等待监听结果),您可以重写start(Promise<Void>)
而不是start()
,代码修改如下:
如此:Verticle的部署完成就一定会发生在HttpServer监听成功之后了!所幸的是大部分情况下,部署Verticle是无需如此操作,异步部署已经是很优秀的解决办法了。配合下边主代码:
打印结果:
同步打印结果是固定的,每次打印结果都会按顺序输出,异步打印结果理论上讲由于Verticle的start()方法要耗费时间,所以Verticle Completed
先打印,但如果小概率出现了Verticle组件先执行完,那么有可能Server Started
也先打印,这点读者尤其注意,两种打印结果前者是固定顺序,后者并不是固定顺序,而是无序的,您理解了这点,那么对异步数据流就有了更深的理解。
5. 总结
本章主要讲解了Vert.x中Handler部分主体内容,也解析了其常用结构和用法,在涉入Vert.x的Web开发之前,这里属于必经之路。您若仔细去阅读Vert.x的源代码,会在整个框架内部看到很多类似Handler<AsyncResult<T>>
的定义,阅读了本章内容,那么您更容易理解Handler,面对很多API的调用以及相关写法,就不会疑惑了。
最后更新于