• Tomcat7.0源代码分析——启动与停止服务原理


    前言

      熟悉Tomcat的project师们。肯定都知道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=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

        /**
         * 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的类载入器,并设置线程上下文类载入器(详细实现细节,读者能够參考《Tomcat7.0源代码分析——类载入体系》一文)。
    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.xml配置文件

      当传递參数start的时候,会调用Bootstrap的load方法(见代码清单5),其作用是用反射调用catalinaDaemon(类型是Catalina)的load方法载入和解析server.xml配置文件,详细细节已在《Tomcat7.0源代码分析——server.xml文件的载入与解析》一文中详细介绍。有兴趣的朋友能够选择阅读。

     代码清单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.xml,这也说明Tomcat仅仅同意Server容器通过配置在server.xml的方式生成。用户也能够自己实现Server接口创建自己定义的Server容器以代替默认的StandardServer。
    2. 启动Server容器,有关容器的启动过程的分析能够參考《Tomcat7.0源代码分析——生命周期管理》一文的内容。
    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. 循环等待接收client发出的命令,假设接收到的命令与SHUTDOWN匹配(因为使用了equals,所以shutdown命令必须是大写的),那么退出循环等待。

    代码清单9
        public void await() {
            // Negative values - don't wait on port - tomcat is embedded or we just don't like ports gja
            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通信。socketclient怎样知道服务端的连接地址与端口呢?以下会慢慢说明。

    停止过程分析

    我们停止Tomcat的命令例如以下:
    sh shutdown.sh
    所以,将从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
       /**
         * 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.xml文件(此处仅仅解析标签),以构造出Server容器(此时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容器。

      有关容器的停止内容。请阅读《Tomcat7.0源代码分析——生命周期管理》一文。

    代码清单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 jiaan
                    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进程作为socketclient向服务端发送shutdown命令。两个进程通过共享server.xml里Server标签的端口以及地址信息打通了socket的通信。



    后记:个人总结整理的《深入理解Spark:核心思想与源代码分析》一书如今已经正式出版上市,眼下京东、当当、天猫等站点均有销售。欢迎感兴趣的同学购买。


    京东:http://item.jd.com/11846120.html 

    当当:http://product.dangdang.com/23838168.html 


  • 相关阅读:
    基于live555的视频直播 DM368IPNC RTSP分析
    C语言中的二级指针(双指针)
    SQL中的IF ELSE(CASE语句的使用)
    Ubuntu下Postgres安装与配置
    虚拟机+ubuntu 图形界面和终端界面的切换
    中科院 2014年工程硕士入学专业课笔试考场安排
    中科院 2014年GCT考前辅导课程安排
    中科院 工程硕士专业课 复试考试前的辅导安排
    中国科学院大学工程管理与信息技术学院 2014年招收以下八个领域在职工程硕
    中国科学院大 工程管理与信息技术学院 在职工程硕士资格审查和复试重要通知
  • 原文地址:https://www.cnblogs.com/zhchoutai/p/8696903.html
Copyright © 2020-2023  润新知