tomcat源代碼系列(三)--啓動過程

本篇我們來一起分析一下Tomcat的啓動過程,啓動過程涉及到了Tomcat組件的生命週期管理,本文將從 Tomcat組件生命週期管理 , Tomcat啓動的總過程 , Tomcat啓動過程關鍵步驟分析 三個方面來進行描述。

Tomcat組件生命週期管理

在 Tomcat總體結構 (Tomcat源代碼閱讀系列之二) 中,我們列出了Tomcat中Server,Service,Connector,Engine,Host,Context的繼承關係圖,你會發現它們都實現了 org.apache.catalina.Lifecycle 接口,而 org.apache.catalina.util.LifecycleBase 採用了 模板方法模式 來對所有支持生命週期管理的組件的生命週期各個階段進行了總體管理,每個需要生命週期管理的組件只需要繼承這個基類,然後覆蓋對應的鉤子方法即可完成相應的生命週期階段性的管理工作。 下面我們首先來看看 org.apache.catalina.Lifecycle 接口的定義,它的類圖如下圖所示:

這裏寫圖片描述

從上圖我們可以清楚的看到LifeCycle中主要有四個生命週期階段,它們分別是init(初始化),start(啓動),stop(停止),destory(銷燬)。知道了這四個生命週期階段以後,咋們就來看看 org.apache.catalina.util.LifecycleBase 是如何實現 模板方法模式 的。 那接下來我們就來看看 org.apache.catalina.util.LifecycleBase 類的定義,它的類圖如下所示:
這裏寫圖片描述

上圖中用紅色標註的四個方法就是 模板方法模式 中的鉤子方法,子類可以通過實現鉤子方法來納入到基類已經流程化好的生命週期管理中。
上面我們對LifeCycle和LifeCycleBase有了一個總體的認識,接下來,我們通過查看 org.apache.catalina.util.LifecycleBase 的源代碼來具體的分析一下。 咋們首先來看 org.apache.catalina.util.LifecycleBase 的init方法的實現。

org.apache.catalina.util.LifecycleBase#init

@Override
public final synchronized void init() throws LifecycleException {
        // 1
        if (!state.equals(LifecycleState.NEW)) {
            invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
        }
        setStateInternal(LifecycleState.INITIALIZING, null, false);

        try {
            // 2 
            initInternal();
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            setStateInternal(LifecycleState.FAILED, null, false);
            throw new LifecycleException(
                    sm.getString("lifecycleBase.initFail",toString()), t);
        }
        // 3 
        setStateInternal(LifecycleState.INITIALIZED, null, false);
}

下面我們逐一來分析一下上述代碼中標註了數字的地方:

標註1的代碼首先檢測當前組件的狀態是不是 NEW (新建),如果不是就調用 org.apache.catalina.util.LifecycleBase#invalidTransition 方法來將當前的狀態轉換過程終止,而 invalidTransition 的實現是拋出了 org.apache.catalina.LifecycleException 異常。接着調用了 setStateInternal 方法將狀態設置爲INITIALIZING(正在初始化)
標註2的代碼就是init模板方法的鉤子,子類可以通過實現 protected abstract void initInternal() throws LifecycleException; 方法來納入初始化的流程。
標註3的代碼將組件的狀態改爲 INITIALIZED (已初始化)。

上面我們分析了init模板方法,接下來我們再看看start方法具體做了什麼事情。start的代碼如下
org.apache.catalina.util.LifecycleBase#start

public final synchronized void start() throws LifecycleException {
    // 1
    if (LifecycleState.STARTING_PREP.equals(state) ||
            LifecycleState.STARTING.equals(state) ||
            LifecycleState.STARTED.equals(state)) {

        if (log.isDebugEnabled()) {
            Exception e = new LifecycleException();
            log.debug(sm.getString("lifecycleBase.alreadyStarted",
                    toString()), e);
        } else if (log.isInfoEnabled()) {
            log.info(sm.getString("lifecycleBase.alreadyStarted",
                    toString()));
        }

        return;
    }

    // 2
    if (state.equals(LifecycleState.NEW)) {
        init();
    } else if (state.equals(LifecycleState.FAILED)){
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
            !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(Lifecycle.BEFORE_START_EVENT);
    }
    // 3
    setStateInternal(LifecycleState.STARTING_PREP, null, false);

    try {
    //4   
        startInternal();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, null, false);
        throw new LifecycleException(
                sm.getString("lifecycleBase.startFail",toString()), t);
    }

    // 5
    if (state.equals(LifecycleState.FAILED) ||
            state.equals(LifecycleState.MUST_STOP)) {
        stop();
    } else {
        // Shouldn't be necessary but acts as a check that sub-classes are
        // doing what they are supposed to.
        if (!state.equals(LifecycleState.STARTING)) {
            invalidTransition(Lifecycle.AFTER_START_EVENT);
        }

        setStateInternal(LifecycleState.STARTED, null, false);
    }
}

下面我們逐一來分析一下上述代碼中標註了數字的地方:

標註1的代碼檢測當前組件的狀態是不是 STARTING_PREP (準備啓動), STARTING (正在啓動), STARTED (已啓動).如果是這三個狀態中的任何一個,則拋出 LifecycleException 。
標註2的代碼的檢查其實主要是爲了保證組件狀態的完整性,在正常啓動的流程中,應該是不會出現沒有初始化就啓動,或者還沒啓動就已經失敗的情況。
標註3的代碼設置組件的狀態爲 STARTING_PREP (準備啓動狀態)
標註4的代碼是start模板方法的鉤子方法,子類通過實現 org.apache.catalina.util.LifecycleBase#startInternal 這個方法來納入到組件啓動的流程中來。
標註5的代碼做了一些狀態檢查,然後最終將組件的狀態設置爲 STARTED (已啓動)

上面我們分析了init和start方法的流程,對於stop和destroy方法的總體過程是類似的,大家可以自己閱讀一下,但是通過上面的分析,我們可以得出生命週期方法的總體的骨架,如果用僞代碼來表示可以簡化爲如下:
org.apache.catalina.util.LifecycleBase#lifeCycleMethod

  public final synchronized void lfieCycleMethod() throws LifecycleException {
        stateCheck();//狀態檢查
        //設置爲進入相應的生命週期之前的狀態
        setStateInternal(LifecycleState.BEFORE_STATE, null, false);
        lfieCycleMethodInternal();//鉤子方法
        //進入相應的生命週期之後的狀態
        setStateInternal(LifecycleState.AFTER_STATE, null, false);
    }

Tomcat啓動的總過程

通過上面的介紹,我們總體上清楚了各個組件的生命週期的各個階段具體都是如何運作的。接下來我們就來看看,Tomcat具體是如何一步步啓動起來的。我們都知道任何Java程序都有一個main函數入口,Tomcat中的main入口是 org.apache.catalina.startup.Bootstrap#main ,下面我們就來分析一下它的代碼:

org.apache.catalina.startup.Bootstrap#main

public static void main(String args[]) {

    if (daemon == null) {
        // Don't set daemon until init() has completed
        // 1 
        Bootstrap bootstrap = new Bootstrap();
        try {
            // 2
            bootstrap.init();
        } catch (Throwable t) {
            handleThrowable(t);
            t.printStackTrace();
            return;
        }
        // 3
        daemon = bootstrap;
    } else {
        // When running as a service the call to stop will be on a new
        // thread so make sure the correct class loader is used to prevent
        // a range of class not found exceptions.
        Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
    }

    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")) {
            // 4
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (null==daemon.getServer()) {
                System.exit(1);
            }
            System.exit(0);
        } else {
            log.warn("Bootstrap: command \"" + command + "\" does not exist.");
        }
    } catch (Throwable t) {
        // Unwrap the Exception for clearer error reporting
        if (t instanceof InvocationTargetException &&
                t.getCause() != null) {
            t = t.getCause();
        }
        handleThrowable(t);
        t.printStackTrace();
        System.exit(1);
    }

}

下面我們逐一來分析一下上述代碼中標註了數字的地方:

標註1的代碼初始化了自舉類的實例,標註2的代碼對BootStrap實例進行了初始化,標註3的代碼將實例賦值給了daemon。
標註4的代碼首先調用了BootStrap的load方法,然後調用了start方法。

接下來我們分別分析一下BootStrap的init,load,start方法具體做了哪些工作

BootStrap#init方法

首先來看 org.apache.catalina.startup.Bootstrap#init 方法,它的代碼如下:

org.apache.catalina.startup.Bootstrap#init

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");
    // 1
    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);
    // 2
    method.invoke(startupInstance, paramValues);
    // 3
    catalinaDaemon = startupInstance;

}

下面我們重點逐一來分析一下上述代碼中標註了數字的地方:

標註1的代碼通過反射實例化了 org.apache.catalina.startup.Catalina 類的實例;
標註2的代碼調用了Catalina實例的setParentClassLoader方法設置了父親ClassLoader,對於ClassLoader方面的內容,我們在本系列的後續文章再來看看。標註3的代碼將Catalina實例賦值給了Bootstrap實例的catalinaDaemon.

BootStrap#load

接下來我們再來看看 org.apache.catalina.startup.Bootstrap#load 方法,通過查看源代碼,我們知道此方法通過反射調用了 org.apache.catalina.startup.Catalina#load 方法,那我們就來看看Catalina的load方法,Catalina#load方法代碼如下:

org.apache.catalina.startup.Catalina#load

public void load() {

    // 1 
    Digester digester = createStartDigester();

    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
        file = configFile();
        inputStream = new FileInputStream(file);
        inputSource = new InputSource(file.toURI().toURL().toString());
    } catch (Exception e) {
        if (log.isDebugEnabled()) {
            log.debug(sm.getString("catalina.configFail", file), e);
        }
    }



    try {
        inputSource.setByteStream(inputStream);
        digester.push(this);
        digester.parse(inputSource);
    } catch (SAXParseException spe) {
        log.warn("Catalina.start using " + getConfigFile() + ": " +
                spe.getMessage());
        return;
    } catch (Exception e) {
        log.warn("Catalina.start using " + getConfigFile() + ": " , e);
        return;
    } finally {
        try {
            inputStream.close();
        } catch (IOException e) {
            // Ignore
        }
    }


    getServer().setCatalina(this);

    // Stream redirection
    initStreams();

    // Start the new server
    try {
        // 2
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error("Catalina.start", e);
        }

    }

}

上面的代碼,我只保留了主流程核心的代碼,下面我們重點逐一來分析一下上述代碼中標註了數字的地方:

