目錄
以Tomcat 9.0.30爲例,參考資料:《Tomcat架構解析》
1. 啓動入口
一般使用Tomcat,都是運行 $CATALINA_BASE/bin/startup.sh(CATALINA_BASE指Tomcat根目錄),所以先來看該腳本的主要內容:
# 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
其實就是根據命令,解析出startup.sh所在位置,然後拼出catalina.sh的路徑,並且執行該腳本。catalina.sh很長,所以不貼內容了,該腳本的主要功能就是配置環境變量,然後拼接Java命令,大概長這樣:
eval exec "\"$_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
從這裏可以看到,Tomcat的啓動類是org.apache.catalina.startup.Bootstrap,同時傳入了start指令。
2.Bootstrap啓動過程
進入Bootstrap#main方法,首先會嘗試同步初始化Bootstrap(去掉了源碼中日誌輸出內容):
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
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方法主要工作有:
1)初始化Tomcat的類加載器
2)嘗試加載並實例化Catalina類(Tomcat核心類)
3)爲Catalina設置ParentClassLoader爲SharedLoader
2.1 Tomcat類加載器結構
Java的類加載器結構分啓動類加載器、擴展類加載器、應用類加載器和自定義類加載器四級:
- 啓動類加載器: BootstrapClassLoader,負責加載存放在 JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被 -Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如rt.jar,所有的java.開頭的類均被 BootstrapClassLoader加載)。啓動類加載器是無法被Java程序直接引用的。
- 擴展類加載器: ExtensionClassLoader,該加載器由 sun.misc.Launcher$ExtClassLoader實現,它負責加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.開頭的類),開發者可以直接使用擴展類加載器。
- 應用類加載器: ApplicationClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
- 自定義類加載器:加載用戶代碼中指定的類
採用雙親委派機制,即先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。
Tomcat的類加載器也是分多層的,包括:
- Common ClassLoader:以應用類加載器爲父類,根據catalina.properties,負責加載"${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"中的類
- Server ClassLoader:以Common爲父加載器,從init方法可以看出,它是用來加載Catalina類的
- Shared ClassLoader:以Common爲父加載器,作爲Web應用的父加載器
- Web ClassLoader:負責加載Web應用中,WEB-INF/classes目錄下的class文件、資源文件,WEB-INF/lib目錄下的Jar包,只對負責加載的Web應用可見
不過Web應用加載器在init方法中還沒有被用到。
這種分層設計的優點,首先是保證了每個Web應用的獨立加載,互不干擾,其次如果需要重新部署,也不需要全局重啓
此外,在加載依賴時,可以讓共享的和私有的ClassLoader各司其職,兼顧性能和安全性
2.2 Bootstrap的啓動
退出同步塊後,會根據啓動命令中傳入的args選擇接下來的工作,默認start,即執行bootstrap的start方法。
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}
如果剛剛沒加載Catalina,則重新調用init方法,否則反射進入Catalina#start方法繼續啓動過程。
3.Catalina的啓動之Server的加載
start方法負責啓動Server。分爲四步:
- 加載Server
- 啓動Server
- 註冊JVM關機事件鉤子
- 設置Server等待請求
3.1 server.xml的加載和解析
首先看第一步。這一步主要是靠Digester工具去解析server.xml。可根據解析元素分爲幾塊:
1)Server
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
這裏指定了Server的默認實現類是StandardServer,在默認的server.xml中,Server標籤只配置了port和shutdown兩個屬性。
2)命名服務
digester.addObjectCreate("Server/GlobalNamingResources",
"org.apache.catalina.deploy.NamingResourcesImpl");
digester.addSetProperties("Server/GlobalNamingResources");
digester.addSetNext("Server/GlobalNamingResources",
"setGlobalNamingResources",
"org.apache.catalina.deploy.NamingResourcesImpl");
配置了命名服務實現爲NamingResourcesImpl。
3)監聽器
digester.addRule("Server/Listener",
new ListenerCreateRule(null, "className"));
digester.addSetProperties("Server/Listener");
digester.addSetNext("Server/Listener",
"addLifecycleListener",
"org.apache.catalina.LifecycleListener");
根據配置文件,默認加載了VersionLoggerListener、SecurityListener、AprLifecycleListener、JreMemoryLeakPreventionListener、GlobalResourcesLifecycleListener、ThreadLocalLeakPreventionListener這幾個監聽器。從代碼中可以看出,他們都實現了LifecycleListener接口,能夠處理Tomcat組件的生命週期事件。
4)Service
Service表示Connector(連接器,負責監聽和轉化Socket請求)和Container(容器,分爲Servlet引擎Engine、虛擬主機Host、Web應用Context和Servlet包裝類Wrapper四層)的集合。
因爲代碼太長,就不貼了,這部分主要步驟包括:
- 配置Service實例
- 添加生命週期監聽器(默認沒有配置任何監聽器)
- 添加Executor
- 添加Connector
- 爲Connector添加虛擬主機SSL配置
- 爲Connector添加生命週期監聽器(默認沒有)
- 爲Connector添加HTTP 2.0支持
- 添加子元素解析規則
- 以RuleSet形式,封裝了子元素,如GlobalNamingResources、Engine、Host、Context、Cluster等的解析規則
- 添加Cluster解析規則
到這一步,還只是完成了server.xml的解析,還沒有開始正式啓動服務器。
3.2 StandardServer的初始化
Server接口的init方法(繼承自Lifecycle)完成Server的創建和初始化。在父類LifycycleBase中,對init做了實現:
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
1.在實際初始化邏輯前後發佈事件;2.將實際初始化邏輯委託給initInternal方法
Lifecycle的其他幾個方法,如start、stop等,都被這樣封裝了一層。所以這裏看initInternal方法,上面絕大部分代碼用來將自身註冊爲MBean和加載JAR包,我們真正關心的,只有最後一點:
for (int i = 0; i < services.length; i++) {
services[i].init();
}
這裏循環初始化了內部的Service元素。
從源碼中,可以看到,Service的實現類是StandardService,其initInternal方法主要內容爲:
if (engine != null) {
engine.init();
}
// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// Initialize mapper listener
mapperListener.init();
// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
connector.init();
}
}
可見是將內部的Engine、Executor、MapperListener、Connector等元素逐個init一遍。顯然Engine是主要內容,所以先看其他幾個元素:
1)Executor:
根據源碼,默認實現是StandardThreadExecutor,它的initInternal方法沒做什麼事情,就是和其他initInterna方法一樣,調用一下父類同名方法,將自身註冊爲LifecycleMBean。
2)MapperListener:
乾脆連initInternal都沒實現,也就是說也是調用的父類方法
3)Connector
server.xml配置了HTTP 1.1和 AJP兩種Connector,AJP可參見百度百科介紹:https://baike.baidu.com/item/ajp/1187933?fr=aladdin
Connector結構如下所示:
ProtocolHandler對協議和I/O進行了實現,例如Http11NioProtocol就是NIO的HTTP處理器,內部包含的Endpoint用於Socket監聽,Processor用於按照指定協議讀取數據並交由Container處理。
所以Connector的初始化,實際就是ProtocolHandler的初始化。ProtocolHandler的默認實現爲Http11NioProtocol,初始化過程中完成了Endpoint的初始化,Http11NioProtocol對應的Endpoint是NioEndpoint。如果配置了初始化階段啓動,就會完成綁定,也就是初始化了ServerSocket:
public void bind() throws Exception {
initServerSocket();
setStopLatch(new CountDownLatch(1));
// Initialize SSL if needed
initialiseSsl();
selectorPool.open(getName());
}
initServerSocket方法用NIO的ServerSocketChannel.open()方法開啓ServerSocketChannel,並且設置爲了阻塞模式。然後配置了SSL。selectorPool是一個Selector池,open方法其實就是調用Selector.open()等待一個請求,然後把selector存入Poller(實際是一個線程),Poller會根據SelectionKey進行處理。
4)Engine:
Engine是Tomcat四層容器中的最頂層,默認實現是StandardEngine。StandardEngine的initInternal方法沒有像其他組件那樣繼續向下初始化,而是在確保Realm實現不爲空(沒有配置就構造爲NullRealm)之後就結束了。
不過,四層容器的構造方法中,都對Pipeline和默認Valve做了初始化。Tomcat採用責任鏈模式具體處理請求,Pipeline就是一條責任鏈,Valve就是鏈上的處理器。每個層級的Container都有對應的Pipeline和默認Valve實現,在鏈的最後執行,負責請求處理和輸出響應,後面添加的Valve都是添加到它的前面。
所有的默認Valve命名都類似於StandardXXXValve(XXX就是對應的容器)。
3.3 啓動的後處理
因爲Server的啓動比較長,所以先來看關機鉤子。
關機鉤子的實現類是CatalinaShutdownHook,源碼如下:
protected class CatalinaShutdownHook extends Thread {
@Override
public void run() {
try {
if (getServer() != null) {
Catalina.this.stop();
}
} catch (Throwable ex) {
ExceptionUtils.handleThrowable(ex);
log.error(sm.getString("catalina.shutdownHookFail"), ex);
} finally {
// If JULI is used, shut JULI down *after* the server shuts down
// so log messages aren't lost
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).shutdown();
}
}
}
}
其實就是調用了stop方法,該方法會從Server開始,通過成員變量方法調用的方式,將加載和啓動的所有組件都逐一關閉。
至於Server的await方法,就屬於請求處理部分了