1.5.Verticle生命周期
本章承接前一章,谈谈未完成的剧情。上一章节讲到了主流程中,Vertx发布Verticle实例的细节,那么在发布之后,就进入了Verticle的内部代码,即start/stop两个核心生命周期,本章节的故事则是围绕Verticle的两个生命周期展开。Verticle的start方法将在Vertx实例调用deployVerticle的API后被触发。
1. Verticle的编写
在书写Verticle^1的过程中,我们会用到一个新类io.vertx.core.AbstractVerticle,我们编写的所有的Verticle都是从这个类继承过来的,最简单的一个Verticle代码如下:
public class MyVerticle extends AbstractVerticle {
// 启动Verticle时被调用的方法:Deploy
public void start() {
}
// 停止Verticle时被调用的方法:Undeploy
public void stop() {
}
} 上边的代码编写了一个最简单的Verticle组件,最直接的编写自定义逻辑的方式就是重写抽象类中的start/stop两个方法,这两个方法分别对应到Verticle的核心生命周期,在继续下边内容之前我们去AbstractVerticle这个类中逛逛:其实这个类的源代码很简单,去掉注释部分如下:
public abstract class AbstractVerticle implements Verticle {
protected Vertx vertx;
protected Context context;
@Override
public Vertx getVertx() {
return vertx;
}
@Override
public void init(Vertx vertx, Context context) {
this.vertx = vertx;
this.context = context;
}
public String deploymentID() {
return context.deploymentID();
}
public JsonObject config() {
return context.config();
}
public List<String> processArgs() {
return context.processArgs();
}
@Override
public void start(Future<Void> startFuture) throws Exception {
start();
startFuture.complete();
}
@Override
public void stop(Future<Void> stopFuture) throws Exception {
stop();
stopFuture.complete();
}
public void start() throws Exception {
}
public void stop() throws Exception {
}
} 上述代码可以看到,AbstractVerticle中定义了四个核心的生命周期方法:两个同步、两个异步、两个开始、两个结束,读者先不要去纠结io.vertx.core.Future类是做什么用的,只需要知道在Vert.x中有它的地方就意味着**“异步”**,综上所述:
开始
start()
start(Future<Void> startFuture)
结束
stop()
stop(Future<Void> stopFuture)
最开始的代码演示了如何同步启动/停止Verticle组件,那么接下来就看看如何异步地启动/停止Verticle组件。
异步启动/停止的使用场景分析:结合官方教程,简单分析一下异步启动的必要性。异步启动和停止两个方法本身为了解决这样一种问题,您想要在这两个生命周期中定义自己的逻辑:
这个代码逻辑本身包含了异步代码,如上边示例中异步调用了
server.listen方法;这个代码可能包含了一些Block的动作,如访问IO装置;
根据Vert.x官方提供的黄金法则,您不能在Verticle中引入直接的阻塞代码,这样的引入有可鞥会直接阻塞Event Loop,这个是不被允许的,在这样的情况下,您需要使用另外的手段来解决这种“阻塞”问题,那么异步的start/stop就是您的选择。这里做个假设,如果上边的代码为:
上边代码会发生什么?

从上图可以看到,在Verticle的start方法执行完成后,HttpServer的listen动作有可能还没完成,也就是说这样的方式导致了listen的完成动作在start完成动作之后,——这样没有办法维持Verticle在启动过程中的状态一致性。我们期望的结果是当HttpServer的listen动作完成后才标志着Verticle的启动完成,那么改变前的代码就可以达到这个效果,这里的主要原因是我们使用了HttpServer的异步listen的API,所以为了确保状态的一致性,使用Verticle组件中的异步start就成为了一种必然。
当然,您如果可以忍受在listen在Verticle的启动之后完成的话,那么您可以使用这样的写法,可是如果出现一种极端的情况:您写好的Verticle不论从日志还是其他参数都看到的是启动完成,而HttpServer在listen过程失败了(可能端口被占用,可能其他),那么这种情况下,从您想要的初衷,这个HttpServer并没有启动完成,那么这个时候Verticle的状态和HttpServer的状态就出现了明显的不一致。——之所以说这是一种极端情况是因为这种情况可能发生概率并不大,但是为了从程序的严谨上考虑,建议这样的代码还是不要出现,因为Vert.x中本来就有解决这种异步调用的解决方案。
2. 关于DeploymentID
在Vert.x中,每个Verticle组件在发布过后会有一个唯一标识符——称为DeploymentID,该标识符的格式是UUID^2的格式如:a8cdd717-49f8-452d-8d07-8263cae99a26,Vert.x在发布(Deploy)时会触发Verticle组件的start方法,进入启动的生命周期,而在撤销(Undeploy)时会触发Verticle组件的stop方法,进入停止的生命周期,冒看之下DeploymentID在启动过程没有使用到,但在停止的过程中往往会用到,参考一下Vertx实例的undeploy系列方法。
Vertx实例中只有两个undeploy方法,这两个方法的第一个参数都是DeploymentID,所以如果要处理“停止”这个生命周期的一些细节,比如使用自定义代码,那么在编程方式发布/撤销时需要考虑将发布过程的DeploymentID记录下来,这样才可以针对对应的Verticle实例执行后续操作。
**「注」**这里的DeploymentID实际上关联的也不是一个Verticle实例(单线程),而是某一类(即您所编写的Verticle类),也就是说DeploymentID还不会涉及到线程这个级别,而是前文提到的“一堆”。
3. 例子:生命周期控制
接下来通过zero中对Deploy/Undeploy的应用解析一下处理Verticle完整生命周期的代码
3.1. 开发Verticle
开发一个Verticle如上,使用打印语句的主要目的是可直接监控当前这个Verticle组件的生命周期。
3.2. 主程序
3.3. 运行
直接运行该程序,然后将该程序关闭,点击IDE中的停止或在命令行中Ctrl + C的中断模式,您将可以看到下边输出:
3.4. 分析
在总结本小节之前先看看上边用到的一个特殊的JVM函数:Runtime.getRuntime().addShutdownHook[^3]函数,该函数参数为一个java.lang.Thread,它的方法签名如下:
该方法用于在JVM中增加一个关闭的钩子,当程序正常退出,系统调用System.exit方法或者虚拟机被关闭时就会执行该线程中的代码。其中shutdownHook是一个已经初始化但是并没启动的线程,当JVM关闭时,会执行系统中已经设置的所有通过addShutdownHook添加的钩子,等到系统执行完这些钩子中的代码后,JVM才会正式关闭,所以可以使用该方法在JVM关闭时进行内存清理、资源回收等工作。
为什么我们需要该方法?Vert.x中的stop方法不会主动触发,它只有在您调用了undeploy过后才会触发,所以对于Vert.x中的”停止“部分,必须设置关闭时的代码,从实际使用看来,该代码放到shutdownHook是最有效的,因为该代码的触发点比较合适——最理想的状况就是在系统关闭之前调用所有Verticle组件的stop方法来清理相关资源,这些清理动作包括:
关闭未关闭完全的连接池。
在微服务模式下更新当前服务的状态(若使用ZooKeeper或Etcd3作为配置中心,需要善后)。
向其他节点发出消息提交当前节点的心跳信息,告诉其他节点当前节点将会停止。
当然清理动作不仅仅是以上的信息,我这边只枚举了一部分。
最后根据日志读者可以简单分析并且彻底理解DeploymentID的用途:
DeploymentID是UUID格式,主要和每一次Deploy行为绑定,因为一次发布可能关联某一类Verticle的多个实例。同一类Verticle(自己编写的Java类)可发布多次,每次发布会产生新的DeploymentID,应该说每个线程会独占一个
DeployementID,这也是为什么反复在强调Verticle究竟是一类还是一个的原因。stop方法在调用了undeploy过后被触发,而且在完成之前打印了所有的日志。
最后,在stop中一定要注意代码本身是异步还是同步,如果使用了异步方法,有可能代码还没执行JVM就关闭了导致关闭状态的不同步,所以在该代码中做好数据同步的完善工作是有必要的,不过这不是一个开发的问题,相反这是架构师应该考虑的点。
4.小结
本章节主要针对Verticle生命周期的细节进行讲解,通过一个完整的例子(zero中的关闭部分远比例子中的复杂)来告诉读者如何设计以及开发Vert.x中的Verticle去触发stop生命周期,使得整个程序变得完整。曾经我遇到过一个朋友,当时一直问我为什么stop的生命周期没触发,当时给我演示时它直接点了IDE中的stop按钮,而当时点击该按钮后stop并没有执行。回到上边使用的手段,因为这个IDE在stop按钮点击时,只有ShutdownHook部分的代码可捕捉到这个行为,而Verticle中的stop生命周期是没有办法捕捉该行为的,所以它的stop并不是自动触发。
[^3]: 《Runtime.addShutdownHook用法》http://kim-miao.iteye.com/blog/1662550, 作者:kim_miao
最后更新于