zoukankan      html  css  js  c++  java
  • 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 


  • 相关阅读:
    x64 平台开发 Mapxtreme 编译错误
    hdu 4305 Lightning
    Ural 1627 Join(生成树计数)
    poj 2104 Kth Number(可持久化线段树)
    ural 1651 Shortest Subchain
    hdu 4351 Digital root
    hdu 3221 Bruteforce Algorithm
    poj 2892 Tunnel Warfare (Splay Tree instead of Segment Tree)
    hdu 4031 Attack(BIT)
    LightOJ 1277 Looking for a Subsequence
  • 原文地址:https://www.cnblogs.com/zhchoutai/p/8696903.html
Copyright © 2011-2022 走看看