標註1的代碼創建Digester實例解析”conf/server.xml”文件
標註2的代碼最終調用了StandardServer的init方法。

大家可以自行查看下源代碼,我們會發現如下的一個調用流程:

init call stack

org.apache.catalina.core.StandardServer#init
->org.apache.catalina.core.StandardService#init
-->org.apache.catalina.connector.Connector#init
-->org.apache.catalina.core.StandardEngine#init

因爲StandardService,Connector,StandardEngine實現了LifeCycle接口,因此符合我們上文所獲的生命週期的管理,最終都是通過他們自己實現的initInternal方法進行初始化

讀到這裏的時候,我想大家應該和我一樣,以爲StandardEngine#init方法會調用StandardHost#init方法,但是當我們查看StandardEngine#init方法的時候,發現並沒有進行StandardHost的初始化,它到底做了什麼呢?讓我們來具體分析一下,我們首先拿StanderEngine的繼承關係圖來看下:

這裏寫圖片描述

通過上圖以及前面說的LifeCyecle的模板方法模式,我們知道StandardEngine的初始化鉤子方法initInternal方法最終調用了ContainerBase的initInternal方法,那我們拿ContainerBase#initInternal方法的代碼看看:

org.apache.catalina.core.ContainerBase#initInternal

protected void initInternal() throws LifecycleException {
    BlockingQueue<Runnable> startStopQueue =
        new LinkedBlockingQueue<Runnable>();
    startStopExecutor = new ThreadPoolExecutor(
            getStartStopThreadsInternal(),
            getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
            startStopQueue,
            new StartStopThreadFactory(getName() + "-startStop-"));
    startStopExecutor.allowCoreThreadTimeOut(true);
    super.initInternal();
}

我們可以看到StandardEngine的初始化僅僅是創建了一個ThreadPoolExecutor,當看到這裏的時候,筆者當時也納悶了,StandardEngine#init竟然沒有調用StandardHost#init方法,那麼StandardHost的init方法是什麼時候被調用的呢?遇到這種不知道到底方法怎麼調用的時候怎麼辦呢?筆者介紹個方法給大家。我們現在需要知道StandardHost#init方法何時被調用的,而我們知道init最終會調用鉤子的initInternal方法,因此這個時候,我們可以在StandardHost中override initInternal方法,增加了實現方法以後,有兩種方法可以用,一種就是設置個斷點debug一下就可以看出線程調用棧了,另外一種就是在新增的方法中打印出調用棧。筆者這裏採用第二種方法,我們增加如下的initInternal方法到StandardHost中:

org.apache.catalina.core.StandardHost#initInternal

protected void initInternal() throws LifecycleException {
    Throwable ex = new Throwable();
    StackTraceElement[] stackElements = ex.getStackTrace();
    if (stackElements != null) {
        for (int i = stackElements.length - 1; i >= 0; i--) {
            System.out.print(stackElements[i].getClassName() + "\t");
            System.out.print(stackElements[i].getMethodName() + "\t");
            System.out.print(stackElements[i].getFileName() + "\t");
            System.out.println(stackElements[i].getLineNumber());
        }
    }
    super.initInternal();
}

上面的代碼將會打印出方法調用堆棧,對於調試非常有用,上面的方法運行以後在控制檯打印出瞭如下的堆棧信息:

stack info

java.lang.Thread    run  Thread.java   680
java.util.concurrent.ThreadPoolExecutor$Worker run  ThreadPoolExecutor.java   918
java.util.concurrent.ThreadPoolExecutor$Worker runTask  ThreadPoolExecutor.java   895
java.util.concurrent.FutureTask   run  FutureTask.java   138
java.util.concurrent.FutureTask$Sync   innerRun FutureTask.java   303
org.apache.catalina.core.ContainerBase$StartChild   call ContainerBase.java    1549
org.apache.catalina.core.ContainerBase$StartChild   call ContainerBase.java    1559
org.apache.catalina.util.LifecycleBase start    LifecycleBase.java    139
org.apache.catalina.util.LifecycleBase init LifecycleBase.java    102
org.apache.catalina.core.StandardHost  initInternal StandardHost.java 794

通過控制檯的信息,我們看到是StartChild#call方法調用的,而我們查看StartChild#call方法其實是在StandardEngine的startInternal方法中通過異步線程池去初始化子容器。因此到這裏我們就理清楚了,StarndardHost的init方法是在調用start方法的時候被初始化。那麼接下來我們就來看看,start方法的整體調用流程。

BootStrap#start

採用分析load方法一樣的方法,經過對BootStrap#start的分析,我們最終可以得到得到如下的調用鏈:

org.apache.catalina.startup.Bootstrap#start call stack

org.apache.catalina.startup.Bootstrap#start
->org.apache.catalina.startup.Catalina#start 通過反射調用
-->org.apache.catalina.core.StandardServer#start
--->org.apache.catalina.core.StandardService#start
---->org.apache.catalina.core.StandardEngine#start
---->org.apache.catalina.Executor#start
---->org.apache.catalina.connector.Connector#start

綜合上文的描述我們總體得到如下的調用鏈:
org.apache.catalina.startup.Bootstrap#main call stack

org.apache.catalina.startup.Bootstrap#main
->org.apache.catalina.startup.Bootstrap#init
->org.apache.catalina.startup.Bootstrap#load
-->org.apache.catalina.startup.Catalina#load
--->org.apache.catalina.core.StandardServer#init
---->org.apache.catalina.core.StandardService#init
----->org.apache.catalina.connector.Connector#init
----->org.apache.catalina.core.StandardEngine#init
->org.apache.catalina.startup.Bootstrap#start
-->org.apache.catalina.startup.Catalina#start 通過反射調用
--->org.apache.catalina.core.StandardServer#start
---->org.apache.catalina.core.StandardService#start
----->org.apache.catalina.core.StandardEngine#start
----->org.apache.catalina.Executor#start
----->org.apache.catalina.connector.Connector#start

通過上面的分析我們已經搞清楚了Tomcat啓動的總體的過程,但是有一些關鍵的步驟,我們還需要進行進一步的深入探究。let’s do it.

Tomcat啓動過程關鍵步驟分析

Connector#init

我們首先來看一下 org.apache.catalina.connector.Connector#init ,我們知道Connector的生命週期也是通過LifeCycle的模板方法模式來管理的,那麼我們只需要查看一下它的initInternal方法即可知道它是如何初始化的。接下來我們就來看一下initInternal方法,代碼如下:

org.apache.catalina.connector.Connector#initInternal

protected void initInternal() throws LifecycleException {

        super.initInternal();

        // Initialize adapter
        adapter = new CoyoteAdapter(this);
        protocolHandler.setAdapter(adapter);

        // Make sure parseBodyMethodsSet has a default
        if( null == parseBodyMethodsSet ) {
            setParseBodyMethods(getParseBodyMethods());
        }

        if (protocolHandler.isAprRequired() &&
                !AprLifecycleListener.isAprAvailable()) {
            throw new LifecycleException(
                    sm.getString("coyoteConnector.protocolHandlerNoApr",
                            getProtocolHandlerClassName()));
        }

        try {
            // 1 
            protocolHandler.init();
        } catch (Exception e) {
            throw new LifecycleException
                (sm.getString
                 ("coyoteConnector.protocolHandlerInitializationFailed"), e);
        }

        // Initialize mapper listener
        mapperListener.init();
    }

上面代碼中,本文最關心的是標註了1的地方,這個地方調用了 org.apache.coyote.ProtocolHandler#init 方法,而ProtocolHandler是在Connector的構造函數中初始化,而Connector的構造函數又是Digester類解析conf/server.xml的時候調用的,明白了這點,我們在來具體看看Connector構造函數中調用的一個核心的方法setProtocol方法,下面是其代碼:

org.apache.catalina.connector.Connector#setProtocol

public void setProtocol(String protocol) {

        if (AprLifecycleListener.isAprAvailable()) {
            //這裏統一使用AprEndpoint
            if ("HTTP/1.1".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11AprProtocol");   //Http11AprProtocol$Http11ConnectionHandler
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.ajp.AjpAprProtocol");     //AjpAprProtocol$AjpConnectionHandler
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
            } else {
                setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11AprProtocol");
            }
        } else {
            // 1 
            if ("HTTP/1.1".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.http11.Http11Protocol");  //Http11Protocol$Http11ConnectionHandler
            } else if ("AJP/1.3".equals(protocol)) {
                setProtocolHandlerClassName
                    ("org.apache.coyote.ajp.AjpProtocol");    //AjpProtocol$AjpConnectionHandler
            } else if (protocol != null) {
                setProtocolHandlerClassName(protocol);
            }
        }
}

從setProtocol的代碼中,我們可以看出主要邏輯分爲了兩塊,一種情況是使用 APR(Apache Portable Runtime) ,另外一種是不使用APR的情況。缺省情況下不採用APR庫,這樣的話,代碼會走到標註1的代碼分支,這裏通過協議的不同,最終初始化了不同的類。如果是http1.1協議就採用 org.apache.coyote.http11.Http11Protocol ,如果是AJP(Apache Jserv Protocol)協議,就採用 org.apache.coyote.ajp.AjpProtocol 類,下面我們來看一下Http11Protocol和AjpProtocol的繼承關係圖如下:

這裏寫圖片描述

這裏寫圖片描述

通過上圖我們可以看到它們都繼承了公共的基類 org.apache.coyote.AbstractProtocol ,而它們自己的init方法最終其實都是調用了AbstractProtocol的init方法,通過查看AbstractProtocol#init代碼,我們可以看到最終是調用了 org.apache.tomcat.util.net.AbstractEndpoint#init ,而AbstractEndpoint的實例化操作是在實例化AjpProtocol和Http11Protocol的時候在其構造函數中實例化的,而AjpProtocol和Http11Protocol構造函數中,其實都是初始化了 org.apache.tomcat.util.net.JIoEndpoint 類,只不過根據是http協議還是AJP協議,它們具有不同的連接處理類。其中Http11Protocol的連接處理類爲 org.apache.coyote.http11.Http11Protocol.Http11ConnectionHandler ,而連接處理類爲 org.apache.coyote.ajp.AjpProtocol.AjpConnectionHandler ,因此到這裏我們基本清楚了Connector的初始化流程,總結如下:
Connect init 採用APR的情況

//1 HTTP/1.1協議連接器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.http11.Http11AprProtocol#init
-->org.apache.tomcat.util.net.AprEndpoint#init
(org.apache.coyote.http11.Http11AprProtocol.Http11ConnectionHandler)


// 2 AJP/1.3協議連接器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.ajp.AjpAprProtocol#init
-->org.apache.tomcat.util.net.AprEndpoint#init
(org.apache.coyote.ajp.AjpAprProtocol.AjpConnectionHandler)

Connector init 不採用APR的情況

// 1 HTTP/1.1協議連接器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.http11.Http11Protocol#init
-->org.apache.tomcat.util.net.JIoEndpoint#init
(org.apache.coyote.http11.Http11Protocol.Http11ConnectionHandler)

// 2 AJP/1.3協議連接器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.ajp.AjpProtocol#init
-->org.apache.tomcat.util.net.JIoEndpoint#init
(org.apache.coyote.ajp.AjpProtocol.AjpConnectionHandler)

這裏需要注意,除了JIoEndpoint外,還有NIoEndpoint,對於Tomcat7.0.24的代碼,並沒有採用NIOEndPoint,NIOEndpoint採用了NIO的方式進行Socket的處理。

最後,咋們再來看看org.apache.tomcat.util.net.JIoEndpoint#init的初始化過程,我們首先來看一下JIoEndpoint的繼承關係圖如下

這裏寫圖片描述

通過上圖我們知道JIoEndpoint繼承了AbstractEndpoint,而通過查看源碼可知,JIoEndpoint沒有實現自己的init方法,它默認採用了父類的init方法,那麼我們就來看看AbstractEndpoint的init,它的代碼如下:

org.apache.tomcat.util.net.AbstractEndpoint#init

 public final void init() throws Exception {
        if (bindOnInit) {
            bind();
            bindState = BindState.BOUND_ON_INIT;
        }
}

通過查看上面的代碼可知,因爲bindOnInit默認是true,所以init調用了bind方法,而bind方法是抽象方法,最終由JIoEndpoint來實現,代碼如下:

org.apache.tomcat.util.net.JIoEndpoint#bind

@Override
public void bind() throws Exception {

        // Initialize thread count defaults for acceptor
        if (acceptorThreadCount == 0) {
            acceptorThreadCount = 1;
        }
        // Initialize maxConnections
        if (getMaxConnections() == 0) {
            // User hasn't set a value - use the default
            setMaxConnections(getMaxThreadsExecutor(true));
        }

        if (serverSocketFactory == null) {
            if (isSSLEnabled()) {
                serverSocketFactory =
                    handler.getSslImplementation().getServerSocketFactory(this);
            } else {
                serverSocketFactory = new DefaultServerSocketFactory(this);
            }
        }

        if (serverSocket == null) {
            try {
                if (getAddress() == null) {
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog());
                } else {
                    serverSocket = serverSocketFactory.createSocket(getPort(),
                            getBacklog(), getAddress());
                }
            } catch (BindException orig) {
                String msg;
                if (getAddress() == null)
                    msg = orig.getMessage() + " <null>:" + getPort();
                else
                    msg = orig.getMessage() + " " +
                            getAddress().toString() + ":" + getPort();
                BindException be = new BindException(msg);
                be.initCause(orig);
                throw be;
            }
        }

}

通過上面代碼可以看出,最終是調用了 org.apache.tomcat.util.net.ServerSocketFactory#createSocket 方法創建一個 java.net.ServerSocket ,並綁定在conf/server.xml中Connector中配置的端口。

綜上我們可以得出如下結論:

Connector#init的時候,無論是AJP還是HTTP最終其實是調用了JioEndpoint的初始化,默認情況在初始化的時候就會創建java.net.ServerSocket綁到到配置的端口上。

Connector#start

接着我們再來分析一下Connector#start,因爲Connector符合LifeCycle模板方法生命週期管理的機制,因此它的start最終會調用startInternal,org.apache.catalina.connector.Connector#startInternal代碼如下

org.apache.catalina.connector.Connector#startInternal

protected void startInternal() throws LifecycleException {

    // Validate settings before starting
    if (getPort() < 0) {
        throw new LifecycleException(sm.getString(
                "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
    }

    setState(LifecycleState.STARTING);

    try {
        protocolHandler.start();
    } catch (Exception e) {
        String errPrefix = "";
        if(this.service != null) {
            errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
        }

        throw new LifecycleException
            (errPrefix + " " + sm.getString
             ("coyoteConnector.protocolHandlerStartFailed"), e);
    }

    mapperListener.start();
}

通過上面的代碼,我們可以清晰的看到最終調用了protocolHandler.start(),而根據Connector#init流程的分析,這裏會分是否採用APR,默認是不採用APR的,這裏會根據不同的協議(AJP,HTTP)來調用對應的org.apache.coyote.ProtocolHandler#start. 其中AJP會採用org.apache.coyote.ajp.AjpProtocol,HTTP協議採用org.apache.coyote.http11.Http11Protocol,而無論是AjpProtocol還是Http11Protocol都會調用JIoEndpoint的方法,那麼接下來我們就來看看JioEndpoint的start方法,它的代碼如下:

org.apache.tomcat.util.net.JIoEndpoint#startInternal

public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            // Create worker collection
            if (getExecutor() == null) {
                createExecutor();
            }

            initializeConnectionLatch();

            startAcceptorThreads();

            // Start async timeout thread
            Thread timeoutThread = new Thread(new AsyncTimeout(),
                    getName() + "-AsyncTimeout");
            timeoutThread.setPriority(threadPriority);
            timeoutThread.setDaemon(true);
            timeoutThread.start();
        }
}

