本系列以Tomcat 8.5.33為例分析Tomcat的啟動和請求處理過程。
啟動腳本
與Tomcat有關的腳本都在Tomcat主目錄的bin子目錄中,其中與啟動有關的腳本有startup.sh、catalina.sh和setclasspath.sh。啟動Tomcat時只需執行startup.sh即可,其內容如下:
# 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 "$@"
該腳本主要做了兩件事:
- 用while循環處理符號鏈接,ls變量存ls命令的結果,link變量存符號鏈接指向的文件路徑,直到PRG的值不再是符號鏈接為止,以此找到Tomcat的主目錄;
- exec命令執行catalina.sh,命令行參數由start和傳遞給startup.sh的命令行參數兩部分組成。
catalina.sh利用setclasspath.sh檢驗JRE環境,然后根據傳遞給腳本的的命令行參數啟動JVM,不同參數對應的分支代碼如下,傳給catalina.sh腳本的第一個命令行參數是給Bootstrap類傳遞的最后一個命令行參數。以默認情況為例,上文的startup.sh只為catalina.sh提供了start參數,則$@為空,因此會執行eval所示的代碼。
if [ "$1" = "debug" ] ; then
# 省略部分代碼
elif [ "$1" = "run" ]; then
# 省略部分代碼
elif [ "$1" = "start" ] ; then
# 省略部分代碼
shift
touch "$CATALINA_OUT"
if [ "$1" = "-security" ] ; then
# 省略部分代碼
else
eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="\"$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."
elif [ "$1" = "stop" ] ; then
# 省略部分代碼
elif [ "$1" = "configtest" ] ; then
# 省略部分代碼
elif [ "$1" = "version" ] ; then
# 省略部分代碼
else
echo "Usage: catalina.sh ( commands ... )"
# 省略部分代碼
fi
用ps aux | grep tomcat可以看到對應的執行命令如下:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java
-Djava.util.logging.config.file=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/conf/logging.properties
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djdk.tls.ephemeralDHKeySize=2048
-Djava.protocol.handler.pkgs=org.apache.catalina.webresources
-Dorg.apache.catalina.security.SecurityListener.UMASK=0027
-Dignore.endorsed.dirs= -classpath /Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/bootstrap.jar:/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/tomcat-juli.jar
-Dcatalina.base=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33
-Dcatalina.home=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33
-Djava.io.tmpdir=/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/temp
org.apache.catalina.startup.Bootstrap start
- 虛擬機參數catalina.home是Tomcat的安裝目錄;
- 虛擬機參數catalina.base是Tomcat的工作目錄,在有多個Tomcat時可以與catalina.home不同;
- 主類是org.apache.catalina.startup.Bootstrap,命令行參數只有一個start。
Bootstrap類
Bootstrap類用于引導Tomcat的啟動過程。
成員變量
Bootstrap類的成員變量如下:
/**
* Daemon object used by main.
*/
private static Bootstrap daemon = null;
private static final File catalinaBaseFile;
private static final File catalinaHomeFile;
/**
* Daemon reference.
*/
private Object catalinaDaemon = null;
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
- daemon引用一個Bootstrap實例,可以在啟動后接受其他命令如stop;
- catalinaHomeFile表示Tomcat的安裝目錄;
- catalinaBaseFile表示Tomcat的工作目錄;
- catalinaDaemon表示正在運行的Catalina實例;
- commonLoader、catalinaLoader和sharedLoader分別引用初始化過程中創建的三個類加載器。
靜態代碼塊
靜態代碼塊為catalinaHomeFile和catalinaBaseFile兩個靜態final成員變量賦值,其代碼如下:
static {
// Will always be non-null
String userDir = System.getProperty("user.dir");
// Home first
String home = System.getProperty(Globals.CATALINA_HOME_PROP);
File homeFile = null;
if (home != null) {
File f = new File(home);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
if (homeFile == null) {
// First fall-back. See if current directory is a bin directory
// in a normal Tomcat install
File bootstrapJar = new File(userDir, "bootstrap.jar");
if (bootstrapJar.exists()) {
File f = new File(userDir, "..");
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
}
if (homeFile == null) {
// Second fall-back. Use current directory
File f = new File(userDir);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
catalinaHomeFile = homeFile;
System.setProperty(
Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
// Then base
String base = System.getProperty(Globals.CATALINA_BASE_PROP);
if (base == null) {
catalinaBaseFile = catalinaHomeFile;
} else {
File baseFile = new File(base);
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
catalinaBaseFile = baseFile;
}
System.setProperty(
Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}
- catalinaHomeFile變量保存Tomcat的安裝目錄,若系統屬性catalina.home存在則使用該屬性的值,否則若當前工作目錄中存在bootstrap.jar文件則使用當前工作目錄的父目錄,否則使用當前工作目錄;
- catalinaBaseFile變量保存Tomcat的工作目錄,若系統屬性catalina.base存在則使用該屬性的值,否則與catalinaHomeFile相同;
- 最終,系統屬性catalina.home和catalina.base都會被重新賦值。
main方法
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();
} 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);
}
}
- 從腳本啟動時,因為最后一個命令行參數是start,所以main方法會先執行init方法,然后執行load方法,最后是start方法;
- 其他命令行參數同理。
init方法
Bootstrap類的init方法代碼如下:
public void init() throws Exception {
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.getConstructor().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方法主要做了以下事情:
- 調用initClassLoaders方法初始化類加載器;
- 將catalinaLoader設置為自己的線程上下文類加載器;
- 利用catalinaLoader加載Catalina類并實例化對象。
初始化類加載器
在初始化類加載器過程中涉及到Catalina配置文件的加載和解析,過程如下:
- 若JVM參數指定了catalina.config則從該屬性值表示的路徑加載配置文件,若文件存在則轉到4,否則轉到2;
- 嘗試加載${catalina.home}/conf/catalina.properties文件,若文件存在則轉到4,否則轉到3;
- 嘗試加載${catalina.home}/bin/bootstrap.jar中的org/apache/catalina/startup/catalina.properties文件;
- 將配置文件中的屬性和值復制到系統屬性中。
默認的${catalina.home}/conf/catalina.properties配置文件部分內容如下,bootstrap.jar中的配置文件與其相似:
package.access=sun.,org.apache.catalina.,org.apache.coyote.,org.apache.jasper.,org.apache.tomcat.
package.definition=sun.,java.,org.apache.catalina.,org.apache.coyote.,\
org.apache.jasper.,org.apache.naming.,org.apache.tomcat.
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=
// 省略一些代碼
initClassLoaders方法會創建三個類加載器:
- commonLoader:父加載器是系統類加載器(ClassLoader類getSystemClassLoader方法的返回值),配置文件中以common.loader為鍵的屬性值表示的路徑會被添加到commonLoader的查找路徑中;
- catalinaLoader:父加載器是commonLoader,配置文件中以server.loader為鍵的屬性值表示的路徑會被添加到catalinaLoader的查找路徑中;
- sharedLoader:父加載器是commonLoader,配置文件中以shared.loader為鍵的屬性值表示的路徑會被添加到sharedLoader的查找路徑中;
- 若使用上述默認的配置文件,則${catalina.base}/lib目錄下未打包的類和資源、${catalina.base}/lib目錄下的jar、${catalina.home}/lib目錄下未打包的類和資源以及${catalina.home}/lib目錄下的jar都會被添加到commonLoader的搜索路徑中。
有關Tomcat的類加載器的更多信息可以參考Class Loader HOW-TO。
反射實例化Catalina類
在標準啟動(即從腳本啟動)時,啟動腳本為java命令傳遞了如下的參數:
-classpath /Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/bootstrap.jar:/Users/suntao/Documents/Workspace/apache-tomcat-8.5.33/bin/tomcat-juli.jar
可見應用類加載器只會從bootstrap.jar和tomcat-juli.jar中加載類。由于Bootstrap類存在于${catalina.home}/bin/bootstrap.jar,而Catalina類在只存在在于${catalina.home}/lib/catalina.jar,因此應用類加載器無法加載Catalina類,只能由創建的類加載器加載。
load方法
Bootstrap類的load方法代碼如下,利用反射在init方法生成的Catalina類實例上調用load方法,參數即為Bootstrap的命令行參數。
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);
}
start方法
Bootstrap類的start方法代碼如下,利用反射在init方法生成的Catalina類實例上調用Catalina類實例的start方法。
public void start()
throws Exception {
if( catalinaDaemon==null ) init();
Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
method.invoke(catalinaDaemon, (Object [])null);
}