詳解Tomcat系列(一):從源碼分析Tomcat的啓動

在整個Tomcat系列文章講解以前, 我想說的是雖然整個Tomcat體系比較複雜, 可是Tomcat中的代碼並不難讀, 只要認真花點功夫, 必定能啃下來.
因爲篇幅的緣由, 很難把Tomcat全部的知識點都放到同一篇文章中, 我將把Tomcat系列文章分爲Tomcat的啓動, Tomcat中各模塊的介紹和Tomcat中的設計模式三部分, 歡迎閱讀與關注.java

一:經過idea搭建Tomcat源碼閱讀環境

  • 首先咱們到Tomcat的官網(tomcat.apache.org/)上下載Tomcat的源碼, 本文分析的Tomcat版本是Tomcat7.0.94.
  • 進入官網後在左邊的目錄中選擇Tomcat7, 而後到頁面末尾的源碼區進行下載.

  • 下載完成後打開idea, 選擇File->Open->選擇tomcat的源碼目錄
  • 而後到項目配置中設置JDK和源碼目錄. File->Project Structure->project SDK

  • 設置完畢後咱們即可以開始愉快的源碼閱讀之旅了. (eclipse或其餘ide搭建環境的思路也是差很少的, 能夠摸索一下).

二:startup源碼分析

  • 當咱們初學tomcat的時候, 確定先要學習怎樣啓動tomcat. 在tomcat的bin目錄下有兩個啓動tomcat的文件, 一個是startup.bat, 它用於windows環境下啓動tomcat; 另外一個是startup.sh, 它用於linux環境下tomcat的啓動. 兩個文件中的邏輯是同樣的, 咱們只分析其中的startup.bat.
  • 下面給出startup.bat的源碼
@echo off
rem Licensed to the Apache Software Foundation (ASF) under one or more
rem contributor license agreements.  See the NOTICE file distributed with
rem this work for additional information regarding copyright ownership.
rem The ASF licenses this file to You under the Apache License, Version 2.0
rem (the "License"); you may not use this file except in compliance with
rem the License.  You may obtain a copy of the License at
rem
rem     http://www.apache.org/licenses/LICENSE-2.0
rem
rem Unless required by applicable law or agreed to in writing, software
rem distributed under the License is distributed on an "AS IS" BASIS,
rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
rem See the License for the specific language governing permissions and
rem limitations under the License.

rem ---------------------------------------------------------------------------
rem Start script for the CATALINA Server
rem ---------------------------------------------------------------------------

setlocal

rem Guess CATALINA_HOME if not defined
set "CURRENT_DIR=%cd%"
if not "%CATALINA_HOME%" == "" goto gotHome
set "CATALINA_HOME=%CURRENT_DIR%"
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome

set "EXECUTABLE=%CATALINA_HOME%\bin\catalina.bat"

rem Check that target executable exists
if exist "%EXECUTABLE%" goto okExec
echo Cannot find "%EXECUTABLE%"
echo This file is needed to run this program
goto end
:okExec

rem Get remaining unshifted command line arguments and save them in the
set CMD_LINE_ARGS=
:setArgs
if ""%1""=="""" goto doneSetArgs
set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
shift
goto setArgs
:doneSetArgs

call "%EXECUTABLE%" start %CMD_LINE_ARGS%

:end
複製代碼
  • .bat文件中@echo是打印指令, 用於控制檯輸出信息, rem是註釋符.
  • 跳過開頭的註釋, 咱們來到配置CATALINA_HOME的代碼段, 執行startup.bat文件首先會設置CATALINA_HOME.
set "CURRENT_DIR=%cd%"
if not "%CATALINA_HOME%" == "" goto gotHome
set "CATALINA_HOME=%CURRENT_DIR%"
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome
複製代碼
  • 先經過set指令把當前目錄設置到一個名爲CURRENT_DIR的變量中,
  • 若是系統中配置過CATALINA_HOME則跳到gotHome代碼段. 正常狀況下咱們的電腦都沒有配置CATALINA_HOME, 因此往下執行, 把當前目錄設置爲CATALINA_HOME.
  • 而後判斷CATALINA_HOME目錄下是否存在catalina.bat文件, 若是存在就跳到okHome代碼塊.
  • 在okHome中, 會把catalina.bat文件的的路徑賦給一個叫EXECUTABLE的變量, 而後會進一步判斷這個路徑是否存在, 存在則跳轉到okExec代碼塊, 不存在的話會在控制檯輸出一些錯誤信息.
  • 在okExec中, 會把setArgs代碼塊的返回結果賦值給CMD_LINE_ARGS變量, 這個變量用於存儲啓動參數.
  • setArgs中首先會判斷是否有參數, (if ""%1""==""""判斷第一個參數是否爲空), 若是沒有參數則至關於參數項爲空. 若是有參數則循環遍歷全部的參數(每次拼接第一個參數).
  • 最後執行call "%EXECUTABLE%" start %CMD_LINE_ARGS%, 也就是說執行catalina.bat文件, 若是有參數則帶上參數.
  • 總結: startup.bat文件實際上就作了一件事情: 啓動catalina.bat.
  • ps: 這樣看來, 在windows下啓動tomcat未必必定要經過startup.bat, 用catalina.bat start也是能夠的.

catalina.bat源碼分析

  • 爲了避免讓文章看起來太臃腫, 這裏就不先把整個文件貼出來了, 咱們逐塊代碼進行分析.
  • 跳過開頭的註釋, 咱們來到下面的代碼段:
setlocal

rem Suppress Terminate batch job on CTRL+C
if not ""%1"" == ""run"" goto mainEntry
if "%TEMP%" == "" goto mainEntry
if exist "%TEMP%\%~nx0.run" goto mainEntry
echo Y>"%TEMP%\%~nx0.run"
if not exist "%TEMP%\%~nx0.run" goto mainEntry
echo Y>"%TEMP%\%~nx0.Y"
call "%~f0" %* <"%TEMP%\%~nx0.Y"
rem Use provided errorlevel
set RETVAL=%ERRORLEVEL%
del /Q "%TEMP%\%~nx0.Y" >NUL 2>&1
exit /B %RETVAL%
:mainEntry
del /Q "%TEMP%\%~nx0.run" >NUL 2>&1
複製代碼
  • 大多狀況下咱們啓動tomcat都沒有設置參數, 因此直接跳到mainEntry代碼段, 刪除了一個臨時文件後, 繼續往下執行.
rem Guess CATALINA_HOME if not defined
set "CURRENT_DIR=%cd%"
if not "%CATALINA_HOME%" == "" goto gotHome
set "CATALINA_HOME=%CURRENT_DIR%"
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"
:gotHome

if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
echo The CATALINA_HOME environment variable is not defined correctly
echo This environment variable is needed to run this program
goto end
:okHome

rem Copy CATALINA_BASE from CATALINA_HOME if not defined
if not "%CATALINA_BASE%" == "" goto gotBase
set "CATALINA_BASE=%CATALINA_HOME%"
複製代碼
  • 能夠看到這段代碼與startup.bat中開頭的代碼類似, 在肯定CATALINA_HOME下有catalina.bat後把CATALINA_HOME賦給變量CATALINA_BASE.
rem Get standard environment variables
if not exist "%CATALINA_BASE%\bin\setenv.bat" goto checkSetenvHome
call "%CATALINA_BASE%\bin\setenv.bat"
goto setenvDone
:checkSetenvHome
if exist "%CATALINA_HOME%\bin\setenv.bat" call "%CATALINA_HOME%\bin\setenv.bat"
:setenvDone

rem Get standard Java environment variables
if exist "%CATALINA_HOME%\bin\setclasspath.bat" goto okSetclasspath
echo Cannot find "%CATALINA_HOME%\bin\setclasspath.bat"
echo This file is needed to run this program
goto end
:okSetclasspath
call "%CATALINA_HOME%\bin\setclasspath.bat" %1
if errorlevel 1 goto end

rem Add on extra jar file to CLASSPATH
rem Note that there are no quotes as we do not want to introduce random
rem quotes into the CLASSPATH
if "%CLASSPATH%" == "" goto emptyClasspath
set "CLASSPATH=%CLASSPATH%;"
:emptyClasspath
set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar"
複製代碼
  • 上面這段代碼依次執行了setenv.bat和setclasspath.bat文件, 目的是得到CLASSPATH, 相信會Java的同窗應該都會在配置環境變量時都配置過classpath, 系統拿到classpath路徑後把它和CATALINA_HOME拼接在一塊兒, 最終定位到一個叫bootstrap.jar的文件. 雖而後面還有不少代碼, 可是這裏必須暫停提示一下: bootstrap.jar將是咱們啓動tomcat的環境.
  • 接下來從gotTmpdir代碼塊到noEndorsedVar代碼塊進行了一些配置, 因爲不是主要內容暫且跳過.
echo Using CATALINA_BASE:   "%CATALINA_BASE%"
echo Using CATALINA_HOME:   "%CATALINA_HOME%"
echo Using CATALINA_TMPDIR: "%CATALINA_TMPDIR%"
if ""%1"" == ""debug"" goto use_jdk
echo Using JRE_HOME:        "%JRE_HOME%"
goto java_dir_displayed
:use_jdk
echo Using JAVA_HOME:       "%JAVA_HOME%"
:java_dir_displayed
echo Using CLASSPATH:       "%CLASSPATH%"

set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start
set SECURITY_POLICY_FILE=
set DEBUG_OPTS=
set JPDA=

if not ""%1"" == ""jpda"" goto noJpda
set JPDA=jpda
if not "%JPDA_TRANSPORT%" == "" goto gotJpdaTransport
set JPDA_TRANSPORT=dt_socket
:gotJpdaTransport
if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress
set JPDA_ADDRESS=8000
:gotJpdaAddress
if not "%JPDA_SUSPEND%" == "" goto gotJpdaSuspend
set JPDA_SUSPEND=n
:gotJpdaSuspend
if not "%JPDA_OPTS%" == "" goto gotJpdaOpts
set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND%
:gotJpdaOpts
shift
:noJpda

if ""%1"" == ""debug"" goto doDebug
if ""%1"" == ""run"" goto doRun
if ""%1"" == ""start"" goto doStart
if ""%1"" == ""stop"" goto doStop
if ""%1"" == ""configtest"" goto doConfigTest
if ""%1"" == ""version"" goto doVersion
複製代碼
  • 接下來, 咱們能看到一些重要的信息, 其中的重點是:
  • set _EXECJAVA=%_RUNJAVA%, 設置了jdk中bin目錄下的java.exe文件路徑.
  • set MAINCLASS=org.apache.catalina.startup.Bootstrap, 設置了tomcat的啓動類爲Bootstrap這個類. (後面會分析這個類)
  • set ACTION=start設置tomcat啓動
  • 你們能夠留意這些參數, 最後執行tomcat的啓動時會用到.
if not ""%1"" == ""jpda"" goto noJpda
set JPDA=jpda
if not "%JPDA_TRANSPORT%" == "" goto gotJpdaTransport
set JPDA_TRANSPORT=dt_socket
:gotJpdaTransport
if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress
set JPDA_ADDRESS=8000
:gotJpdaAddress
if not "%JPDA_SUSPEND%" == "" goto gotJpdaSuspend
set JPDA_SUSPEND=n
:gotJpdaSuspend
if not "%JPDA_OPTS%" == "" goto gotJpdaOpts
set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND%
:gotJpdaOpts
shift
:noJpda

if ""%1"" == ""debug"" goto doDebug
if ""%1"" == ""run"" goto doRun
if ""%1"" == ""start"" goto doStart
if ""%1"" == ""stop"" goto doStop
if ""%1"" == ""configtest"" goto doConfigTest
if ""%1"" == ""version"" goto doVersion
複製代碼
  • 接着判斷第一個參數是不是jpda, 是則進行一些設定. 而正常狀況下第一個參數是start, 因此跳過這段代碼. 接着會判斷第一個參數的內容, 根據判斷, 咱們會跳到doStart代碼段. (有餘力的同窗不妨看下debug, run等啓動方式)
:doStart
shift
if "%TITLE%" == "" set TITLE=Tomcat
set _EXECJAVA=start "%TITLE%" %_RUNJAVA%
if not ""%1"" == ""-security"" goto execCmd
shift
echo Using Security Manager
set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy"
goto execCmd
複製代碼
  • 能夠看到doStart中無非也是設定一些參數, 最終會跳轉到execCmd代碼段
