3.4.一切都是Handler

    Handler是Vert.x中最常见的一种行为结构,也是Vert.x中为什么最小JDK支持为8.0的主要原因——没错,它使用了JDK 8.0引入的lambda语法,您在官方文档中也许看到最多的代码就如下:

vertx.setPeriodic(1000, id -> {
  // This handler will get called every second
  System.out.println("timer fired!");
});

    从此处开始,您就打开了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
public interface Handler<E> {

  /**
   * Something has happened, so handle it.
   *
   * @param event  the event to handle
   */
  void handle(E event);
}

    它是一个函数式接口(@FunctionalInterface修饰),基本接口十分简单,此处我们讨论下这个方法背后的设计。在整个Vert.x中,实现了这个接口的类不少,您可以将它理解成Vert.x框架中行为的神经系统,由于这个方法的返回值是void,意味着它可以支持异步也可支持同步,Vert.x内部的核心系统基础实际是回调模式,您可以在该方法的实现中直接书写类似:

    @Override
    public void mount(final Route route, final RRecord record) {
        route.handler(res -> {
            // 异步回调:Handler内部代码,该代码无返回值
        });
        route.blockingHandler(res -> {
            // 同步回调
        })
    }

    遵循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的源代码如下:

    // 代码文件:io.vertx.core.impl.ContextImpl
    PoolMetrics metrics = workerPool.metrics();
    Object queueMetric = metrics != null ? metrics.submitted() : null;
    Promise<T> promise = context.promise();
    Future<T> fut = promise.future();
    try {
      Runnable command = () -> {
        Object execMetric = null;
        if (metrics != null) {
          execMetric = metrics.begin(queueMetric);
        }
        context.dispatch(promise, f -> {
          try {
            blockingCodeHandler.handle(promise);
          } catch (Throwable e) {
            promise.tryFail(e);
          }
        });
        if (metrics != null) {
          metrics.end(execMetric, fut.succeeded());
        }
      };
      Executor exec = workerPool.executor();
      if (queue != null) {
        queue.execute(command, exec);
      } else {
        exec.execute(command);
      }
    } catch (RejectedExecutionException e) {
      // Pool is already shut down
      if (metrics != null) {
        metrics.rejected(queueMetric);
      }
      throw e;
    }
    return fut;

    细心的读者会发现,Vert.x整个框架中的组件在初始化时都包含了初始化组件的API函数,该函数的参数签名一般是(Vertx,JsonObject)格式或(Vertx,Options)格式,Vertx实例的引用就是通过它传入到组件内部,让所有组件共享的。阅读上述源代码之前,您需回顾一下两个基本概念:函数引用函数调用

  • 函数引用:通常构造如:var fn = xxx,构造出来的fn本身是一个函数,如Java8之后的java.util.function.Function对象,在构造该函数时,您只是使用fn变量获取了一个函数引用,而此时函数没有执行

  • 函数调用:这个概念最简单,就是我们通常说的方法调用。

    代码中的blockingCodeHandler就是一个函数引用,它的类型就是本小节提到的Handler,所以在下边这行代码之前,函数内部的绑定代码都不会运行:

blockingCodeHandler.handle(promise);

    如果您区分了函数引用和函数调用的概念,理解Handler就轻而易举了,最后说明一点:是否异步执行取决于函数调用时的上下文(代码块),而不是函数引用的上下文,在Vert.x环境中对比如下:

// 假设此处的 data 是普通Java对象,并非Vert.x中的Future或Promise
// 同步执行
blockingCodeHandler.handle(data);

// 异步执行,等待当前future完成之后
future.onComplete(res -> {
    blockingCodeHandler.handle(data);
})

    上述概念代码就是Handler的核心原理,至于blockingCodeHandler引用函数的内部代码是同步还是异步取决于函数内部的实现代码,回到本小节最初提到的第三点:如果您已经拿到了一个函数引用,那么该函数就具备了延迟调用的功能,因为这个函数会在您触发它的时候执行(如执行handler.handle(xx)),而触发之前环境中只是维持了该函数引用;若函数内代码本身是同步的,那么只是单纯的延迟调用,若函数内代码是异步的,那么它才是异步调用。之所以会将延迟调用和异步调用混淆,是因为二者具备极高的相似性,它们都是延迟得到执行结果,但从原理上分析,二者存在本质的区别。

