一直以来早有将这些年用Vert.x的经验整理一下的想法,奈何天生不是勤快人,直到最近扶墙老师问起,遂成此文。
现在想想,我们应该算是国内用 Vert.x 的最早一批人,版本大概是 1.2.x 吧,当时 Vert.x 内置了一个比较坑爹的模块系统,看似不错,但其实很坑爹。但即使这样,我们当时还是在技术选型上采用了它。理由大致如下:
于是乎,它顺利成章地成为了我们当时系统接入层的中流砥柱,在实践中也确实发挥了很好的作用。
鉴于 Vert.x 当前的版本是 3.3.3,因此本文的内容也主要针对这个版本而言,一些我们遇到并且已经修复的 bug,也就不会也没有必要在此啰嗦了。
此外,本文也不是入门文档,而是为了预防陷坑而给出的指导意见,故在阅读本文之前还请先仔细阅读Vert.x 的文档。
虽然 Vert.x 的一大亮点号称是支持“多语言”,即同一个工程内可以同时用 Java、Groovy、Javascript 等不同语言编写 Verticle,但我还是建议采用 Java 为主,最多辅以 Groovy。原因是:我发现很多新出的 Vert.x 模块还是对 Java 支持最好,对于其他的则就相当一般了,起码不会让你感觉特意针对这个语言而开发的。加上本来 Java 8 之后支持 lambda,Java 程序员的苦逼生活其实已经改善不少。
在dgate中,我主要采用 Java + Groovy 的方式,两者分工也很明确:前者用于数据处理,后者则用于 DSL 和数据类。
此时,由于混用了两者,并且可能会出现 Groovy 类要用到程序中 Java 类的情况,那么就要用到 joint compile。在 build.gradle 中需要配置如下:
sourceSets.main.java.srcDirs = []
sourceSets.main.groovy.srcDirs += ["src/main/java"]
即,将 Java 类也交由 Groovy 编译器来编译。
虽然 Vert.x 可以内嵌到其他框架中,但在实际项目上我还是偏爱单独部署,项目的构建方式则为:gradle + fatjar。具体例子,可以参见这个 build.gradle 文件。
我在 Vert.x 邮件组中经常看到有新人问关于 Vert.x 的组织方式,其实这是没有理解 Vert.x 的本质:Verticle。Verticle 可视作 Vert.x 的一个最小部署和运行单元,简单的说,可类比为 Servlet。因此,整个应用可以这样来划分:
前两者负责初始化,Verticle 则类似 Servlet 一样等待被触发(来自 TCP/Eventbus/HTTP 的 Request),在实际处理时会调用到其他类。
这也就是为何在上面的 build.gradle 中有这样关键的两行的原因:
manifest {
attributes 'Main-Class': '……'
attributes 'Main-Verticle': '……'
}
Vert.x 默认支持 JUL,对于其他 Logging 框架也有支持。但我嫌每次运行要敲那么多命令很烦,那么可以在 Launcher 中强制设置环境变量:
System.setProperty("vertx.logger-delegate-factory-class-name",
"io.vertx.core.logging.SLF4JLogDelegateFactory");
跟 Servlet 类似,多个 Verticle 之间也会有依赖关系,存在先后部署的需要。
对于单个 Verticle 之间的依赖,如 A 依赖 B,很简单,利用 deployVerticle 的回调就很好解决。因为代码简单,这里就不再单独列出,还是那句话,看文档。
对于依赖多个 Verticle,如 A 依赖 B 和 C,则需要有点技巧了:
private void deployVerties(List<Map> verticles, Closure completeHandler = null) {
AtomicInteger count = new AtomicInteger(0)
verticles.each { verticle ->
vertx.deployVerticle(verticle.name, verticle.option ?: [:]) { result ->
if (result.succeeded()) {
if (count.incrementAndGet() == verticles.size()) {
if (completeHandler) {
completeHandler.call()
}
}
} else {
exit(verticle.name, result.cause())
}
}
}
}
看到 Atom 对象,你是否觉得也可以采用 CountDownLatch 对象?很不幸,不行。我当时做过尝试,整个代码立马被 Block 住,直到我按了 Ctrl-C。原因在于:Block 住了 EventLoop。
至于 deployVerticle(),它可以接受字符串和类实例。当使用字符串时,若是非 Java 类,如 Groovy,需要采用这样的格式:“语言前缀:类全限定名”。如:
'groovy:hawkeyes.rtds.processor.MailMan'
此外,部署的 Verticle 实例并非越多越好,还跟 CPU 的核数相关。
Vert.x 应用最忌讳 Blocking 操作,对此有多种处理:
凡是涉及 IO 的操作,都请考虑一下。
EventBus 相当于 Vert.x 应用的神经系统,但有几点需要注意:
严格来讲,3.2 之后,上述第一点并不完全正确。这两个 Verticle 之间可以采用 TCP EventBusBridge 来进行通信,具体参见这篇文章。
Cluster 是当时我选择 Vert.x 的一个重要考量,而且将 Vert.x 应用单独打成 fatjar 还有一个附带好处就是 Vert.x 的 cli 都可以直接使用,其中就包括 cluster 命令。
Vert.x 的集群建立在Hazelcast之上,除了集群调度,它本身还能做内存存储,即具备了 Redis 的主要功能。并且查询语法也比 Redis(2.x)的要灵活,支持类 SQL 语法。更重要的是,其 ReadThrough 特性让人欲罢不能,简化了编程。当然,还包括其他如分布式锁、队列、任务等等。
所谓 ReadThrough,即“若内存中没有,则查询将下传到下一级(通常是 DB)”。Hazelcast 的 ReadThrough 可通过实现 MapLoader 接口来实现。这个例子很简单,故可查看 Hazelcast 的文档了解。这里重点讲一下如何在 Vert.x 中去配置,因为 Vert.x 没有对此提供直接支持。
首先,cluster.xml 即为一个标准的 Hazelcast 配置文件,故可在此配置相应的 MapLoader 即可:
<map name="map_name">
<map-store enabled="true">
<class-name>xxxLoader</class-name>
</map-store>
</map>
在从未给集群 Map 赋过值且第一次运行下列代码时,注意两个名字要相同,则触发 ReadThrough:
vertx.sharedData().getClusterWideMap("map_name") {……}
如果想在 Vert.x 中获得 Hazelcast 实例,则可以直接使用下面代码:
Set<HazelcastInstance> instances = Hazelcast.getAllHazelcastInstances()
hz = instances.first()
这样便可利用 Hazelcast 的其他功能。在 3.3.3 之后,Vert.x 集群支持Ignite,它是比 Hazelcast 更强大的内存计算工具。而且,在 Vert.x 3.4-beta1 中已经不再是技术预览版,日后我肯定会全面拥抱它。
Ignite/Hazelcast 不像 Redis 那样曝光率那么高,但鉴于其本身都是老牌内存计算软件,且在开源之前都在高强度生产环境(没记错的话是银行系统)实战演练过,同时对比一下两者之间的功能列表,你会发现这些工具其实更强大,尤其是 Ignite。它们的文档都不错,值得一看。
最后说一说 Handler 中需要注意的地方,它非常适合写 Restful API。
之前用 Vert.x 写接入层代码,主要集中在 Core、Groovy 和 Shell 部分。这次写dgate,算是扎扎实实用了一下 Web 部分。至于历史,我就不详细说了,总之一句话:哥是看着它长大的,;)。
Handler 其实很简单,只需要注意几点:
至于其他,没啥可说的,都很简单。
最后,来句鸡汤:遇坑不可怕,还得勇于尝试方能有所收获,希望对各位有帮助!
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章