注:本文译自Julien Viet的Advanced Vert.x Guide

本文旨在解释和讨论以下内容

  • Vert.x的设计
  • 内部API
  • 与Netty集成

当你阅读本指南时你将:

  • 更深入了解Vert.x
  • 了解如何将Vert.x与其他第三方库集成
  • 了解如何使用Netty和Vertx.x编写网络应用

这是一个实时指南,你可以做出贡献,只需提交PR或者提issuus

本指南中公开了一些内部Vert.x API,但谨记,这些API可能会在需要时进行更改。

Vert.x中的上下文

io.vertx.core.Context接口是Vert.x的一个重要组件。

上下文(Context)可以被认为是应用程序如何执行事件(或者handler创建的任务)。

绝大部分的事件是通过上下文(Context)下发的, 当应用程序消费事件时,往往存在与事件调度相关联的上下文。

Verticle上下文

部署verticle的实例时,Vert.x会创建一个上下文并将其与该实例关联。你可以通过AbstractVerticlecontext字段在verticle中访问此上下文

1
2
3
4
5
public class MyVerticle extends AbstractVerticle {
public void start() {
JsonObject config = context.config();
}
}

MyVerticle部署后,Vert.x 发送一个 start 事件, Verticle上下文调用start 方法:

  • 默认情况下,上下文始终是事件循环(event-loop)上下文,调用线程是事件循环 (event loop)

  • 当vertile部署为worker时,调用线程是Vert.x的worker池之一

上下文(Context)的特殊之处

从 Vert.x 3开始支持不通过Verticle使用Vert.x的API,这引出了一个有意思的问题:到底使用哪个上下文(Context)?

当一个Vert.x的API被调用,Vert.x关联当前线程到一个特殊的event-loop context,Vertx#getOrCreateContext()在第一次被非Vertx线程调用时创建上下文,然后在随后的调用中返回此上下文。

因此,异步Vert.x API上的回调在同一上下文中发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
WebClient client = WebClient.create(vertx);

for (int i = 0;i < 4;i++) {
client
.get(8080, "myserver.mycompany.com", "/some-uri")
.send()
.onSuccess(ar -> {
// 所有的回调在同一个上下文
});
}
}
}

上下文(Context)的传递

大多数Vert.x API都包含上下文的存在(原文aware of明白上下文的存在)。

在上下文中执行的异步操作将调用具有相同上下文的应用程序。

同样,事件处理程序也在同一上下文上调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyVerticle extends AbstractVerticle {
public void start() {
Future<HttpServer> future = vertx.createHttpServer()
.requestHandler(request -> {
// 在verticle上下文中执行
})
.listen(8080, "localhost");

future.onComplete(ar -> {
// 在verticle上下文中执行
});
}
}

处理上下文(Context)

大多数应用程序不需要与上下文进行紧密的交互,但有时访问它们会很有用,例如,你的应用程序使用另一个库,该库在自己的线程上执行回调,并且你希望在原始上下文中执行代码。

上文我们看到,verticle可以通过context字段访问其上下文,但这意味着使用verticle并在verticle上有一个引用可能并不总是方便的。

你可以通过 getOrCreateContext()获取当前上下文:

1
Context context = vertx.getOrCreateContext();

你也可以使用静态方法Vertx.currentContext():

1
Context context = Vertx.currentContext();

如果当前线程没有与上下文关联,则后者可能返回null,而前者将在需要时创建一个上下文,因此永远不会返回null。

在获取了上下文之后,你可以在这个上下文中执行代码:

1
2
3
4
5
6
7
8
9
public void integrateWithExternalSystem(Handler<Event> handler) {
// 捕获当前上下文
Context context = vertx.getOrCreateContext();

// 在应用上下文上执行事件处理器
externalSystem.onEvent(event -> {
context.runOnContext(v -> handler.handle(event));
});
}

在实践中,很多Vert.x的API和第三方库就是这样实现的。