從上面的代碼可以看出,啓動了Acceptor線程和AsyncTimeout線程,首先來看看Acceptor線程,我們再來看看startAcceptorThreads方法,代碼如下:

org.apache.tomcat.util.net.AbstractEndpoint#startAcceptorThreads

protected final void startAcceptorThreads() {
        int count = getAcceptorThreadCount();
        acceptors = new Acceptor[count];

        for (int i = 0; i < count; i++) {
            acceptors[i] = createAcceptor();
            String threadName = getName() + "-Acceptor-" + i;
            acceptors[i].setThreadName(threadName);
            Thread t = new Thread(acceptors[i], threadName);
            t.setPriority(getAcceptorThreadPriority());
            t.setDaemon(getDaemon());
            t.start();
        }
}

通過上面的代碼,我們可以看出其實是通過 org.apache.tomcat.util.net.AbstractEndpoint.Acceptor 這個Runable接口的實現類來啓動線程,接下來我們就來看看Acceptor#run方法,通過查看run方法,它裏面其實就是調用了 java.net.ServerSocket#accept 的方法來接受一個Socket連接。

啓動完了Acceptor線程以後,接着就會啓動AsyncTimeout線程,而這裏面需要注意的時候,無論是Acceptor還是AsyncTimeout線程,它們都是Daemon線程,而設置爲Daemon的原因,我們會在下篇Tomcat的關閉中進行說明。

StandardEngine#start

從本文上面的分析中,我們得知StandardEngine繼承了ContainerBase,而StandardEngine的startInternal鉤子方法也僅僅是調用了父類ContainerBase的startInternal方法,那接下來我們分析一下ContainerBase的startInternal方法,代碼如下:
org.apache.catalina.core.ContainerBase#startInternal

protected synchronized void startInternal() throws LifecycleException {

    // Start our child containers, if any
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<Future<Void>>();
    for (int i = 0; i < children.length; i++) {
        results.add(startStopExecutor.submit(new StartChild(children[i])));
    }

    boolean fail = false;
    for (Future<Void> result : results) {
        try {
            result.get();
        } catch (Exception e) {
            log.error(sm.getString("containerBase.threadedStartFailed"), e);
            fail = true;
        }

    }
    if (fail) {
        throw new LifecycleException(
                sm.getString("containerBase.threadedStartFailed"));
    }


    setState(LifecycleState.STARTING);

}

我們刪除了對本文的分析不相關的代碼,只留下一些核心的代碼,我們可以看到通過startStopExecutor異步的對子容器進行了啓動,然後設置狀態爲 STARTING 的狀態。而startStopExecutor是在容器的initInternal方法中進行初始化好的,接下來我們就來看看StartChild,StardChild的代碼如下:

org.apache.catalina.core.ContainerBase.StartChild

private static class StartChild implements Callable<Void> {

        private Container child;

        public StartChild(Container child) {
            this.child = child;
        }

        @Override
        public Void call() throws LifecycleException {
            child.start();
            return null;
        }
}

通過上面的代碼,我們可以看到StartChild實現了Callable接口,實現這個接口的類可以將其放到對應的executor中執行(對於executor不熟悉的童鞋可以去看一下相關的文章,本文不做介紹),StartChild在運行的時候就會調用到子容器的start方法,而此時的父容器是StandardEngine,子容器就是StandardHost,接下來我們就來看看StandardHost的啓動過程。通過前面對於init流程的分析,我們知道StandardHost不是在StandardEngine#init的時候初始化,因此在執行StandardHost#start的時候,要首先進行init方法的調用,具體的代碼如下:
org.apache.catalina.util.LifecycleBase#start

public final synchronized void start() throws LifecycleException {


    if (state.equals(LifecycleState.NEW)) {
        init(); //因爲此時的StandardHost還沒有初始化,因此會走到這一步代碼
    } else if (state.equals(LifecycleState.FAILED)){
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
            !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(Lifecycle.BEFORE_START_EVENT);
    }

    setStateInternal(LifecycleState.STARTING_PREP, null, false);

    try {
        startInternal();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, null, false);
        throw new LifecycleException(
                sm.getString("lifecycleBase.startFail",toString()), t);
    }

   setStateInternal(LifecycleState.STARTED, null, false);

}

上面省略了部分不相關的代碼,在調用中我們可以清楚的看到,對於StandardHost的初始化,是在start的時候進行的。那接下來我們在來看一下StandardHost的init方法,通過查看代碼,我們發現StandardHost本身沒有實現initInternal的鉤子方法,也就意味着最終初始化會調用ContainerBase#initInternal方法,而通過上文的描述,我們已經清楚ContainerBase#initInternal主要是初始化了一個startStopExecutor,這個線程池主要是爲了異步的初始化子容器來用的。

我們知道StandardEngine初始化的時候,也是初始化了一個線程池,而StandardHost也初始化了一個線程池,他們的不同點在與創建線程的工廠方法不同,在採用缺省配置的情況下,StandardEngine的線程池中的線程是以 Catalina-startStop 的形式命名的,而StandardHost是以 localhost-startStop 的方式進行命名的。大家注意區分。

StandardHost#start調用init方法初始化完StandardHost以後,會調用鉤子的startInternal方法,而startInternal方法又是調用了ContainerBased#startInternal方法,而ContainerBase#startInternal方法最終又會去啓動子容器的,對於StandardHost來說,子容器就是StandardContext。 因此分析到這裏我們可以得出如下結論:

對於StandardEngine,StandardHost的啓動,父容器在init的時候創建一個啓動和停止子容器的線程池,然後父容器啓動的時候首先通過異步的方式將子容器的啓動通過 org.apache.catalina.core.ContainerBase.StartChild 提交到父容器中對應的線程池中進行啓動,而子容器啓動的時候首先會初始化,然後再啓動。

另外這裏還需要注意一點就是,StandEngine#start的時候,最終調用了ContainerBase#startInternal方法,而ContainerBase#startInternal的最後,調用了threadStart(),我們來看看它的代碼如下:

org.apache.catalina.core.ContainerBase#threadStart

protected void threadStart() {

        if (thread != null)
            return;
        if (backgroundProcessorDelay <= 0)
            return;

        threadDone = false;
        String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
        thread = new Thread(new ContainerBackgroundProcessor(), threadName);
        thread.setDaemon(true);
        thread.start();

}

上面的代碼,首先會判斷backgroundProcessorDelay是否小於0,而這個值默認情況下是-1,也就意味這後面的代碼不會運行,而對於StandardEngine來說,它將backgroundProcessorDelay的值在構造函數中賦值爲了10,這樣的話,當StandardEngine啓動的時候,就會啓動名稱爲“ContainerBackgroundProcessor[StandardEngine[Catalina]]”的線程。

經過上面的分析,我們已經清楚了StandardEngine啓動的過程了,但是我們還有一個地方需要進一步的分析。因爲上面的分析我們僅僅只是分析了容器通過conf/server.xml配置文件的配置結構進行的啓動,而我們都知道 CATALINA-HOME/webapps/ 中的應用也是需要啓動的,那麼webapps目錄的應用又是如何啓動的呢?我們下面來分析一下,通過Tomcat總體結構的描述,我們已經知道,webapps目錄下面的應用其實是屬於Context的,而Context對應Tomcat中的StandardContext類,因此我們就知道應該對誰下手了,知道了目標以後,咋們還是採用之前的那種方式,要麼debug,要麼打印調用棧,這裏我們還是通過打印調用棧的方式進行,我們在 org.apache.catalina.core.StandardContext#initInternal 中增加打印調用棧的方法,具體代碼如下:

org.apache.catalina.core.StandardContext#initInternal

protected void initInternal() throws LifecycleException {
        super.initInternal();
        Throwable ex = new Throwable();
        StackTraceElement[] stackElements = ex.getStackTrace();
        if (stackElements != null) {
            for (int i = stackElements.length - 1; i >= 0; i--) {
                System.out.print(stackElements[i].getClassName() + "\t");
                System.out.print(stackElements[i].getMethodName() + "\t");
                System.out.print(stackElements[i].getFileName() + "\t");
                System.out.println(stackElements[i].getLineNumber());
            }
        }
        if (processTlds) {
            this.addLifecycleListener(new TldConfig());
        }

        // Register the naming resources
        if (namingResources != null) {
            namingResources.init();
        }

        // Send j2ee.object.created notification 
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.object.created",
                    this.getObjectName(), sequenceNumber.getAndIncrement());
            broadcaster.sendNotification(notification);
        }
}

運行代碼,可以看到控制檯有如下的輸出:

terminal info

java.util.concurrent.ThreadPoolExecutor$Worker    run  ThreadPoolExecutor.java   918
java.util.concurrent.ThreadPoolExecutor$Worker runTask  ThreadPoolExecutor.java   895
java.util.concurrent.FutureTask   run  FutureTask.java   138
java.util.concurrent.FutureTask$Sync   innerRun FutureTask.java   303
java.util.concurrent.Executors$RunnableAdapter call Executors.java    439
org.apache.catalina.startup.HostConfig$DeployDirectory  run  HostConfig.java   1671
org.apache.catalina.startup.HostConfig deployDirectory  HostConfig.java   1113
org.apache.catalina.core.StandardHost  addChild StandardHost.java 622
org.apache.catalina.core.ContainerBase addChild ContainerBase.java    877
org.apache.catalina.core.ContainerBase addChildInternal ContainerBase.java    901
org.apache.catalina.util.LifecycleBase start    LifecycleBase.java    139
org.apache.catalina.util.LifecycleBase init LifecycleBase.java    102
org.apache.catalina.core.StandardContext   initInternal StandardContext.java  6449

通過查看控制檯的輸出,我們可以看到有一個 org.apache.catalina.startup.HostConfig$DeployDirectory 類,於是乎找到這個類去看看唄。打開一看它是一個Runable接口的實現類,因此我們推斷它也是放到某個線程池中進行異步運行的,最終通過IntellIJ IDEA提供的類調用棧分析工具(ctrl+alt+h)得到DeployDirectory構造器方法的調用棧如下圖所示:

這裏寫圖片描述

通過上圖我們可以清楚的看到,最終的調用方是 org.apache.catalina.startup.HostConfig#lifecycleEvent ,到這裏我們就知道了Context的啓動是通過某個組件的生命週期事件的監聽器來啓動的,而HostConfig到底是誰的監聽器呢?通過名稱我們應該可以猜測出它是StandardHost的監聽器,那麼它到底監聽哪個事件呢?我們查看下org.apache.catalina.startup.HostConfig#lifecycleEvent的代碼如下:
org.apache.catalina.startup.HostConfig#lifecycleEvent

public void lifecycleEvent(LifecycleEvent event) {

        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
            }
        } catch (ClassCastException e) {
            log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
            check();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
}

通過上面的代碼,我們可以看出監聽的事件是 Lifecycle.START_EVENT ,而通過查看 org.apache.catalina.LifecycleState 的代碼 STARTING(true,Lifecycle.START_EVENT) 就可以得知,此時生命週期狀態應該是STARTING,到這裏我們應該已經猜到了,HostConfig是在StandardHost#start的時候通過監聽器調用,爲了驗證我們的猜測,我們debug一下代碼,我們可以在HostConfig#start方法中打個斷點,運行以後得到如下內存結構:

這裏寫圖片描述

通過上圖也就驗證了我們剛纔的猜測。

通過上面的分析我們清楚了webapps目錄中context的啓動,總結如下:

webapps目錄中應用的啓動在StandardHost#start的時候,通過 Lifecycle.START_EVENT 這個事件的監聽器HostConfig進行進一步的啓動。

綜合上面的文章所述,最後我們再來一下總結,我們知道Java程序啓動以後,最終會以進程的形式存在,而Java進程中又會有很多條線程存在,因此最後我們就來看看Tomcat啓動以後,到底啓動了哪些線程,通過這些我們可以反過來驗證我們對源代碼的理解是否正確。接下來我們啓動Tomcat,然後運行 jstack -l 來看看,在筆者的機器上面,jstack的輸入如下所示:

Full thread dump Java HotSpot(TM) 64-Bit Server VM (20.51-b01-457 mixed mode):

"ajp-bio-8009-AsyncTimeout" daemon prio=5 tid=7f8738afe000 nid=0x115ad6000 waiting on condition [115ad5000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java:148)
        at java.lang.Thread.run(Thread.java:680)

   Locked ownable synchronizers:
        - None

"ajp-bio-8009-Acceptor-0" daemon prio=5 tid=7f8738b05800 nid=0x1159d3000 runnable [1159d2000]
   java.lang.Thread.State: RUNNABLE
        at java.net.PlainSocketImpl.socketAccept(Native Method)
        at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:439)
        - locked <7f46a8710> (a java.net.SocksSocketImpl)
        at java.net.ServerSocket.implAccept(ServerSocket.java:468)
        at java.net.ServerSocket.accept(ServerSocket.java:436)
        at org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFactory.java:60)
        at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:216)
        at java.lang.Thread.run(Thread.java:680)

   Locked ownable synchronizers:
        - None

"http-bio-8080-AsyncTimeout" daemon prio=5 tid=7f8735acb800 nid=0x1158d0000 waiting on condition [1158cf000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at org.apache.tomcat.util.net.JIoEndpoint$AsyncTimeout.run(JIoEndpoint.java:148)
        at java.lang.Thread.run(Thread.java:680)

   Locked ownable synchronizers:
        - None

"http-bio-8080-Acceptor-0" daemon prio=5 tid=7f8735acd000 nid=0x1157cd000 runnable [1157cc000]
   java.lang.Thread.State: RUNNABLE
        at java.net.PlainSocketImpl.socketAccept(Native Method)
        at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:439)
        - locked <7f46a8690> (a java.net.SocksSocketImpl)
        at java.net.ServerSocket.implAccept(ServerSocket.java:468)
        at java.net.ServerSocket.accept(ServerSocket.java:436)
        at org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFactory.java:60)
        at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:216)
        at java.lang.Thread.run(Thread.java:680)

   Locked ownable synchronizers:
        - None

"ContainerBackgroundProcessor[StandardEngine[Catalina]]" daemon prio=5 tid=7f8732850800 nid=0x111203000 waiting on condition [111202000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1508)
        at java.lang.Thread.run(Thread.java:680)

   Locked ownable synchronizers:
        - None


"main" prio=5 tid=7f8735000800 nid=0x10843e000 runnable [10843c000]
   java.lang.Thread.State: RUNNABLE
        at java.net.PlainSocketImpl.socketAccept(Native Method)
        at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:439)
        - locked <7f32ea7c8> (a java.net.SocksSocketImpl)
        at java.net.ServerSocket.implAccept(ServerSocket.java:468)
        at java.net.ServerSocket.accept(ServerSocket.java:436)
        at org.apache.catalina.core.StandardServer.await(StandardServer.java:452)
        at org.apache.catalina.startup.Catalina.await(Catalina.java:779)
        at org.apache.catalina.startup.Catalina.start(Catalina.java:725)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:322)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:456)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

上面的截圖我已經取消了JVM自己本生的線程,從上圖中我們可以清楚的看到,有6條線程,其中 ajp-bio-8009-AsyncTimeout 和 ajp-bio-8009-Acceptor-0 是在Ajp的Connector啓動的時候啓動的, http-bio-8080-AsyncTimeout 和 http-bio-8080-Acceptor-0 是http的Connector啓動的時候啓動的, ContainerBackgroundProcessor[StandardEngine[Catalina]] 是在StandardEngine啓動的時候啓動的,而main線程就是我們的主線程。這裏還需要注意一點就是除了Main線程以外,其它的線程都是Dameon線程,相關的內容在下篇Tomcat的關閉我們再來詳細說明。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章