2. AsyncResult接口

看完了Handler之后,我们继续屠城。

    如果说Handler的设计是用来区分函数引用函数调用,那么另外一个接口io.vertx.core.AsyncResult就是用来区分同步调用异步调用,该接口的定义如下:

public interface AsyncResult<T> {
    // 其他方法定义
}

    理解接口之前先看一个图示:

    上图描述了AsyncResult接口中所有API的概念图,它是一个典型的Monad容器,容器内数据类型为T,包含了两种状态:成功失败,每种状态提供了判断函数以及读取数据的函数,失败时读取的数据为一个Java语言中的Throwable对象;由于mapotherwise是绑定的函数,函数中实现同步或异步由开发人员自定。

    参考下边的简单代码:

package io.vertx.up._03.handler;

import io.vertx.core.Future;

public class AsyncMain {

    public static void main(final String[] args) {
        first().map(AsyncMain::add5).onSuccess(res -> {
            if (res.succeeded()) {
                System.out.println(res.result() + "," + Thread.currentThread().getName());
            }
        });
        System.out.println("-------------------------");
        first().map(AsyncMain::addx).onFailure(res -> {
            res.printStackTrace();
            System.out.println("Error, " + Thread.currentThread().getName());
        });
        System.out.println("-------------------------");
        first().map(AsyncMain::addDefaultX)
            .otherwise(AsyncMain::addDefault).onComplete(res -> {
                if (res.succeeded()) {
                    System.out.println("Default, " + res.result());
                } else {
                    System.out.println("Other Error");
                }
            });
    }

    private static Integer addDefault(final Throwable error) {
        error.printStackTrace();
        System.out.println("Default, 3 " + Thread.currentThread().getName());
        return 3;
    }

    private static Integer addDefaultX(final int seed) {
        throw new RuntimeException("Default, " + seed);
    }

    private static Future<Integer> addx(final int seed) {
        throw new RuntimeException("Err, " + seed);
    }

    private static Future<Integer> add5(final int seed) {
        System.out.println("Seed, " + seed + " " + Thread.currentThread().getName());
        return Future.succeededFuture(5 + seed);
    }

    private static Future<Integer> first() {
        System.out.println("First, 10 " + Thread.currentThread().getName());
        return Future.succeededFuture(10);
    }
}

    运行该代码您会得到如下信息输出:

First, 10 main
Seed, 10 main
15,main
-------------------------
First, 10 main
Error, main
-------------------------
First, 10 main
Default, 3 main
java.lang.RuntimeException: Err, 10
	at io.vertx.up._03.handler.AsyncMain.addx(AsyncMain.java:40)
	at io.vertx.core.impl.future.Mapping.onSuccess(Mapping.java:35)
	at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
	at io.vertx.core.impl.future.SucceededFuture.addListener(SucceededFuture.java:88)
	at io.vertx.core.impl.future.FutureBase.map(FutureBase.java:108)
	at io.vertx.core.impl.future.SucceededFuture.map(SucceededFuture.java:27)
	at io.vertx.up._03.handler.AsyncMain.main(AsyncMain.java:14)
java.lang.RuntimeException: Default, 10
	at io.vertx.up._03.handler.AsyncMain.addDefaultX(AsyncMain.java:36)
	at io.vertx.core.impl.future.Mapping.onSuccess(Mapping.java:35)
	at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:60)
	at io.vertx.core.impl.future.SucceededFuture.addListener(SucceededFuture.java:88)
	at io.vertx.core.impl.future.FutureBase.map(FutureBase.java:108)
	at io.vertx.core.impl.future.SucceededFuture.map(SucceededFuture.java:27)
	at io.vertx.up._03.handler.AsyncMain.main(AsyncMain.java:19)
Default, 3

    上述代码中有三个例子:

  1. 第一个例子最简单,全程没有任何异常抛出,所以最终调用了onSuccess方法打印了最终结果,它的数据变化如:10 -> 15,最终输出结果。

  2. 第二个例子中,强制性在addx方法内抛出了一个异常,最终调用onFailure方法捕捉异常发生后的情况。

  3. 第三个例子中绑定了otherwize函数,并返回了同步结果Integer,最终调用onComplete方法来捕捉结果。

    开发人员在使用AsyncResult<T>一定要注意此处的T类型,在关注类型时,T类型本身很简单的,比较复杂的是转换之后的类型。代码中的第三个例子在调用map时绑定的是同步函数,也就意味着最终转换出来的结果类型已经是Integer,此时如果您直接使用onSuccess,那么都不用执行下边的检查代码:

    if (res.succeeded()) {
        System.out.println("Default, " + res.result());
    } else {
        System.out.println("Other Error");
    }

    此处根据map/otherwise两个方法的定义来理解:

    default <U> AsyncResult<U> map(Function<T, U> mapper)
    default AsyncResult<T> otherwise(Function<Throwable, T> mapper)

    两个函数在执行过程中,不会去理睬返回值(map中的Totherwise中的U)是一个异步结构还是一个同步结构,通常开发人员可以选择两种方式捕捉结果:

  • onSuccess/onFailure配对,这种模式下函数参数就已经是内部结果TU了。

  • onComplete,这种模式下您拿到的其实是原始结果,它并没有执行任何数据提取操作。

    发现问题没?其实mapotherwize构造出来的实际是类似Java中lambda的map效果,它是一个函数链,并不是字面理解的异步数据流(我们所有的例子此时都还是同步的),如果您把第一个例子中的onSuccess改成onComplete,您会发现res的数据结构形如AsyncResult<Future<Integer>>,和期望的AsyncResult<Integer>不同,这就是map带来的效果——函数返回什么内容,那么它就解析成什么类型,并对此类型执行AsyncResult封装,至于该类型是同步还是异步,它不关心。mapotherwize构造了如下的一种数据结构:

    所有的函数执行都会有成功失败两种状态,而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>

    紧接前一章节提到的AsyncResult<T>,本章我们再来温习两个可爱的小宝贝:Future和Promise,只是这次我们换个视角,从整体结构来解读Vert.x中这部分内容:

3.1. 整体结构

[I]标记是接口,[A]标记是抽象类。

    从结构图上可知:

  • Future的类型是前文提到的AsyncResult<T>

  • Promise的类型则是Handler<AsyncResult<T>>

    Future<T>接口定义中存在几个和Handler直接相关的方法:

// 成功时回调
Future<T> onSuccess(Handler<T> handler)
// 失败时回调
Future<T> onFailure(Handler<Throwable> handler)
// 完成时回调(双态)
Future<T> onComplete(Handler<AsyncResult<T>> handler)

    这三个方法最早是没有的,3.x中原始版本最常用的方法是setHandler,该方法等价于onComplete,从3.8开始该方法被标记为废弃@Deprecated),4.x之后就被移除了,这种设计是因为早期代码经常会是如下写法:

future.setHandler(res -> {
    if(res.succeeded()){
        // 成功返回
    }else{
        // 失败返回
    }
})

    虽然这种写法会使得函数本身更趋近于全函数,但在开发过程中,我们的业务层面有时候只关心单边状态,如成功时如何,又或者失败时如何,如果一直使用setHandler绑定函数,那么系统中会到处充斥着上述结构的代码,这样会显得冗余,所以取而代之提供了onSuccessonFailure的绑定模式。

    也就是说上述示例代码:

first().map(AsyncMain::add5).onSuccess(res -> {
    if (res.succeeded()) {
        System.out.println(res.result() + "," + Thread.currentThread().getName());
    }
});

    也可直接取消res.succeeded()的判断,直接写成:

