天天看點

Tomcat7.0源碼分析——啟動與停止服務原理

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/beliefer/article/details/51585006

前言

  熟悉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的類加載器,并設定線程上下文類加載器(具體實作細節,讀者可以參考 《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. 循環等待接收用戶端發出的指令,如果接收到的指令與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非常相似,其中也有兩個主要的變量,分别是:

根據最後一行代碼: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的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容器。有關容器的停止内容,請閱讀 一文。

代碼清單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的通信。

後記:個人總結整理的《深入了解Spark:核心思想與源碼分析》一書現在已經正式出版上市,目前京東、當當、天貓等網站均有銷售,歡迎感興趣的同學購買。

Tomcat7.0源碼分析——啟動與停止服務原理

京東:

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

當當:

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