【Web容器】Tomcat源码分析(4)-启动与停止服务
来源:互联网 发布:java微信 api好友列表 编辑:程序博客网 时间:2024/05/21 19:24
前言
熟悉Tomcat的工程师们,肯定都知道Tomcat是如何启动与停止的。对于startup.sh、startup.bat、shutdown.sh、shutdown.bat等脚本或者批处理命令,大家一定知道改如何使用它,但是它们究竟是如何实现的,尤其是shutdown.sh脚本(或者shutdown.bat)究竟是如何和Tomcat进程通信的呢?本文将通过对Tomcat7.0的源码阅读,深入剖析这一过程。
由于在生产环境中,Tomcat一般部署在Linux系统下,所以本文将以startup.sh和shutdown.sh等shell脚本为准,对Tomcat的启动与停止进行分析。
启动过程分析
我们启动Tomcat的命令如下:
所以,将从shell脚本startup.sh开始分析Tomcat的启动过程。startup.sh的脚本代码见代码清单1。
代码清单1
- os400=false
- case "`uname`" in
- OS400*) os400=true;;
- esac
-
- # resolve links - $0 may be a softlink
- PRG="$0"
-
- while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> .∗$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`/"$link"
- fi
- done
-
- PRGDIR=`dirname "$PRG"`
- EXECUTABLE=catalina.sh
-
- # Check that target executable exists
- if $os400; then
- # -x will Only work on the os400 if the files are:
- # 1. owned by the user
- # 2. owned by the PRIMARY group of the user
- # this will not work if the user belongs in secondary groups
- eval
- else
- if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
- echo "Cannot find $PRGDIR/$EXECUTABLE"
- echo "The file is absent or does not have execute permission"
- echo "This file is needed to run this program"
- exit 1
- fi
- fi
-
- exec "$PRGDIR"/"$EXECUTABLE" start "$@"
代码清单1中有两个主要的变量,分别是:
- PRGDIR:当前shell脚本所在的路径;
- EXECUTABLE:脚本catalina.sh。
根据最后一行代码:exec "$PRGDIR"/"$EXECUTABLE" start "$@",我们知道执行了shell脚本catalina.sh,并且传递参数start。catalina.sh中接收到start参数后的执行的脚本分支见代码清单2。
代码清单2
- elif [ "$1" = "start" ] ; then
-
- # 此处省略参数校验的脚本
-
- shift
- touch "$CATALINA_OUT"
- if [ "$1" = "-security" ] ; then
- if [ $have_tty -eq 1 ]; then
- echo "Using Security Manager"
- fi
- shift
- eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
- -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
- -Djava.security.manager \
- -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
- -Dcatalina.base="\"$CATALINA_BASE\"" \
- -Dcatalina.home="\"$CATALINA_HOME\"" \
- -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
- org.apache.catalina.startup.Bootstrap "$@" start \
- >> "$CATALINA_OUT" 2>&1 "&"
-
- else
- eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
- -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
- -Dcatalina.base="\"$CATALINA_BASE\"" \
- -Dcatalina.home="\"$CATALINA_HOME\"" \
- -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
- org.apache.catalina.startup.Bootstrap "$@" start \
- >> "$CATALINA_OUT" 2>&1 "&"
-
- fi
-
- if [ ! -z "$CATALINA_PID" ]; then
- echo $! > "$CATALINA_PID"
- fi
-
- echo "Tomcat started."
从代码清单2可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数也是start。Bootstrap的main方法的实现见代码清单3。
代码清单3
-
-
-
-
-
- public static void main(String args[]) {
-
- if (daemon == null) {
-
- Bootstrap bootstrap = new Bootstrap();
- try {
- bootstrap.init();
- } catch (Throwable t) {
- t.printStackTrace();
- return;
- }
- daemon = bootstrap;
- }
-
- try {
- String command = "start";
- if (args.length > 0) {
- command = args[args.length - 1];
- }
-
- if (command.equals("startd")) {
- args[args.length - 1] = "start";
- daemon.load(args);
- daemon.start();
- } else if (command.equals("stopd")) {
- args[args.length - 1] = "stop";
- daemon.stop();
- } else if (command.equals("start")) {
- daemon.setAwait(true);
- daemon.load(args);
- daemon.start();
- } else if (command.equals("stop")) {
- daemon.stopServer(args);
- } else {
- log.warn("Bootstrap: command \"" + command + "\" does not exist.");
- }
- } catch (Throwable t) {
- t.printStackTrace();
- }
-
- }
从代码清单3可以看出,当传递参数start的时候,command等于start,此时main方法的执行步骤如下:
步骤一 初始化Bootstrap
Bootstrap的init方法(见代码清单4)的执行步骤如下:
- 设置Catalina路径,默认为Tomcat的根目录;
- 初始化Tomcat的类加载器,并设置线程上下文类加载器(具体实现细节,读者可以参考《Tomcat7.0源码分析——类加载体系》一文);
- 用反射实例化org.apache.catalina.startup.Catalina对象,并且使用反射调用其setParentClassLoader方法,给Catalina对象设置Tomcat类加载体系的顶级加载器(Java自带的三种类加载器除外)。
代码清单4
-
-
-
- public void init()
- throws Exception
- {
-
-
- setCatalinaHome();
- setCatalinaBase();
-
- initClassLoaders();
-
- Thread.currentThread().setContextClassLoader(catalinaLoader);
-
- SecurityClassLoad.securityClassLoad(catalinaLoader);
-
-
- if (log.isDebugEnabled())
- log.debug("Loading startup class");
- Class<?> startupClass =
- catalinaLoader.loadClass
- ("org.apache.catalina.startup.Catalina");
- Object startupInstance = startupClass.newInstance();
-
-
- if (log.isDebugEnabled())
- log.debug("Setting startup class properties");
- String methodName = "setParentClassLoader";
- Class<?> paramTypes[] = new Class[1];
- paramTypes[0] = Class.forName("java.lang.ClassLoader");
- Object paramValues[] = new Object[1];
- paramValues[0] = sharedLoader;
- Method method =
- startupInstance.getClass().getMethod(methodName, paramTypes);
- method.invoke(startupInstance, paramValues);
-
- catalinaDaemon = startupInstance;
-
- }
步骤二 加载、解析server.xml配置文件
当传递参数start的时候,会调用Bootstrap的load方法(见代码清单5),其作用是用反射调用catalinaDaemon(类型是Catalina)的load方法加载和解析server.xml配置文件,具体细节已在《TOMCAT源码分析——SERVER.XML文件的加载与解析》一文中详细介绍,有兴趣的朋友可以选择阅读。
代码清单5
-
-
-
- private void load(String[] arguments)
- throws Exception {
-
-
- String methodName = "load";
- Object param[];
- Class<?> paramTypes[];
- if (arguments==null || arguments.length==0) {
- paramTypes = null;
- param = null;
- } else {
- paramTypes = new Class[1];
- paramTypes[0] = arguments.getClass();
- param = new Object[1];
- param[0] = arguments;
- }
- Method method =
- catalinaDaemon.getClass().getMethod(methodName, paramTypes);
- if (log.isDebugEnabled())
- log.debug("Calling startup class " + method);
- method.invoke(catalinaDaemon, param);
-
- }
步骤三 启动Tomcat
当传递参数start的时候,调用Bootstrap的load方法之后会接着调用start方法(见代码清单6)启动Tomcat,此方法实际是用反射调用了catalinaDaemon(类型是Catalina)的start方法。
代码清单6
-
-
-
- public void start()
- throws Exception {
- if( catalinaDaemon==null ) init();
-
- Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
- method.invoke(catalinaDaemon, (Object [])null);
-
- }
Catalina的start方法(见代码清单7)的执行步骤如下:
- 验证Server容器是否已经实例化。如果没有实例化Server容器,还会再次调用Catalina的load方法加载和解析server.xml,这也说明Tomcat只允许Server容器通过配置在server.xml的方式生成,用户也可以自己实现Server接口创建自定义的Server容器以取代默认的StandardServer。
- 启动Server容器,有关容器的启动过程的分析可以参考《TOMCAT7.0源码分析——生命周期管理》一文的内容。
- 设置关闭钩子。这么说可能有些不好理解,那就换个说法。Tomcat本身可能由于所在机器断点,程序bug甚至内存溢出导致进程退出,但是Tomcat可能需要在退出的时候做一些清理工作,比如:内存清理、对象销毁等。这些清理动作需要封装在一个Thread的实现中,然后将此Thread对象作为参数传递给Runtime的addShutdownHook方法即可。
- 最后调用Catalina的await方法循环等待接收Tomcat的shutdown命令。
- 如果Tomcat运行正常且没有收到shutdown命令,是不会向下执行stop方法的,当接收到shutdown命令,Catalina的await方法会退出循环等待,然后顺序执行stop方法停止Tomcat。
代码清单7
-
-
-
- public void start() {
-
- if (getServer() == null) {
- load();
- }
-
- if (getServer() == null) {
- log.fatal("Cannot start server. Server instance is not configured.");
- return;
- }
-
- long t1 = System.nanoTime();
-
-
- try {
- getServer().start();
- } catch (LifecycleException e) {
- log.error("Catalina.start: ", e);
- }
-
- long t2 = System.nanoTime();
- if(log.isInfoEnabled())
- log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
-
- try {
-
- if (useShutdownHook) {
- if (shutdownHook == null) {
- shutdownHook = new CatalinaShutdownHook();
- }
- Runtime.getRuntime().addShutdownHook(shutdownHook);
-
-
-
-
- LogManager logManager = LogManager.getLogManager();
- if (logManager instanceof ClassLoaderLogManager) {
- ((ClassLoaderLogManager) logManager).setUseShutdownHook(
- false);
- }
- }
- } catch (Throwable t) {
-
-
- }
-
- if (await) {
- await();
- stop();
- }
-
- }
Catalina的await方法(见代码清单8)实际只是代理执行了Server容器的await方法。代码清单8
-
-
-
- public void await() {
-
- getServer().await();
-
- }
以Server的默认实现StandardServer为例,其await方法(见代码清单9)的执行步骤如下:- 创建socket连接的服务端对象ServerSocket;
- 循环等待接收客户端发出的命令,如果接收到的命令与SHUTDOWN匹配(由于使用了equals,所以shutdown命令必须是大写的),那么退出循环等待。
代码清单9
- public void await() {
-
- if( port == -2 ) {
-
- return;
- }
- if( port==-1 ) {
- while( true ) {
- try {
- Thread.sleep( 10000 );
- } catch( InterruptedException ex ) {
- }
- if( stopAwait ) return;
- }
- }
-
-
- ServerSocket serverSocket = null;
- try {
- serverSocket =
- new ServerSocket(port, 1,
- InetAddress.getByName(address));
- } catch (IOException e) {
- log.error("StandardServer.await: create[" + address
- + ":" + port
- + "]: ", e);
- System.exit(1);
- }
-
-
- while (true) {
-
-
- Socket socket = null;
- InputStream stream = null;
- try {
- socket = serverSocket.accept();
- socket.setSoTimeout(10 * 1000);
- stream = socket.getInputStream();
- } catch (AccessControlException ace) {
- log.warn("StandardServer.accept security exception: "
- + ace.getMessage(), ace);
- continue;
- } catch (IOException e) {
- log.error("StandardServer.await: accept: ", e);
- System.exit(1);
- }
-
-
- StringBuilder command = new StringBuilder();
- int expected = 1024;
- while (expected < shutdown.length()) {
- if (random == null)
- random = new Random();
- expected += (random.nextInt() % 1024);
- }
- while (expected > 0) {
- int ch = -1;
- try {
- ch = stream.read();
- } catch (IOException e) {
- log.warn("StandardServer.await: read: ", e);
- ch = -1;
- }
- if (ch < 32)
- break;
- command.append((char) ch);
- expected--;
- }
-
-
- try {
- socket.close();
- } catch (IOException e) {
-
- }
-
-
- boolean match = command.toString().equals(shutdown);
- if (match) {
- log.info(sm.getString("standardServer.shutdownViaPort"));
- break;
- } else
- log.warn("StandardServer.await: Invalid command '" +
- command.toString() + "' received");
-
- }
-
-
- try {
- serverSocket.close();
- } catch (IOException e) {
-
- }
-
- }
至此,Tomcat启动完毕。很多人可能会问,执行sh shutdown.sh脚本时,是如何与Tomcat进程通信的呢?如果要与Tomcat的ServerSocket通信,socket客户端如何知道服务端的连接地址与端口呢?下面会慢慢说明。停止过程分析
我们停止Tomcat的命令如下:
所以,将从shell脚本shutdown.sh开始分析Tomcat的停止过程。shutdown.sh的脚本代码见代码清单10。 代码清单10
- os400=false
- case "`uname`" in
- OS400*) os400=true;;
- esac
-
- # resolve links - $0 may be a softlink
- PRG="$0"
-
- while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> .∗$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`/"$link"
- fi
- done
-
- PRGDIR=`dirname "$PRG"`
- EXECUTABLE=catalina.sh
-
- # Check that target executable exists
- if $os400; then
- # -x will Only work on the os400 if the files are:
- # 1. owned by the user
- # 2. owned by the PRIMARY group of the user
- # this will not work if the user belongs in secondary groups
- eval
- else
- if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
- echo "Cannot find $PRGDIR/$EXECUTABLE"
- echo "The file is absent or does not have execute permission"
- echo "This file is needed to run this program"
- exit 1
- fi
- fi
-
- exec "$PRGDIR"/"$EXECUTABLE" stop "$@"
代码清单10和代码清单1非常相似,其中也有两个主要的变量,分别是:- PRGDIR:当前shell脚本所在的路径;
- EXECUTABLE:脚本catalina.sh。
根据最后一行代码:exec "$PRGDIR"/"$EXECUTABLE" stop "$@",我们知道执行了shell脚本catalina.sh,并且传递参数stop。catalina.sh中接收到stop参数后的执行的脚本分支见代码清单11。代码清单11
- elif [ "$1" = "stop" ] ; then
-
- #省略参数校验脚本
-
- eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_OPTS \
- -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
- -Dcatalina.base="\"$CATALINA_BASE\"" \
- -Dcatalina.home="\"$CATALINA_HOME\"" \
- -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
- org.apache.catalina.startup.Bootstrap "$@" stop
从代码清单11可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数是stop。从代码清单3可以看出,当传递参数stop的时候,command等于stop,此时main方法的执行步骤如下:步骤一 初始化Bootstrap
已经在启动过程分析中介绍, 不再赘述。
步骤二 停止服务
通过调用Bootstrap的stopServer方法(见代码清单12)停止Tomcat,其实质是用反射调用catalinaDaemon(类型是Catalina)的stopServer方法。
代码清单12
-
-
-
- public void stopServer(String[] arguments)
- throws Exception {
-
- Object param[];
- Class<?> paramTypes[];
- if (arguments==null || arguments.length==0) {
- paramTypes = null;
- param = null;
- } else {
- paramTypes = new Class[1];
- paramTypes[0] = arguments.getClass();
- param = new Object[1];
- param[0] = arguments;
- }
- Method method =
- catalinaDaemon.getClass().getMethod("stopServer", paramTypes);
- method.invoke(catalinaDaemon, param);
-
- }
Catalina的stopServer方法(见代码清单13)的执行步骤如下:- 创建Digester解析server.xml文件(此处只解析标签),以构造出Server容器(此时Server容器的子容器没有被实例化);
- 从实例化的Server容器获取Server的socket监听端口和地址,然后创建Socket对象连接启动Tomcat时创建的ServerSocket,最后向ServerSocket发送SHUTDOWN命令。根据代码清单9的内容,ServerSocket循环等待接收到SHUTDOWN命令后,最终调用stop方法停止Tomcat。
代码清单13
- public void stopServer() {
- stopServer(null);
- }
-
- public void stopServer(String[] arguments) {
-
- if (arguments != null) {
- arguments(arguments);
- }
-
- if( getServer() == null ) {
-
- Digester digester = createStopDigester();
- digester.setClassLoader(Thread.currentThread().getContextClassLoader());
- File file = configFile();
- try {
- InputSource is =
- new InputSource("file://" + file.getAbsolutePath());
- FileInputStream fis = new FileInputStream(file);
- is.setByteStream(fis);
- digester.push(this);
- digester.parse(is);
- fis.close();
- } catch (Exception e) {
- log.error("Catalina.stop: ", e);
- System.exit(1);
- }
- }
-
-
- try {
- if (getServer().getPort()>0) {
- Socket socket = new Socket(getServer().getAddress(),
- getServer().getPort());
- OutputStream stream = socket.getOutputStream();
- String shutdown = getServer().getShutdown();
- for (int i = 0; i < shutdown.length(); i++)
- stream.write(shutdown.charAt(i));
- stream.flush();
- stream.close();
- socket.close();
- } else {
- log.error(sm.getString("catalina.stopServer"));
- System.exit(1);
- }
- } catch (IOException e) {
- log.error("Catalina.stop: ", e);
- System.exit(1);
- }
-
- }
最后,我们看看Catalina的stop方法(见代码清单14)的实现,其执行步骤如下:- 将启动过程中添加的关闭钩子移除。Tomcat启动过程辛辛苦苦添加的关闭钩子为什么又要去掉呢?因为关闭钩子是为了在JVM异常退出后,进行资源的回收工作。主动停止Tomcat时调用的stop方法里已经包含了资源回收的内容,所以不再需要这个钩子了。
- 停止Server容器。有关容器的停止内容,请阅读《TOMCAT源码分析——生命周期管理》一文。
代码清单14
-
-
-
- public void stop() {
-
- try {
-
-
- if (useShutdownHook) {
- Runtime.getRuntime().removeShutdownHook(shutdownHook);
-
-
-
- LogManager logManager = LogManager.getLogManager();
- if (logManager instanceof ClassLoaderLogManager) {
- ((ClassLoaderLogManager) logManager).setUseShutdownHook(
- true);
- }
- }
- } catch (Throwable t) {
-
-
- }
-
-
- try {
- getServer().stop();
- } catch (LifecycleException e) {
- log.error("Catalina.stop", e);
- }
-
- }
总结
通过对Tomcat源码的分析我们了解到Tomcat的启动和停止都离不开org.apache.catalina.startup.Bootstrap。当停止Tomcat时,已经启动的Tomcat作为socket服务端,停止脚本启动的Bootstrap进程作为socket客户端向服务端发送shutdown命令,两个进程通过共享server.xml里Server标签的端口以及地址信息打通了socket的通信。
http://blog.csdn.net/beliefer/article/details/51585006
0 0