first().map(AsyncMain::add5).onSuccess(res -> {
    System.out.println(res.result() + "," + Thread.currentThread().getName());
});

    此处牵涉一个简单的编程思维问题: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中常用的代码如:

// 成功
Future.succeededFuture(t);
// 失败
Future.failedFuture(throwable);

    上述两个方法会构造SucceededFuture实例和FailedFuture实例,这两个类的出现是细粒度设计的体现,二者一个职责是表示成功,另外一个职责就是表示失败,它们的区别如下:

  1. 二者构造函数不同

    // 成功
    public SucceededFuture(T result)
    // 失败
    public FailedFuture(Throwable t)
  2. 置留的空方法不同,由于这两个类都是单态,所以对另外一种状态会存在忽略不计的情况,若您在书写时使用了反向绑定(如SucceededFuture调用了onFailure绑定)可能什么也不会发生,这也是开发过程中容易被忽视的点。

    // SucceededFuture中的 onFailure 方法
    public Future<T> onFailure(Handler<Throwable> handler) {
        return this;
    }
    // FailedFuture中的 onSuccess 方法
    public Future<T> onSuccess(Handler<T> handler){
        return this;
    }

    FutureBase中有两个只有子类(protected域)才能访问的主方法emitSuccess(T,Listener<T>)emitFailure(Throwable,Listener<T>),这两个方法是整个Future/Promise的核心逻辑,FutureBase的构造函数如下:

    // 未绑定Vert.x中的上下文对象Context
    FutureBase() {
        this(null);
    }
    // 绑定了Vert.x中的上下文对象Context
    FutureBase(ContextInternal context) {
        this.context = context;
    }

    到此处,相信读者对下边几个方法已经有答案了:

  • 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实例在初始化时支持两种模式:单机模式和集群模式,单机模式的启动是同步创建,集群模式则是异步实现,二者函数签名如:

// VertxBuilder中的代码
// 单机模式
public Vertx vertx();
// 集群模式
public void clusteredVertx(Handler<AsyncResult<Vertx>> handler)

    于是就有了在初始化集群时的官方代码:

/*
 * Vertx中的clusteredVertx方法内部封装如
 * new VertxBuilder(options).init().clusteredVertx(resultHandler);
 * VertxBuilder会优先读取配置并执行组件初始化,再调用上边提到的构造函数实例化Vertx
 */
VertxOptions options = new VertxOptions();
Vertx.clusteredVertx(options, res -> {
  if (res.succeeded()) {
    Vertx vertx = res.result();
    EventBus eventBus = vertx.eventBus();
    System.out.println("We now have a clustered event bus: " + eventBus);
  } else {
    System.out.println("Failed: " + res.cause());
  }
});

4.2. Verticle的部署

    Vertx实例从部署Verticle开始就已经是异步部署了,部署组件没有同步模式,它的API形如:

// Future封装
Future<String> deployVerticle(Verticle verticle, DeploymentOptions options)
// Handler模式
void deployVerticle(Verticle verticle, DeploymentOptions options, 
                    Handler<AsyncResult<String>> completionHandler)

    官方有一段代码:

// 标准模式
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", res -> {
    if (res.succeeded()) {
        System.out.println("Deployment id is: " + res.result());
    } else {
        System.out.println("Deployment failed!");
    }
});
// 小坑代码
DeploymentOptions options = new DeploymentOptions().setInstances(16);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);

    之所以说此处代码是小坑是因为在部署Verticle组件时,我们本身就会等待部署结果,无关部署完成后去执行此操作,Future<String>的返回值并不代表代码本身不执行,而是随后会执行,执行完成待定。它提供了这样一种视角:一个函数返回Future,并不代表它不会执行,而是它不会立即执行,若您不关心返回值,则无需设置对应的Handler,当然也不需要调用onSuccess/onFailure执行绑定,等待结果即可。若您想要在执行完成后有对应操作,则必须设置Handler代码,Handler的另外一种本质就是类似JS中的回调,它采用了Callback风格的代码让您可以在异步操作执行完成之后再处理其他事情。图示如下:

    图中的灰色区域是开发人员无法触碰的区域,主代码一旦调用了deployVerticle之后,它的执行就不受主代码管控了,能等待它执行结果的地方就只有异步回调代码了,而主代码中的return操作在此时不起任何作用,这和JS中的异步如出一辙,而灰色区域实则是开发过程中的盲区。

