这篇文章将介绍如何使用Eclipse Vert.x设计和开发一个基于消息驱动的响应式持续集成(CI)系统。我们将利用Java平台模块系统(JPMS)来构建一个由多个模块组成的应用程序,模块之间通过定义好的接口进行通信。
有了JPMS,架构师和开发者就可以使用模块来重构大型的遗留系统,或者用它们来创建新的应用程序。不过,要在模块系统中使用已有的Java类库并不是件容易的事。因此,我们也会探讨在使用JPMS过程中可能遇到的各种问题,以及如何解决这些问题。
先让我们来定义这个CI系统的最小可用产品(MVP),我们将把它构建成Docker原生系统。这个系统需要提供如下特性,并通过REST API暴露出来:
支持针对仓库的CRUD操作。一个仓库代表一个项目,并带有Git仓库的连接地址。
支持“管道即代码”。管道定义了构建流程,并使用JavaScript来定义,JavaScript脚本文件可以与代码保存在一起。
提供用于启动或关闭管道的API。管道的一个实例就代表一次构建过程。
定义好MVP后,就可以开始构建我们的系统了。首先要创建项目的骨架,可以使用IntelliJ提供的多模块Gradle项目模板来创建骨架。因为要使用JDK 9,所以最好可以选择最新版的Gradle(在写这篇文章是最新版是4.4.1)。我们还需要添加Jigsaw插件,并把代码兼容性设置为Java 9。项目的主文件“build.gradle”应该看起来像下面这样:
与其他大多数系统一样,我们将会有一个公共库,用来放置实体类、工具类、共享常量、查询解析器等。我们把这个公共库定义成一个Java 9模块。
之前已经讲过,Java 9模块是接口、类和资源文件的集合,具有自描述的特点,而且有自己的名字。JPMS引入了“module-info.java”文件,开发者用它定义模块的公共契约和对其他模块的依赖。我们也将使用这个文件来命名我们的模块,并指定对其他模块的依赖以及模块自身暴露出来的公共包。
下图是module-info.java文件的示例代码:
每个“module-info.java”文件都以关键字“module”作为开头,后面跟上模块的名字。用在包命名上的反向域名命名方式也可以用在模块的命名上。
代码块中有两个新的关键词——“exports”和“requires”。“exports”用于声明由该模块暴露出来的公共包,也就是模块的公共API。“requires”用于声明对其他模块的依赖。
那么,问题来了,如果一个Java 9模块的依赖包并不是模块,那该怎么办?这个时候,自动模块就派上用场了。
正如它的名字告诉我们的那样,非模块的JAR包会被自动转成模块,并基于JAR包的名字来生成模块名。模块名的生成遵循这样的规则:以JAR包文件开头,去掉扩展名,用点号替代连字符,如果有版本号就把版本号去掉。这样的话,“vert-core-3.5.0.jar”对应的模块名就是“vertx.core”。不过,这种方式不一定都能奏效,后面我们会举一个与Netty依赖包相关的例子。
除了核心模块,我们还要定义其他一些模块,用于访问数据库、用户认证、运行引擎以及与CI系统中的其他插件交互。
在介绍了Java模块化的一些概念后,接下来让我们来聊聊Vert.x。Vert.x是一个工具套件,提供了非阻塞的API,也就是说,Vert.x应用程序只需要使用很少的线程就可以处理大量的并发请求。Vert.x采用了multi-reactor模式来达到这个目的。
熟悉JavaScript的开发者或许还记得单线程事件循环模型,multi-reactor模式与之类似,只不过它使用了多个线程。Vert.x根据给定服务器的CPU核数创建相应个数的事件循环对象。
Vert.x还提供了另一种基于actor的并发模型。在Vert.x生态系统中,actor被称为“verticle”,verticle之间通过JSON消息进行通信,这些消息通过事件总线进行传送。我们还可以指定部署多少个verticle实例。
事件总线可以是一个集群,使用集群管理器来管理,比如Hazelcast或Zookeeper。我们可以把运行在Vert.x实例上的verticle或verticle组合看成是微服务。mutli-reactor模型、verticle和事件总线让Vert.x应用程序具备了高响应式、高弹性的特点,因此,我们可以说Vert.x应用程序是反应式的。
现在让我们来看看这个CI系统的整体流程: