《深入剖析Tomcat》书中第十五章讲解了如何通过配置XML的方式来配置Tomcat的各个组件,并通过Digester库来解析XML。我们常操作的xml文件应该就是 server.xml这个文件,当在一台机器上部署多个Tomcat时,就必须修改连接器和 [“关闭Tomcat”程序] 监听的端口号,以解决端口冲突,即8005和8080 两个端口号。
<Server port="8005" shutdown="SHUTDOWN" debug="0">
<!-- Uncomment these entries to enable JMX MBeans support -->
<Listener className="org.apache.catalina.mbeans.ServerLifecycleListener"
debug="0"/>
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"
debug="0"/>
<!-- Global JNDI resources -->
<GlobalNamingResources>
<!-- Test entry for demonstration purposes -->
<Environment name="simpleValue" type="java.lang.Integer" value="30"/>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users -->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved">
</Resource>
<ResourceParams name="UserDatabase">
<parameter>
<name>factory</name>
<value>org.apache.catalina.users.MemoryUserDatabaseFactory</value>
</parameter>
<parameter>
<name>pathname</name>
<value>conf/tomcat-users.xml</value>
</parameter>
</ResourceParams>
</GlobalNamingResources>
<Service name="Tomcat-Standalone">
<Connector className="org.apache.catalina.connector.http.HttpConnector"
port="8080" minProcessors="5" maxProcessors="75"
enableLookups="true" redirectPort="8443"
acceptCount="10" debug="0"/>
<Engine name="Standalone" defaultHost="localhost" debug="0">
<Logger className="org.apache.catalina.logger.FileLogger"
prefix="catalina_log." suffix=".txt"
timestamp="true"/>
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
debug="0" resourceName="UserDatabase"/>
<Host name="localhost" debug="0" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Logger className="org.apache.catalina.logger.FileLogger"
directory="logs" prefix="localhost_log." suffix=".txt"
timestamp="true"/>
</Host>
</Engine>
</Service>
</Server>
第十五章的内容就不展开讲了,我们直接来到第十六章:“关闭钩子”。
上一章中,讲到了Tomcat可以通过接收一条“关闭消息”的方式来正常关闭Tomcat(执行StandardServer的stop方法),代码逻辑如下图所示
server.await() 方法监听8005端口
但是正常情况下我们想停止Tomcat时,很少会向Tomcat的8005端口发送一条关闭消息(甚至很多同学都不知道还有这个机制),而是直接kill掉Tomcat对应的java进程,如果直接kill 进程的话,上图中的代码就根本不会走到第3步,“关闭方式1”这条消息也不会打印出来(已亲测过)。
那Tomcat是如何保证关闭流程能够在kill 进程时也能触发的呢?
这就得益于Java的设计了,Java 为程序员提供了一种优雅的方法,可以在关闭JVM进程时执行一些代码,这样就能确保那些负责善后处理的代码肯定能够执行。
在Java中,虚拟机会对两类事件进行响应,然后执行关闭操作:
- 当调用System.exit() 方法或程序的最后一个非守护进程线程退出时,应用程序正常退出。
- 用户突然强制虚拟机中断运行,例如用户按CTRL+C 快捷键或kill掉这个java进程。
虚拟机在执行关闭操作时,会经过以下两个阶段:
- 虚拟机启动所有已经注册的关闭钩子(如果有的话)。关闭钩子是先前已经通过Runtime类注册的线程,所有的关闭钩子会并发执行,直到完成任务;
- 虚拟机根据情况调用所有没有被调用过的终结器 (finalizer)。
本章重点说明第一个阶段,因为该阶段允许程序员告诉虚拟机在应用程序关闭时需要执行哪些清理代码。StandardServer#Stop() 方法就可以安排在这个阶段。
关闭钩子
上面提到的关闭钩子是啥呢?其实很简单,任何一个继承了Thread类的线程类都可以当做一个关闭钩子,钩子怎么生效呢?使用下面这行代码就能生效
Runtime.getRuntime().addShutdownHook(关闭钩子的对象);
下面我们设计一个关闭钩子,来优雅的关闭Tomcat。
先定义钩子类
该类是Bootstrap启动类的一个内部类,继承了Thread类,拥有一个Server组件实例的引用,以便在run方法中调用Server组件的stop()方法。
然后是将这个关闭钩子注册到当前JVM进程中
我这里将它安排在 server组件的 start() 方法后(即Tomcat启动后)注册进来。如下图所示
然后通过Bootstrap启动Tomcat类后,kill 进程,可以看到关闭钩子类的run方法被执行了,其实就是,关闭钩子对应的线程被创建了,然后执行了该线程的run方法。而“关闭方式1”这条日志和添加钩子前一样,不会打印。
测试结束,可以看到咱们自定义的关闭钩子MyShutDownHook成功完成使命:在被杀进程时执行了Server组件的stop() 方法。
注意!关闭钩子是属于Java的,并不单属于Tomcat,所有基于java的程序都可以定义关闭钩子,大家千万不要被局限了。
kill 与 kill -9
kill 的杀,比较有同情心,杀之前问问该进程有没有遗言,有遗言的话就说,于是进程开始巴拉巴拉交代遗言(执行所有关闭钩子),交代完之后,系统刽子手就手起刀落,杀死该进程。特殊情况下,该进程是个话痨,遗言巴拉了半天也不完,于是系统刽子手就不管它了,于是该进程就逃过一截 (キ`゚Д゚´)!! ,这也就是我们常见的 kill 一个进程却 kill 不掉的情况。
kill -9 的杀,是个无情的刽子手,用户一发出 kill -9的执行,系统刽子手二话不说咔嚓一下就将该进程杀死了,管你有什么遗言没有,就是不给你说的机会。所以说执行 kill -9 命令要慎重,避免该进程本来有遗言要说却没说出来,造成对应故障。
Tomcat是怎么使用关闭钩子的
Tomcat的启动入口其实是org.apache.catalina.startup.Catalina类,该类中有一个自定义的关闭钩子,如下所示,功能和我们上面定义的一样,都是去调用server组件的stop方法。
然后在Catalina的start()方法中注册了这个钩子,在注册钩子前其实已经执行了server组件的start()方法,这个我没截出来,你知道即可。
在stop() 方法中做了移除关闭钩子的操作
以上就是Tomcat使用关闭钩子的方式。
为什么在stop() 方法中要移除关闭钩子呢?
这里通过几个实验来说明问题
前提布置
基于我们开篇的自定义钩子的代码,在两条日志中均加上线程名,1.正常关闭Tomcat的日志 2.通过关闭钩子关闭Tomcat的日志
StandardServer的stop()方法加一段线程阻塞的代码,模拟长时间的代码执行。在入口处加上日志
然后进行实验
实验一:仅通过kill 命令关闭Tomcat
关闭钩子触发,stop()方法正常运行,一切正常。
实验二:仅通过发送“shutdown”消息关闭Tomcat
还是通过Stopper类来发送shutdown消息
public class Stopper {
public static void main(String[] args) {
// the following code is taken from the Stop method of
// the org.apache.catalina.startup.Catalina class
int port = 8005;
try {
Socket socket = new Socket("127.0.0.1", port);
OutputStream stream = socket.getOutputStream();
String shutdown = "SHUTDOWN";
for (int i = 0; i < shutdown.length(); i++)
stream.write(shutdown.charAt(i));
stream.flush();
stream.close();
socket.close();
System.out.println("The server was successfully shut down.");
} catch (IOException e) {
System.out.println("Error. The server has not been started.");
}
}
}
结果:正常关闭流程走完,JVM进程即将终止时又触发了关闭钩子,导致 StandardServer#stop() 方法重复执行并报错了。
实验三:发送“shutdown”消息后马上执行 kill 命令
StandardServer#stop() 仍然被执行了两次,导致报错。
实验四:仅发送“shutdown”消息,但是stop方法中加上移除关闭钩子的方法
结果:Tomcat正常关闭,没有报错。可见stop() 方法中移除了关闭钩子后,在终止JVM进程时就不会再触发关闭钩子了。
实验五:stop方法中加上移除关闭钩子的方法,发送“shutdown”消息后马上执行 kill 命令
我来说下现象:发送“shutdown”消息后,stop() 方法开始执行,并移除了关闭钩子。接下来执行 kill 命令,可以看到进程被立刻终止了,并没有等待stop()方法执行完。分析一下执行 kill 命令后的现象,应该是系统发现该进程已经没有关闭钩子了(被刚才的stop方法移除掉了),于是认为这个进程不需要遗言,于是干脆利落的杀死了。
对比实验四的日志也可以发现在结尾少了两条 stop() 方法中的日志。
通过上述五个实验可以得出,在 stop() 方法中移除关闭钩子,可以防止用户守规矩的通过发送“shutdown”消息来关闭Tomcat时,关闭钩子又被触发,stop() 方法被重复执行的问题。但是“shutdown”消息和 kill 命令两种关闭方式最好别一起用,否则很容易达到 kill -9 的效果。
OK,本章的内容就到这里,本章主要介绍了Java中的“关闭钩子”机制,Tomcat运用了这个机制,最大程度的保证了用户在关闭Tomcat时,StandardServer#stop() 方法能够被执行,实现了Tomcat的优雅关闭。下一章我们来研究下Tomcat的启动脚本,敬请期待!
源码分享
https://gitee.com/huo-ming-lu/HowTomcatWorks
本章中自定义关闭钩子的代码在Bootstrap类中