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=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的類加載器,並設置線程上下文類加載器(具體實現細節,讀者可以參考《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.xml配置文件

  當傳遞參數start的時候,會調用Bootstrap的load方法(見代碼清單5),其作用是用反射調用catalinaDaemon(類型是Catalina)的load方法加載和解析server.xml配置文件,具體細節已在《TOMCAT源碼分析——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容器,有關容器的啟動過程的分析可以參考《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 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通信,socket客戶端如何知道服務端的連接地址與端口呢?下面會慢慢說明。

停止過程分析

我們停止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容器(此時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 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進程作為socket客戶端向服務端發送shutdown命令,兩個進程通過共享server.xml里Server標簽的端口以及地址信息打通了socket的通信。

 

如需轉載,請標明本文作者及出處——作者:jiaan.gja,本文原創首發:博客園,原文鏈接:http://www.cnblogs.com/jiaan-geng/p/4872550.html

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM