Tomcat的啟停指令碼原始碼解析

程式設計師自由之路發表於2020-07-04

Tomcat是一款我們平時開發過程中最常用到的Servlet容器。本系列部落格會記錄Tomcat的整體架構、主要元件、IO執行緒模型、請求在Tomcat內部的流轉過程以及一些Tomcat調優的相關知識。

力求達到以下幾個目的:

  • 更加熟悉Tomcat的工作機制,工作中遇到Tomcat相關問題能夠快速定位,從源頭來解決;
  • 對於一些高併發場景能夠對Tomcat進行調優;
  • 通過對Tomcat原始碼的分析,吸收一些Tomcat的設計的理念,應用到自己的軟體開發過程中。

Tomcat的啟動和停止是通過startup.bat和shutdown.bat這兩個指令碼實現的。本篇部落格就分析下這兩個指令碼的主要執行流程。

1. startup.bat指令碼分析

//關閉命令自身輸出
@echo off
//setlocal命令表示,這邊對環境變數的修改只對當前指令碼生效
setlocal
//檢查CATALINA_HOME這個環境變數有沒設定,如果有設定就使用設定的環境變數
//如果沒設定,將CATALINA_HOME設定成當前目錄。
//檢測%CATALINA_HOME%\bin\catalina.bat這個指令碼存不存在,不存在整合指令碼結束,報錯
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
//拼接catalina.bat這個指令碼的命令列引數
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
//執行catalina.bat這個指令碼,執行start,並新增命令列引數
call "%EXECUTABLE%" start %CMD_LINE_ARGS%

:end

整個startup.bat指令碼很簡單,根據CATALINA_HOME檢測catalina.bat是否存在,不存在的話就報錯,存在的話拼接命令列引數然後執行catalina.bat這個指令碼。CATALINA_HOME這個環境變數的取值邏輯如下圖所示:

如果環境變數設定了CATALINA_HOME,則直接使用環境變數設定的值作為Tomcat安裝目錄。假如未設定環境變數CATALINA_HOME,則以當前目錄作為CATALINA_HOME。此時,如果%CATALINA_HOME%\bin\catalina.bat存在,則批處理或命令列當前目錄作為CATALINA_HOME。假如%CATALINA_HOME%\bin\catalina.bat不存在,則把當前目錄的上一級目錄作為CATALINA_HOME,然後再判斷%CATALINA_HOME%\bin\catalina.bat是否存在。如果存在,則上一級目錄就是CATALINA_HOME;否則,提示找不到CATALINA_HOME環境變數並結束執行。

我們可以看出來,正真執行的指令碼是catalina.bat這個指令碼,那為什麼還要整個startup.bat指令碼呢?

其實這個startup.bat指令碼就是提供給使用者用來修改的,我們可以在其中設定JAVA_HOME,CATALINA_HOME等環境變數,但我們並不需要深入到較為複雜的catalina.bat指令碼中,這正是startup.bat指令碼的真正用意所在。

我們知道,軟體設計模式中有一個重要的原則就是開閉原則,即我們可以允許別人擴充套件我們的程式,但在程式釋出後,我們拒絕任何修改,因為修改會產生新的Bug,使得我們已經Bug-free的程式又要重新測試。開閉原則是物件導向世界中的一個非常重要的原則,我們可以把這個原則從Java類擴充套件至原始碼級別。startup指令碼就是要求使用者不要修改catalina.bat指令碼,這是符合軟體設計思想的。我們如果想要徹底貫徹這個重要的軟體設計原則,可以寫一個新指令碼tomcat.bat,指令碼內容大致如下:

set JAVA_HOME=C:\Program Files\Java\jdk1.5.0_09
set CATALINA_HOME=C:\carl\it\tomcat_research\jakarta-tomcat-5.0.28   
call %CATALINA_HOME%\bin\startup.bat  

這個tomcat.bat檔案可以存放在任何目錄並能執行,並且不需要修改tomcat自帶的任何指令碼及其它環境變數,這就徹底貫徹了開閉原則。

2. catalina.bat指令碼簡析

當startup指令碼完成環境變數的設定後,就開始呼叫catalina.bat指令碼來啟動Tomcat。Catalina指令碼的主要任務是根據環境變數和不同的命令列引數,拼湊出完整的java命令,呼叫Tomcat的主啟動類org.apache.catalina.startup.Bootstrap來啟動Tomcat

@echo off
rem ---------------------------------------------------------------------------
rem Start/Stop Script for the CATALINA Server
rem
rem Environment Variable Prerequisites
rem
rem   Do not set the variables in this script. Instead put them into a script
rem   setenv.bat in CATALINA_BASE/bin to keep your customizations separate.
rem
rem   WHEN RUNNING TOMCAT AS A WINDOWS SERVICE:
rem   Note that the environment variables that affect the behavior of this
rem   script will have no effect at all on Windows Services. As such, any
rem   local customizations made in a CATALINA_BASE/bin/setenv.bat script
rem   will also have no effect on Tomcat when launched as a Windows Service.
rem   The configuration that controls Windows Services is stored in the Windows
rem   Registry, and is most conveniently maintained using the "tomcatXw.exe"
rem   maintenance utility, where "X" is the major version of Tomcat you are
rem   running.
rem
rem   CATALINA_HOME   May point at your Catalina "build" directory.
rem
rem   CATALINA_BASE   (Optional) Base directory for resolving dynamic portions
rem                   of a Catalina installation.  If not present, resolves to
rem                   the same directory that CATALINA_HOME points to.
rem
rem   CATALINA_OPTS   (Optional) Java runtime options used when the "start",
rem                   "run" or "debug" command is executed.
rem                   Include here and not in JAVA_OPTS all options, that should
rem                   only be used by Tomcat itself, not by the stop process,
rem                   the version command etc.
rem                   Examples are heap size, GC logging, JMX ports etc.
rem
rem   CATALINA_TMPDIR (Optional) Directory path location of temporary directory
rem                   the JVM should use (java.io.tmpdir).  Defaults to
rem                   %CATALINA_BASE%\temp.
rem
rem   JAVA_HOME       Must point at your Java Development Kit installation.
rem                   Required to run the with the "debug" argument.
rem
rem   JRE_HOME        Must point at your Java Runtime installation.
rem                   Defaults to JAVA_HOME if empty. If JRE_HOME and JAVA_HOME
rem                   are both set, JRE_HOME is used.
rem
rem   JAVA_OPTS       (Optional) Java runtime options used when any command
rem                   is executed.
rem                   Include here and not in CATALINA_OPTS all options, that
rem                   should be used by Tomcat and also by the stop process,
rem                   the version command etc.
rem                   Most options should go into CATALINA_OPTS.
rem
rem   JPDA_TRANSPORT  (Optional) JPDA transport used when the "jpda start"
rem                   command is executed. The default is "dt_socket".
rem
rem   JPDA_ADDRESS    (Optional) Java runtime options used when the "jpda start"
rem                   command is executed. The default is localhost:8000.
rem
rem   JPDA_SUSPEND    (Optional) Java runtime options used when the "jpda start"
rem                   command is executed. Specifies whether JVM should suspend
rem                   execution immediately after startup. Default is "n".
rem
rem   JPDA_OPTS       (Optional) Java runtime options used when the "jpda start"
rem                   command is executed. If used, JPDA_TRANSPORT, JPDA_ADDRESS,
rem                   and JPDA_SUSPEND are ignored. Thus, all required jpda
rem                   options MUST be specified. The default is:
rem
rem                   -agentlib:jdwp=transport=%JPDA_TRANSPORT%,
rem                       address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND%
rem
rem   JSSE_OPTS       (Optional) Java runtime options used to control the TLS
rem                   implementation when JSSE is used. Default is:
rem                   "-Djdk.tls.ephemeralDHKeySize=2048"
rem
rem   LOGGING_CONFIG  (Optional) Override Tomcat's logging config file
rem                   Example (all one line)
rem                   set LOGGING_CONFIG="-Djava.util.logging.config.file=%CATALINA_BASE%\conf\logging.properties"
rem
rem   LOGGING_MANAGER (Optional) Override Tomcat's logging manager
rem                   Example (all one line)
rem                   set LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager"
rem
rem   TITLE           (Optional) Specify the title of Tomcat window. The default
rem                   TITLE is Tomcat if it's not specified.
rem                   Example (all one line)
rem                   set TITLE=Tomcat.Cluster#1.Server#1 [%DATE% %TIME%]
rem ---------------------------------------------------------------------------

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

//防止使用者直接執行catalina.bat這個指令碼,再次檢測下CATALINA_HOME環境變數
//檢測catalina.bat這個指令碼存不存在
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

//檢測CATALINA_BASE是否存在,不存在就設定成和CATALINA_HOME一致
rem Copy CATALINA_BASE from CATALINA_HOME if not defined
if not "%CATALINA_BASE%" == "" goto gotBase
set "CATALINA_BASE=%CATALINA_HOME%"
:gotBase

rem Ensure that neither CATALINA_HOME nor CATALINA_BASE contains a semi-colon
rem as this is used as the separator in the classpath and Java provides no
rem mechanism for escaping if the same character appears in the path. Check this
rem by replacing all occurrences of ';' with '' and checking that neither
rem CATALINA_HOME nor CATALINA_BASE have changed
if "%CATALINA_HOME%" == "%CATALINA_HOME:;=%" goto homeNoSemicolon
echo Using CATALINA_HOME:   "%CATALINA_HOME%"
echo Unable to start as CATALINA_HOME contains a semicolon (;) character
goto end
:homeNoSemicolon

if "%CATALINA_BASE%" == "%CATALINA_BASE:;=%" goto baseNoSemicolon
echo Using CATALINA_BASE:   "%CATALINA_BASE%"
echo Unable to start as CATALINA_BASE contains a semicolon (;) character
goto end
:baseNoSemicolon

rem Ensure that any user defined CLASSPATH variables are not used on startup,
rem but allow them to be specified in setenv.bat, in rare case when it is needed.
set CLASSPATH=

//如果%CATALINA_BASE%\bin\setenv.bat存在,執行setenv.bat這個指令碼,不存在
//執行%CATALINA_HOME%\bin\setenv.bat這個指令碼來設定環境變數
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

//如果%CATALINA_HOME%\bin\setclasspath.bat存在,執行setclasspath.bat,這個指令碼的主要作用是檢測
//JAVA_HOME有沒有正確設定
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

//將bootstrap.jar加入classpath
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"

//設定CATALINA_TMPDIR=%CATALINA_BASE%\temp
if not "%CATALINA_TMPDIR%" == "" goto gotTmpdir
set "CATALINA_TMPDIR=%CATALINA_BASE%\temp"
:gotTmpdir

//將tomcat-juli.jar加入classpath
rem Add tomcat-juli.jar to classpath
rem tomcat-juli.jar can be over-ridden per instance
if not exist "%CATALINA_BASE%\bin\tomcat-juli.jar" goto juliClasspathHome
set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%\bin\tomcat-juli.jar"
goto juliClasspathDone
:juliClasspathHome
set "CLASSPATH=%CLASSPATH%;%CATALINA_HOME%\bin\tomcat-juli.jar"
:juliClasspathDone

//如果沒有設定JSSE_OPTS,JSSE_OPTS="-Djdk.tls.ephemeralDHKeySize=2048"
//再將JAVA_OPTS設定成"JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS%"
if not "%JSSE_OPTS%" == "" goto gotJsseOpts
set JSSE_OPTS="-Djdk.tls.ephemeralDHKeySize=2048"
:gotJsseOpts
set "JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS%"

rem Register custom URL handlers
rem Do this here so custom URL handles (specifically 'war:...') can be used in the security policy
set "JAVA_OPTS=%JAVA_OPTS% -Djava.protocol.handler.pkgs=org.apache.catalina.webresources"

//將日誌配置檔案設定成logging.properties
if not "%LOGGING_CONFIG%" == "" goto noJuliConfig
set LOGGING_CONFIG=-Dnop
if not exist "%CATALINA_BASE%\conf\logging.properties" goto noJuliConfig
set LOGGING_CONFIG=-Djava.util.logging.config.file="%CATALINA_BASE%\conf\logging.properties"
:noJuliConfig

//配置預設的LOGGING_MANAGER
if not "%LOGGING_MANAGER%" == "" goto noJuliManager
set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
:noJuliManager

rem ----- Execute The Requested Command ---------------------------------------

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=localhost: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

echo Usage:  catalina ( commands ... )
echo commands:
echo   debug             Start Catalina in a debugger
echo   debug -security   Debug Catalina with a security manager
echo   jpda start        Start Catalina under JPDA debugger
echo   run               Start Catalina in the current window
echo   run -security     Start in the current window with security manager
echo   start             Start Catalina in a separate window
echo   start -security   Start in a separate window with security manager
echo   stop              Stop Catalina
echo   configtest        Run a basic syntax check on server.xml
echo   version           What version of tomcat are you running?
goto end

:doDebug
shift
set _EXECJAVA=%_RUNJDB%
set DEBUG_OPTS=-sourcepath "%CATALINA_HOME%\..\..\java"
if not ""%1"" == ""-security"" goto execCmd
shift
echo Using Security Manager
set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy"
goto execCmd

:doRun
shift
if not ""%1"" == ""-security"" goto execCmd
shift
echo Using Security Manager
set "SECURITY_POLICY_FILE=%CATALINA_BASE%\conf\catalina.policy"
goto execCmd

: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

:doStop
shift
set ACTION=stop
set CATALINA_OPTS=
goto execCmd

:doConfigTest
shift
set ACTION=configtest
set CATALINA_OPTS=
goto execCmd

:doVersion
%_EXECJAVA% -classpath "%CATALINA_HOME%\lib\catalina.jar" org.apache.catalina.util.ServerInfo
goto end


: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

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% -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% -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% -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% -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

總結下catalina.bat的整個執行流程:

  • 檢查並設定CATALINA_HOME環境變數,檢測%CATALINA_HOME%\bin\catalina.bat這個指令碼是否存在,不存在直接報錯;
  • 檢測CATALINA_BASE是否存在,不存在就設定成和CATALINA_HOME一致;
  • 如果%CATALINA_BASE%\bin\setenv.bat這個指令碼存在,執行setenv.bat這個指令碼,不存在的話執行%CATALINA_HOME%\bin\setenv.bat這個指令碼來設定環境變數,都不存在繼續往下執行;
  • 如果%CATALINA_HOME%\bin\setclasspath.bat存在,執行setclasspath.bat,這個指令碼的主要作用是檢測
    JAVA_HOME有沒有正確設定;
  • 將bootstrap.jar加入classpath;
  • 設定CATALINA_TMPDIR=%CATALINA_BASE%\temp;
  • 將tomcat-juli.jar加入classpath;
  • 如果沒有設定環境變數JSSE_OPTS,預設設定JSSE_OPTS="-Djdk.tls.ephemeralDHKeySize=2048"
    再將JAVA_OPTS設定成"JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS%"
  • 通過LOGGING_CONFIG變數,將日誌配置檔案設定成%CATALINA_BASE%\conf\logging.properties
  • 配置預設的LOGGING_MANAGER;
  • 拼寫java執行命令。

拼接命令的程式碼主要是下面這段:

%_EXECJAVA% %LOGGING_CONFIG% %LOGGING_MANAGER% %JAVA_OPTS% %CATALINA_OPTS% %DEBUG_OPTS% -classpath "%CLASSPATH%" -Dcatalina.base="%CATALINA_BASE%" -Dcatalina.home="%CATALINA_HOME%" -Djava.io.tmpdir="%CATALINA_TMPDIR%" %MAINCLASS% %CMD_LINE_ARGS% %ACTION%

通過這段程式碼我們可以看到Tomcat在啟動的時候配置了哪些引數。我們執行下面的命令:

startup.bat arg1 arg2

實際執行的命令如下:

"start "Tomcat" "C:\Program Files\Java\jdk1.8.0_73\bin\java.exe" -Djava.util.logging.config.file="D:\software\tomcat-64\apache-tomcat-9.0.0.M21-windows-x64 (1)\apache-tomcat-9.0.0.M21\conf\logging.properties" -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager  "-Djdk.tls.ephemeralDHKeySize=2048" -Djava.protocol.handler.pkgs=org.apache.catalina.webresources   -classpath "D:\software\tomcat-64\apache-tomcat-9.0.0.M21-windows-x64 (1)\apache-tomcat-9.0.0.M21\bin\bootstrap.jar;D:\software\tomcat-64\apache-tomcat-9.0.0.M21-windows-x64 (1)\apache-tomcat-9.0.0.M21\bin\tomcat-juli.jar" -Dcatalina.base="D:\software\tomcat-64\apache-tomcat-9.0.0.M21-windows-x64 (1)\apache-tomcat-9.0.0.M21" -Dcatalina.home="D:\software\tomcat-64\apache-tomcat-9.0.0.M21-windows-x64 (1)\apache-tomcat-9.0.0.M21" -Djava.io.tmpdir="D:\software\tomcat-64\apache-tomcat-9.0.0.M21-windows-x64 (1)\apache-tomcat-9.0.0.M21\temp" org.apache.catalina.startup.Bootstrap arg1 arg2 start"

上面命令中開頭的start Tomcat的意思是重新開啟一個叫tomcat的視窗執行Java命令。

我們看到上面的程式碼中,有個jpda模式,它是Java平臺除錯體系結構,可以提供很方便的遠端除錯,一般情況下我們不會用到這個模式。如果我們想啟動這個模式的話可以執行catalina.bat jpda start這個命令。這個模式下我們可以對另外的環境變數JPDA_OPTS進行配置。

3. 關於配置的一些建議

通過上面的指令碼,我們可以看到在啟動過程中我們可以配置很多環境變數。

  • CATALINA_HOME:可以不配置,預設使用安裝目錄;
  • CATALINA_BASE:建議不要自己配置,不配置的話會自動配置成和CATALINA_HOME一致;
  • CATALINA_OPTS:可以配置;
  • CATALINA_TMPDIR:建議不要自己配置,預設%CATALINA_BASE%\temp;
  • JAVA_OPTS:可以配置;
  • JSSE_OPTS:不建議自己配置,預設值-Djdk.tls.ephemeralDHKeySize=2048;
  • LOGGING_CONFIG:建議不要自己配置,這個配置用於配置日誌的配置檔案,預設會使用LOGGING_CONFIG="-Djava.util.logging.config.file=%CATALINA_BASE%\conf\logging.properties"
  • LOGGING_MANAGER:建議不要自己配置,預設會使用LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager"

通過上面介紹我們發現大多數配置都不需要我們自己進行配置,一般情況下我們只需要配置CATALINA_OPTS和JAVA_OPTS這兩個配置選項就可以了。這兩個引數都可以配置Java執行時所需的一些引數,下面給出兩個列子。

在startup指令碼中新增配置:

//windows
set JAVA_OPTS=-server -Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m
//Linux
JAVA_OPTS="-server -Dfile.encoding=UTF-8 -Xms=512m -Xmx1024m -XX:PermSize=128m -XX:MaxPermSize=256m"

4. shutdown.bat指令碼分析

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%" stop %CMD_LINE_ARGS%

:end

我們可以發現shutdown.bat的邏輯和startup.bat的邏輯是一樣的,也是先設定CATALINA_HOME,拼接命令列引數,不一樣是最後執行的是catalina.bat stop。

這邊還是有必要來分析下Tomcat的關閉原理的。我們知道,Tomcat中的工作執行緒都是demo執行緒,如果沒有一個主執行緒的話那麼Tomcat會立即停止執行的。(前臺執行緒死亡後,demo執行緒會自動消失。)那麼Tomcat是在哪裡啟動的這個主執行緒的呢?通過程式碼跟蹤,我們發現是StandardServer這個類的await方法建立了這個主執行緒,這個執行緒hold了Tomcat程式不停止

public void await() {
        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
        if (getPortWithOffset() == -2) {
            // undocumented yet - for embedding apps that are around, alive.
            return;
        }
        //
        if (getPortWithOffset() == -1) {
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }

        // Set up a server socket to wait on
        try {
            awaitSocket = new ServerSocket(getPortWithOffset(), 1,
                    InetAddress.getByName(address));
        } catch (IOException e) {
            log.error(sm.getString("standardServer.awaitSocket.fail", address,
                    String.valueOf(getPortWithOffset()), String.valueOf(getPort()),
                    String.valueOf(getPortOffset())), e);
            return;
        }

        try {
            awaitThread = Thread.currentThread();

            // Loop waiting for a connection and a valid command
            while (!stopAwait) {
                ServerSocket serverSocket = awaitSocket;
                if (serverSocket == null) {
                    break;
                }

                // Wait for the next connection
                Socket socket = null;
                StringBuilder command = new StringBuilder();
                try {
                    InputStream stream;
                    long acceptStartTime = System.currentTimeMillis();
                    try {
                        socket = serverSocket.accept();
                        socket.setSoTimeout(10 * 1000);  // Ten seconds
                        stream = socket.getInputStream();
                    } catch (SocketTimeoutException ste) {
                        // This should never happen but bug 56684 suggests that
                        // it does.
                        log.warn(sm.getString("standardServer.accept.timeout",
                                Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
                        continue;
                    } catch (AccessControlException ace) {
                        log.warn("StandardServer.accept security exception: "
                                + ace.getMessage(), ace);
                        continue;
                    } catch (IOException e) {
                        if (stopAwait) {
                            // Wait was aborted with socket.close()
                            break;
                        }
                        log.error("StandardServer.await: accept: ", e);
                        break;
                    }

                    // Read a set of characters from the socket
                    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;
                        }
                        // Control character or EOF (-1) terminates loop
                        if (ch < 32 || ch == 127) {
                            break;
                        }
                        command.append((char) ch);
                        expected--;
                    }
                } finally {
                    // Close the socket now that we are done with it
                    try {
                        if (socket != null) {
                            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");
            }
        } finally {
            ServerSocket serverSocket = awaitSocket;
            awaitThread = null;
            awaitSocket = null;

            // Close the server socket and return
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
    }

StandardServer預設監聽的埠是8005埠(注意這個埠和Connector元件監聽埠的區別),當發現監聽到的連線的輸入流中的內容與預設配置的值匹配(該值預設為字串SHUTDOWN)則跳出迴圈。否則該方法會一直迴圈執行下去。所以只要沒人向8005埠傳送shutdown,這個執行緒就會一直執行,其他的守護執行緒也就不會消失。

當然我們可以將StandardServer的監聽埠設定成-1(SpringBoot中內嵌的Tomcat就是這麼做的),此時Tomcat不會再監聽具體的埠,主執行緒會每10秒睡眠一次,知道我們手動將stopAwait設定為true。

知道了這個原理,我們只要將這個主執行緒結束掉,整個Tomcat程式就結束了。通過上面的分析,可以有兩種方式來關閉Tomcat:

  • 通過shutdown.bat指令碼,這個指令碼最終會呼叫到Catalina的stopServer方法,這個方法中建立了一個Socket,並向StandardServer監聽的埠傳送了一個shutdown命令,主執行緒接收到後就退出了,其他守護執行緒也隨之結束;

如果我們將server.xml配置檔案修改成:

<Server port="8005" shutdown="GET /SHUTDOWN HTTP/1.1"> 

這樣直接在瀏覽器中輸入http://localhost:8005/SHUTDOWN就可以關閉Tomcat了。

  • 另外的一種方式就是直接呼叫StandardServer的stop方法。

相關文章