你的位置:首页 > Java教程

[Java教程]Tomcat源码分析——启动与停止服务


前言

  熟悉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的命令如下:

sh startup.sh

所以,将从shell脚本startup.sh开始分析Tomcat的启动过程。startup.sh的脚本代码见代码清单1。

代码清单1

os400=falsecase "`uname`" inOS400*) os400=true;;esac# resolve links - $0 may be a softlinkPRG="$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" fidonePRGDIR=`dirname "$PRG"`EXECUTABLE=catalina.sh# Check that target executable existsif $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 evalelse 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 fifiexec "$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

  /**   * Main method, used for testing only.   *   * @param args Command line arguments to be processed   */  public static void main(String args[]) {    if (daemon == null) {      // Don't set daemon until init() has completed      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)的执行步骤如下:

  1. 设置Catalina路径,默认为Tomcat的根目录;
  2. 初始化Tomcat的类加载器,并设置线程上下文类加载器(具体实现细节,读者可以参考《TOMCAT源码分析——类加载体系》一文);
  3. 用反射实例化org.apache.catalina.startup.Catalina对象,并且使用反射调用其setParentClassLoader方法,给Catalina对象设置Tomcat类加载体系的顶级加载器(Java自带的三种类加载器除外)。

代码清单4

 

  /**   * Initialize daemon.   */  public void init()    throws Exception  {    // Set Catalina path    setCatalinaHome();    setCatalinaBase();    initClassLoaders();    Thread.currentThread().setContextClassLoader(catalinaLoader);    SecurityClassLoad.securityClassLoad(catalinaLoader);    // Load our startup class and call its process() method    if (log.isDebugEnabled())      log.debug("Loading startup class");    Class<?> startupClass =      catalinaLoader.loadClass      ("org.apache.catalina.startup.Catalina");    Object startupInstance = startupClass.newInstance();    // Set the shared extensions class loader    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.

  当传递参数start的时候,会调用Bootstrap的load方法(见代码清单5),其作用是用反射调用catalinaDaemon(类型是Catalina)的load方法加载和解析server.

 

代码清单5

 

  /**   * Load daemon.   */  private void load(String[] arguments)    throws Exception {    // Call the load() method    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

  /**   * Start the Catalina daemon.   */  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)的执行步骤如下:

  1. 验证Server容器是否已经实例化。如果没有实例化Server容器,还会再次调用Catalina的load方法加载和解析server.
  2. 启动Server容器,有关容器的启动过程的分析可以参考《TOMCAT源码分析——生命周期管理》一文的内容。
  3. 设置关闭钩子。这么说可能有些不好理解,那就换个说法。Tomcat本身可能由于所在机器断点,程序bug甚至内存溢出导致进程退出,但是Tomcat可能需要在退出的时候做一些清理工作,比如:内存清理、对象销毁等。这些清理动作需要封装在一个Thread的实现中,然后将此Thread对象作为参数传递给Runtime的addShutdownHook方法即可。
  4. 最后调用Catalina的await方法循环等待接收Tomcat的shutdown命令。
  5. 如果Tomcat运行正常且没有收到shutdown命令,是不会向下执行stop方法的,当接收到shutdown命令,Catalina的await方法会退出循环等待,然后顺序执行stop方法停止Tomcat。

代码清单7

  /**   * Start a new server instance.   */  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();    // Start the new server    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 {      // Register shutdown hook      if (useShutdownHook) {        if (shutdownHook == null) {          shutdownHook = new CatalinaShutdownHook();        }        Runtime.getRuntime().addShutdownHook(shutdownHook);                // If JULI is being used, disable JULI's shutdown hook since        // shutdown hooks run in parallel and log messages may be lost        // if JULI's hook completes before the CatalinaShutdownHook()        LogManager logManager = LogManager.getLogManager();        if (logManager instanceof ClassLoaderLogManager) {          ((ClassLoaderLogManager) logManager).setUseShutdownHook(              false);        }      }    } catch (Throwable t) {      // This will fail on JDK 1.2. Ignoring, as Tomcat can run      // fine without the shutdown hook.    }    if (await) {      await();      stop();    }  }

Catalina的await方法(见代码清单8)实际只是代理执行了Server容器的await方法。

代码清单8

  /**   * Await and shutdown.   */  public void await() {    getServer().await();  }

 以Server的默认实现StandardServer为例,其await方法(见代码清单9)的执行步骤如下:

  1. 创建socket连接的服务端对象ServerSocket;
  2. 循环等待接收客户端发出的命令,如果接收到的命令与SHUTDOWN匹配(由于使用了equals,所以shutdown命令必须是大写的),那么退出循环等待。

代码清单9

  public void await() {    // Negative values - don't wait on port - tomcat is embedded or we just don't like ports    if( port == -2 ) {      // undocumented yet - for embedding apps that are around, alive.      return;    }    if( port==-1 ) {      while( true ) {        try {          Thread.sleep( 10000 );        } catch( InterruptedException ex ) {        }        if( stopAwait ) return;      }    }        // Set up a server socket to wait on    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);    }    // Loop waiting for a connection and a valid command    while (true) {      // Wait for the next connection      Socket socket = null;      InputStream stream = null;      try {        socket = serverSocket.accept();        socket.setSoTimeout(10 * 1000); // Ten seconds        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);      }      // Read a set of characters from the socket      StringBuilder command = new StringBuilder();      int expected = 1024; // Cut off to avoid DoS attack      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) // Control character or EOF terminates loop          break;        command.append((char) ch);        expected--;      }      // Close the socket now that we are done with it      try {        socket.close();      } catch (IOException e) {        // Ignore      }      // Match against our command string      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");    }    // Close the server socket and return    try {      serverSocket.close();    } catch (IOException e) {      // Ignore    }  }

 

至此,Tomcat启动完毕。很多人可能会问,执行sh shutdown.sh脚本时,是如何与Tomcat进程通信的呢?如果要与Tomcat的ServerSocket通信,socket客户端如何知道服务端的连接地址与端口呢?下面会慢慢说明。

停止过程分析

我们停止Tomcat的命令如下:

sh shutdown.sh

所以,将从shell脚本shutdown.sh开始分析Tomcat的停止过程。shutdown.sh的脚本代码见代码清单10。

代码清单10

os400=falsecase "`uname`" inOS400*) os400=true;;esac# resolve links - $0 may be a softlinkPRG="$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" fidonePRGDIR=`dirname "$PRG"`EXECUTABLE=catalina.sh# Check that target executable existsif $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 evalelse 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 fifiexec "$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

 

  /**   * Stop the standalone server.   */  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)的执行步骤如下:

  1. 创建Digester解析server.
  2. 从实例化的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 ) {      // Create and execute our Digester      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);      }    }    // Stop the existing server    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)的实现,其执行步骤如下:

  1. 将启动过程中添加的关闭钩子移除。Tomcat启动过程辛辛苦苦添加的关闭钩子为什么又要去掉呢?因为关闭钩子是为了在JVM异常退出后,进行资源的回收工作。主动停止Tomcat时调用的stop方法里已经包含了资源回收的内容,所以不再需要这个钩子了。
  2. 停止Server容器。有关容器的停止内容,请阅读《TOMCAT源码分析——生命周期管理》一文。

代码清单14

  /**   * Stop an existing server instance.   */  public void stop() {    try {      // Remove the ShutdownHook first so that server.stop()       // doesn't get invoked twice      if (useShutdownHook) {        Runtime.getRuntime().removeShutdownHook(shutdownHook);        // If JULI is being used, re-enable JULI's shutdown to ensure        // log messages are not lost        LogManager logManager = LogManager.getLogManager();        if (logManager instanceof ClassLoaderLogManager) {          ((ClassLoaderLogManager) logManager).setUseShutdownHook(              true);        }      }    } catch (Throwable t) {      // This will fail on JDK 1.2. Ignoring, as Tomcat can run      // fine without the shutdown hook.    }    // Shut down the server    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.