4.3. Verticle启动

    我们一般写Verticle代码,只会去重写start()方法,通常写法如:

public class OptionVerticle extends AbstractVerticle {

    @Override
    public void start() {
        final HttpServer server = this.vertx.createHttpServer();
        server.requestHandler(handler -> {
            System.out.println(Thread.currentThread().getName());
            handler.response()
                    .putHeader("content-type", "text/plain")
                    .end("Hello Direct Server!");
        });
        server.listen(1023, res -> {
            System.out.println("Server Started");
        });
    }
}

    而AbstractVerticle中包含了异步方法函数如下:

  @Override
  public void start(Promise<Void> startPromise) throws Exception {
      start();
      startPromise.complete();
  }

    这里有个小细节:异步实现中只是单纯调用了start()函数,如果该函数本身包含了异步行为,那么这个行为您可以在函数内部书写回调,Verticle组件本身无法捕捉回调内容,也就是说,Verticle的默认异步启动只是发起了异步请求,并没等待您的start()方法执行完成,若要让您的start()方法在执行完成后再标识Verticle部署完成(示例中等待监听结果),您可以重写start(Promise<Void>)而不是start(),代码修改如下:

    @Override
    public void start(final Promise<Void> startPromise) throws Exception {
        final HttpServer server = this.vertx.createHttpServer();
        server.requestHandler(handler -> {
            System.out.println(Thread.currentThread().getName());
            handler.response()
                .putHeader("content-type", "text/plain")
                .end("Hello Direct Server!");
        });
        server.listen(1023, res -> {
            if (res.succeeded()) {
                System.out.println("Server Started");
                startPromise.complete();
            } else {
                startPromise.fail(res.cause());
            }
        });
    }

    如此:Verticle的部署完成就一定会发生在HttpServer监听成功之后了!所幸的是大部分情况下,部署Verticle是无需如此操作,异步部署已经是很优秀的解决办法了。配合下边主代码:

public class AsyncLauncher {

    public static void main(final String[] args) {
        // 选择单点模式
        final Launcher launcher = new SingleLauncher();

        launcher.start(vertx -> {
            // 发布
            vertx.deployVerticle(OptionVerticle::new,
                new DeploymentOptions().setInstances(1), res -> {
                    if (res.succeeded()) {
                        System.out.println("Verticle Completed");
                    }
                });
        });
    }
}

    打印结果:

# 同步启动打印结果,重写 start(Promise)
Server Started
Verticle Completed

# 异步启动打印结果,重写 start()
Verticle Completed
Server Started

    同步打印结果是固定的,每次打印结果都会按顺序输出,异步打印结果理论上讲由于Verticle的start()方法要耗费时间,所以Verticle Completed先打印,但如果小概率出现了Verticle组件先执行完,那么有可能Server Started也先打印,这点读者尤其注意,两种打印结果前者是固定顺序,后者并不是固定顺序,而是无序的,您理解了这点,那么对异步数据流就有了更深的理解。

5. 总结

    本章主要讲解了Vert.x中Handler部分主体内容,也解析了其常用结构和用法,在涉入Vert.x的Web开发之前,这里属于必经之路。您若仔细去阅读Vert.x的源代码,会在整个框架内部看到很多类似Handler<AsyncResult<T>>的定义,阅读了本章内容,那么您更容易理解Handler,面对很多API的调用以及相关写法,就不会疑惑了。

最后更新于