:execCmd
rem Get remaining unshifted command line arguments and save them in the
set CMD_LINE_ARGS=
:setArgs
if ""%1""=="""" goto doneSetArgs
set CMD_LINE_ARGS=%CMD_LINE_ARGS% %1
shift
goto setArgs
:doneSetArgs
複製代碼
  • 能夠看到這段代碼也是在拼接參數, 把參數拼接到一個叫CMD_LINE_ARGS的變量中, 接下來就是catalina最後的一段代碼了.
rem Execute Java with the applicable properties
if not "%JPDA%" == "" goto doJpda
if not "%SECURITY_POLICY_FILE%" == "" goto doSecurity
%_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%
goto end
:doSecurity
%_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%
goto end
:doJpda
if not "%SECURITY_POLICY_FILE%" == "" goto doSecurityJpda
%_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %JPDA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%
goto end
:doSecurityJpda
%_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %JPDA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Djava.security.manager -Djava.security.policy=="%SECURITY_POLICY_FILE%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%
goto end

:end
複製代碼
  • 跳過前面兩行判斷後, 來到了關鍵語句:
  • %_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -D%ENDORSED_PROP%="%JAVA_ENDORSED_DIRS%" -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%
  • _EXECJAVA也就是_RUNJAVA, 也就是平時說的java指令, 但在以前的doStart代碼塊中把_EXECJAVA改成了start "%TITLE%" %_RUNJAVA%, 因此係統會另啓一個命令行窗口, 名字叫Tomcat.
  • 在拼接一系列參數後, 咱們會看見%MAINCLASS%, 也就是org.apache.catalina.startup.Bootstrap啓動類, 拼接完啓動參數後, 最後拼接的是%ACTION%, 也就是start.
  • 總結: catalina.bat最終執行了Bootstrap類中的main方法.
  • ps: 分析完整個catalina.bat咱們發現咱們能夠經過設定不一樣的參數讓tomcat以不一樣的方式運行. 在ide中咱們是能夠選擇debug等模式啓動tomcat的, 也能夠爲其配置參數, 在catalina.bat中咱們看到了啓動tomcat背後的運做流程.

Tomcat啓動流程分析

  • 上面咱們從運行startup.bat一路分析來到Bootstrap的啓動, 下面咱們先對Tomcat中的組件進行一次總覽. 因爲篇幅的緣由, tomcat中各個模塊的關係將留到下一篇文章敘述, 這裏先給出一張tomcat中各模塊的關係圖.

  • 從圖中咱們能夠看出tomcat中模塊仍是挺多的, 那麼tomcat是如何啓動這些模塊的呢? 請看下面這張示意圖.

  • 由圖中咱們能夠看到從Bootstrap類的main方法開始, tomcat會以鏈的方式逐級調用各個模塊的init()方法進行初始化, 待各個模塊都初始化後, 又會逐級調用各個模塊的start()方法啓動各個模塊. 下面咱們經過部分源碼看一下這個過程.
  • 首先咱們來到Bootstrap類的main方法
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) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            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")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } 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
			// 省略
            System.exit(1);
        }
    }
複製代碼
  • 咱們能夠看到, Bootstrap類首先會建立一個本類對象, 而後調用init()方法進行初始化. 執行完init()方法後會判斷啓動參數的值, 因爲咱們採起默認的啓動方式, 因此main方法的參數是start, 會進入下面的判斷代碼塊
else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
複製代碼
  • 能夠看到在設置等待後, 調用了本類對象的load()方法. 咱們跟進查看load()方法的源碼.
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);
    }
複製代碼
  • 能夠看到方法的最後經過反射的方式調用了成員變量catalinaDaemon的load()方法. 經過跟蹤源碼咱們能夠看到catalinaDaemon是Bootstrap類的一個私有成員變量
/** * Daemon reference. */
private Object catalinaDaemon = null;
複製代碼
  • 它會在Bootstrap的init()方法中經過反射的方式完成初始化. 下面咱們回過頭來看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");
    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;
}
複製代碼
  • 能夠看到init()方法建立了一個Catalina對象, 並把該對象賦給了catalinaDaemon.
  • 再回過頭來看以前分享給你們的啓動流程圖, 對應的是這一塊

  • 後面的過程我就不逐一分析了, 你能夠按照這張圖去跟蹤代碼進行驗證. 最終全部模塊都能被初始化並啓動.

本篇結束linux

  • 看到這裏相信你應該對tomcat的啓動過程已經有一個清晰的認識了, 可是這僅僅是理解tomcat的開始, 下一篇文章開始我將會詳細介紹tomcat中的組件, 以及組件間的關係. 瞭解了各個組件後, tomcat的結構於你而言將再也不神祕.
相關文章
相關標籤/搜索