事件循环上下文(Event-loop context)

事件循环上下文使用事件循环(Event-loop)来运行代码:处理程序直接在IO线程上执行,因此:

  • 处理程序将始终使用同一个线程执行
  • 处理程序决不能阻塞线程,否则它将会导致该事件循环关联的所有IO任务阻塞(原文是starvation)

这种行为通过保证关联的处理程序总是在同一个线程上执行,从而消除了同步和其他锁机制的需要,从而大大简化了线程模型。

事件循环上下文是默认和最常用的上下文类型,在没有worker标志的情况下部署的verticle将始终使用事件循环上下文进行部署。

Worker上下文(Worker context)

Worker上下文被分配给在启用worker选项的情况下部署的verticle上。Worker上下文与标准事件循环上下文的区别在于,工作线程在单独的工作线程池上执行。

这种与事件循环线程的分离允许Worker上下文执行阻塞事件循环的阻塞操作类型:阻塞这样的线程除了阻塞一个线程之外不会影响应用程序。

正如事件循环上下文一样,Worker上下文确保处理程序在任何给定时间只在一个线程上执行,也就是说,Worker作上下文上执行的处理程序将始终按顺序执行,一个接一个,但不同的操作可能在不同的线程上执行。

上下文异常处理器

你可以在上下文上设置异常处理程序,用以捕获在上下文上运行的任务引发的任何未检查的异常,如果未设置异常处理程序,则默认改为调用Vertx异常处理器。

1
2
3
4
5
6
7
context.exceptionHandler(throwable -> {
// 任何通过上下文抛出的异常
});

vertx.exceptionHandler(throwable -> {
// 任何上下文抛出的未捕获异常
});

如果未设置任何处理程序,则异常将作为错误记录,并显示消息_Unhanded exception_你可以使用reportException报告上下文中的异常

1
context.reportException(new Exception());

发射事件

runOnContext是在上下文上执行一段代码的最常见方式,尽管它非常适合将外部库与Vert.x集成,但它并不总是最适合将在事件循环级别执行的代码(如Netty事件)与应用程序代码集成。

Vert.x有一些内部方法可以根据情况实现类似的行为 :

  • ContextInternal#dispatch(E, Handler<E>)

  • ContextInternal#execute(E, Handler<E>)

  • ContextInternal#emit(E, Handler<E>)

Dispatch

dispatch假定调用线程是上下文线程,它将当前执行线程与上下文关联起来:

1
2
3
4
assertNull(Vertx.currentContext());
context.dispatch(event, evt -> {
assertSame(context, Vertx.currentContext());
});

该处理器也被阻塞线程检查器检测。

最后,处理程序抛出的任何异常都会报告给上下文:

1
2
3
4
5
6
context.exceptionHandler(err -> {
// 将会接收到下面抛出的异常
});
context.dispatch(event, evt -> {
throw new RuntimeException();
});

Execute

execute 在上下文上执行任务,当调用线程已经是上下文线程时,直接执行任务,否则安排此任务计划执行(实际是提交给背后的event-loop执行,补充源码如下)。

没有上下文关联也可以。

1
2
3
4
5
6
7
8
protected <T> void execute(ContextInternal ctx, Runnable task) {
EventLoop eventLoop = nettyEventLoop();
if (eventLoop.inEventLoop()) {// 如果是上下文线程,直接执行
task.run();
} else {
eventLoop.execute(task);// 否则提交到eventloop中执行
}
}

Emit

emitexecutedispatch的组合

1
2
3
default void emit(E event, Handler<E> eventHandler) {
execute(v -> dispatch(argument, task));
}

emit 可以用于从任何线程上发射事件到处理器上:

  • 在任何线程中,它的行为都类似于runOnContext
  • 如果是上下文线程,它通过上下文中的本地线程关联关系、阻塞线程检查器运行事件处理器,并报告上下文上的失败

在大多数情况下,emit方法是让应用程序处理事件的方法,dispatchexecute方法的主要目的是赋予代码更多的控制权,以实现非常具体的事情。

上下文感知的futures

在 Vert.x 4 之前,Future 都是静态创建的对象,与上下文没有特定关系。 Vert.x 4 提供了一个基于 future 的 API,它遵循了与 Vert.x 3 相同的语义:future 上的任何回调都应该可预测地在相同的上下文上运行。

Vert.x 4 的API 创建绑定到调用者上下文的 future,在上下文上运行回调:

1
2
3
4
5
Promise<String> promise = context.promise();

Future<String> future = promise.future();

future.onSuccess(handler);

任何回调都会在创建 Promise 的上下文中发出,上面的代码大概率会长这样:

1
2
3
4
5
Promise<String> promise = Promise.promise();

Future<String> future = promise.future();

future.onSuccess(result -> context.emit(result, handler));

此外,该 API 允许创建成功和失败的 future:

1
2
Future<String> succeeded = context.succeededFuture("OK usa");
Future<String> failed = context.failedFuture("Oh sorry");

上下文追踪(Contexts and tracing)

从 Vert.x 4 开始,Vert.x 集成了流行的分布式跟踪系统。

追踪库通常依赖于thread local来传播跟踪数据,例如,处理 HTTP 请求时收到的跟踪信息应该在整个 HTTP 客户端中传播。Vert.x 以类似的方式集成追踪,但依赖于上下文而不是thread local,上下文由 Vert.x API 传播,因此为实现追踪提供了可靠的存储。

由于给定服务器处理的所有HTTP请求都使用创建HTTP服务器的相同上下文,因此服务器上下文对于每个HTTP请求,是 duplicated _(重复的)_,以授予每个HTTP请求的唯一性。

1
2
3
4
5
6
7
8
9
public class MyVerticle extends AbstractVerticle {
public void start() {
vertx.createHttpServer()
.requestHandler(request -> {
// Executed in a duplicate verticle context
})
.listen(8080, "localhost");
}
}

这种重复(复制)操作共享原始上下文的大部分特性并提供特定的本地存储。

1
2
3
4
5
6
7
8
vertx.createHttpServer()
.requestHandler(request -> {
JsonObject specificRequestData = getRequestData(request);
Context context = vertx.getOrCreateContext();
context.putLocal("my-stuff", specificRequestData);
processRequest(request);
})
.listen(8080, "localhost");

然后应用就可以使用它了

1
2
Context context = vertx.getOrCreateContext();
JsonObject specificRequestData = context.getLocal("my-stuff");

ContextInternal#duplicate() 复制当前上下文,它可用于确定追踪行动的范围

关闭钩子函数(Close hooks)

Close hooks 是 Vert.x 的一项内部功能,在 Verticle 或 Vertx 实例关闭时可通知到组件, 它可用于实现 verticle 中的自动清理功能,例如 Vert.x HTTP 服务器。

io.vertx.core.Closeable 接口及其 close(Promise<Void> closePromise) 方法定义了接收关闭通知的实现

1
2
3
4
5
@Override
public void close(Promise<Void> completion) {
// Do cleanup, the method will complete the future
doClose(completion);
}

ContextInternal#addCloseHook注册了一个Closeable的实例用于通知上下文什么时候关闭:

1
context.addCloseHook(closeable);

当 Verticle 实例停止时,Verticle 部署创建的上下文会调用该钩子。否则,当 Vertx 实例关闭时会调用该钩子。

Context#removeCloseHook 取消注册关闭钩子,并会在调用关闭钩子函数之前资源即将关闭时使用。

1
context.removeCloseHook(closeable);

钩子函数为避免泄漏是用弱引用实现的,但是不论如何你也应该取消注册钩子。

在重复上下文上添加钩子,会将钩子添加到原始上下文。

同样,VertxInternal 也暴露了相同的方法来在 Vertx 实例关闭时接收通知。