如上图所示,有好几个verticle通过Vert.x事件总线进行通信。要注意,图中的插件也是verticle。Server verticle是CI系统的入口,对外暴露了一个REST API,命令行或GUI客户端可以通过这个API指定代码仓库的连接地址、创建和运行构建管道。
下面的代码告诉我们如何在Vert.x中定义API和路由:
我们使用Vert.x的Web类库来定义REST API,而且所有的路由均以“/api/v1/”作为前缀。Vert.x还提供了很多其他类库,用于快速开发反应式应用程序。
例如,我们可以使用Web API类库来设计一个基于OpenAPI 3的应用程序API,这个类库会帮我们处理好请求验证和安全验证问题。Vert.x的OAuth类库可用来提高应用程序API的安全性,OAuth厂商可以是谷歌、Facebook,也可以自定义。
在上一张图片中,Engine verticle负责协调管道的执行。在客户端调用Server verticle提供的API之后,Server verticle向Engine verticle发送消息,Engine verticle在收到消息之后会初始化一个新的flow对象。
flow对象实际上是一个简单的状态机,用来跟踪管道的执行状态。在任意时刻,flow对象可能处于这三种状态中的一种:setup、run或teardown。它会根据输入消息来改变状态。在进入一个新的状态时,flow对象会触发一个事件,并将事件发送到事件总线。
注册到事件总线上插件会处理这些消息,并把处理结果通过事件总线异步传回。下面的代码演示了如何注册一个消息处理器、创建flow对象以及处理流入的消息:
Engine verticle也负责定位和部署其他插件或verticle。我们使用了在Java 6中引入并在Java 9中改进过的服务加载器机制,用它在服务器启动过程中定位和部署插件。为了更好地理解服务加载机制,有必要讨论一下服务和服务提供者。
服务其实就是一个已知的接口或类(通常是抽象类),而服务提供者则是服务的具体实现。ServiceLoader类用于加载实现了给定服务的服务提供者。我们可以在模块中声明它使用了某个特定的服务,然后使用ServiceLoader来定位和加载部署在运行环境中的服务提供者。
例如,server模块声明了它要使用Plugin接口,workspace模块则声明它将提供两个实现了Plugin接口的服务。
因此,server模块在启动的时候,它会调用ServiceLoader,找到两个插件,然后把它们部署成verticle:
插件会完成很多工作,包括注册消息处理器,用于处理感兴趣的管道事件。例如,workspace插件负责同步Git代码,而脚本解析器插件负责扫描workspace,找出和执行管道脚本文件(使用JavaScript编写)。执行完脚本文件会生成一些shell命令,Docker容器中的脚本执行器插件会执行这些命令。因为Vert.x使用了Java内置的Nashorn引擎,所以完全可以运行JavaScript代码。要知道,Vert.x还可以支持JavaScript、Kotlin和Groovy。
下面是管道脚本文件的部分代码:
收到消息后,脚本执行器插件将会下载Docker镜像、创建容器并执行shell命令。
Docker的REST API通常是通过基于unix domain socket的HTTP来暴露的,并非传统的基于TCP socket的HTTP(S)。这也是Vert.x得以发挥其作用的地方之一,我们可以使用Vert.x的异步客户端与Docker进行交互,而不是使用普通的同步阻塞式方案。
如果项目中包含了由Netty提供的原生传输类库,Vert.x就会转而使用原生传输。如果我们在“build.gradle”和“module-info.java”文件中指定了类似“netty-transport-native-kqueue”这样的依赖,就会发生这样的情况。
或许Vert.x下一个版本会支持基于unix domain socket的HTTP。目前,我们可以通过修改Vert.x类库的少量代码来解决这个问题。与Docker引擎交互的插件代码看起来是这样的: