前言
這是我們分析 Tomcat 的第七篇文章,前面我們依據(jù)啟動過程理解了類加載過程,生命周期組件,容器組件等。基本上將啟動過程拆的七零八落,分析的差不多了, 但是還沒有從整體的視圖下來分析Tomcat 的啟動過程。因此,這篇文章的任務就是這個,我們想將Tomcat的啟動過程徹底的摸清,把它最后一件衣服扒掉。然后我們就分析連接器和URL請求了,不再留戀這里了。
好吧。我們開始吧。
說到Tomcat的啟動,我們都知道,我們每次需要運行tomcat/bin/startup.sh這個腳本,而這個腳本的內(nèi)容到底是什么呢?我們來看看。
1. startup.sh 腳本內(nèi)容
#!/bin/sh
os400=false
case "`uname`" in
OS400*) os400=true;;
esac
# 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 "$@"
樓主刪除了一些無用的注釋,我們來看看這腳本。該腳本中有2個重要的變量:
- PRGDIR:表示當前腳本所在的路徑
- EXECUTABLE:catalina.sh 腳本名稱
其中最關鍵的一行代碼就是exec "$PRGDIR"/"$EXECUTABLE" start "$@",表示執(zhí)行了腳本catalina.sh,參數(shù)是start。
2. catalina.sh 腳本實現(xiàn)
然后我們看看catalina.sh 腳本中的實現(xiàn):
elif [ "$1" = "start" ] ; then
if [ ! -z "$CATALINA_PID" ]; then
if [ -f "$CATALINA_PID" ]; then
if [ -s "$CATALINA_PID" ]; then
echo "Existing PID file found during start."
if [ -r "$CATALINA_PID" ]; then
PID=`cat "$CATALINA_PID"`
ps -p $PID >/dev/null 2>&1
if [ $? -eq 0 ] ; then
echo "Tomcat appears to still be running with PID $PID. Start aborted."
echo "If the following process is not a Tomcat process, remove the PID file and try again:"
ps -f -p $PID
exit 1
else
echo "Removing/clearing stale PID file."
rm -f "$CATALINA_PID" >/dev/null 2>&1
if [ $? != 0 ]; then
if [ -w "$CATALINA_PID" ]; then
cat /dev/null > "$CATALINA_PID"
else
echo "Unable to remove or clear stale PID file. Start aborted."
exit 1
fi
fi
fi
else
echo "Unable to read PID file. Start aborted."
exit 1
fi
else
rm -f "$CATALINA_PID" >/dev/null 2>&1
if [ $? != 0 ]; then
if [ ! -w "$CATALINA_PID" ]; then
echo "Unable to remove or write to empty PID file. Start aborted."
exit 1
fi
fi
fi
fi
fi
shift
touch "$CATALINA_OUT"
if [ "$1" = "-security" ] ; then
if [ $have_tty -eq 1 ]; then
echo "Using Security Manager"
fi
shift
eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-classpath "\"$CLASSPATH\"" \
-Djava.security.manager \
-Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" start \
>> "$CATALINA_OUT" 2>&1 "&"
else
eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-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."
該腳本很長,但我們只關心我們感興趣的:如果參數(shù)是 start, 那么執(zhí)行這里的邏輯,關鍵再最后一行執(zhí)行了 org.apache.catalina.startup.Bootstrap "$@" start, 也就是說,執(zhí)行了我們熟悉的main方法,并且攜帶了start 參數(shù),那么我們就來看Bootstrap 的main方法是如何實現(xiàn)的。
3. Bootstrap.main 方法實現(xiàn)
public static void main(String args[]) {
System.err.println("Have fun and Enjoy! cxs");
// daemon 就是 bootstrap
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
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) {
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
我們看看該方法, 首先 bootstrap.init() 的方法用于初始化類加載器,我們已經(jīng)分析過該方法了,就不再贅述了,然后我們看下面的try塊,默認命令行參數(shù)是 start ,但我們剛剛的腳本傳的參數(shù)就是 start, 因此進入該if塊
else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
- 設置catalina 的 await 屬性為true;
- 運行 catalina 的 load 方法。該方法內(nèi)部主要邏輯是解析server.xml文件,初始化容器。我們已經(jīng)再生命周期那篇文章中講過容器的初始化。
- 運行 catalina 的 start 方法。也就是啟動 tomcat。這個部分我們上次分析了容器啟動。但是容器之后的邏輯我們沒有分析。今天我們就來看看。
4. Catalina.start 方法
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// Start the new server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
}
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
if (await) {
await();
stop();
}
}
該方法我們上次分析到了 getServer().start() 這里,也就是容器啟動的邏輯,我們不再贅述。
今天我們繼續(xù)分析下面的邏輯。主要邏輯是:
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
if (await) {
await();
stop();
}
可以看到是 Runtime.getRuntime().addShutdownHook(shutdownHook)方法。那么這個方法的作用是什么呢?JDK 文檔是這樣說的:
注冊新的虛擬機來關閉鉤子。
只是一個已初始化但尚未啟動的線程。虛擬機開始啟用其關閉序列時,它會以某種未指定的順序啟動所有已注冊的關閉鉤子,并讓它們同時運行。運行完所有的鉤子后,如果已啟用退出終結,那么虛擬機接著會運行所有未調(diào)用的終結方法。最后,虛擬機會暫停。注意,關閉序列期間會繼續(xù)運行守護線程,如果通過調(diào)用方法來發(fā)起關閉序列,那么也會繼續(xù)運行非守護線程。
簡單來說,如果用戶的程序出現(xiàn)了bug, 或者使用control + C 關閉了命令行,那么就需要做一些內(nèi)存清理的工作。該方法就會再虛擬機退出時做清理工作。再ApplicationShutdownHooks 類種維護著一個IdentityHashMap<Thread, Thread> Map,用于后臺清理工作。那么該線程對象的run方法中是什么邏輯呢?我們來看看:
5. CatalinaShutdownHook.run 線程方法實現(xiàn)
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();
}
}
}
}
該線程是Catalina的內(nèi)部類,方法邏輯是,如果Server容器還存在,就是執(zhí)行Catalina的stop方法用于停止容器。(為什么要用Catalina.this.stop 呢?因為它繼承了Thread,而Thread也有一個stop方法,因此需要顯式的指定該方法)最后關閉日志管理器。我們看看stop方法的實現(xiàn):
6. Catalina.stop 方法實現(xiàn):
public void stop() {
try {
// Remove the ShutdownHook first so that server.stop()
// doesn't get invoked twice
if (useShutdownHook) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
// If JULI is being used, re-enable JULI's shutdown to ensure
// log messages are not lost
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
true);
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// This will fail on JDK 1.2. Ignoring, as Tomcat can run
// fine without the shutdown hook.
}
// Shut down the server
try {
Server s = getServer();
LifecycleState state = s.getState();
if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
&& LifecycleState.DESTROYED.compareTo(state) >= 0) {
// Nothing to do. stop() was already called
} else {
s.stop();
s.destroy();
}
} catch (LifecycleException e) {
log.error("Catalina.stop", e);
}
}
該方法首先移除關閉鉤子,為什么要移除呢,因為他的任務已經(jīng)完成了。然后設置useShutdownHook 為true。最后執(zhí)行Server的stop方法,Server的stop方法基本和init方法和start方法一樣,都是使用父類的模板方法,首先出發(fā)事件,然后調(diào)用stopInternal,該方法內(nèi)部循環(huán)停止子容器,子容器遞歸停止,和我們之前的邏輯一致,不再贅述。destroy方法同理。
好了,我們已經(jīng)看清了關閉鉤子的邏輯,其實就是開辟一個守護線程交給虛擬機,然后虛擬機在某些異常情況(比如System.exit(0))前執(zhí)行停止容器的邏輯。
好。我們回到start方法。
7. 回到 Catalina.start 方法
在設置好關閉鉤子后,tomcat 的啟動過程還沒有啟動完畢,接下來的邏輯式什么呢?
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
if (await) {
await();
stop();
}
在設置完關閉鉤子之后,會將 useShutdownHook 這個變量為false,然后執(zhí)行 await 方法。然后執(zhí)行stop方法,我們記得stop方法式關閉容器的方法,神經(jīng)病啊,好不容易啟動了,為什么又要關閉呢? 先不著急,我們還是看看 await 方法吧,該方法調(diào)用了Server.await 方法,我們來看看:
8. Catalian.await 方法實現(xiàn)
注意:該方法很長
@Override
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports
if( port == -2 ) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if( port==-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(port, 1,
InetAddress.getByName(address));
} catch (IOException e) {
log.error("StandardServer.await: create[" + address
+ ":" + port
+ "]: ", 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;
try {
socket = serverSocket.accept();
socket.setSoTimeout(10 * 1000); // Ten seconds
stream = socket.getInputStream();
} 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;
}
if (ch < 32) // Control character or EOF terminates loop
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
}
}
}
}
我們看一下他的邏輯:首先創(chuàng)建一個socketServer 鏈接,然后循環(huán)等待消息。如果發(fā)過來的消息為字符串SHUTDOWN, 那么就break,停止循環(huán),關閉socket。否則永不停歇。回到我們剛剛的疑問,await 方法后面執(zhí)行 stop 方法,現(xiàn)在一看就合情合理了,只要不發(fā)出關閉命令,則不會執(zhí)行stop方法,否則則繼續(xù)執(zhí)行關閉方法。
到現(xiàn)在,Tomcat 的整體啟動過程我們已經(jīng)了然于胸了,總結一下就是:
- 初始化類加載器。
- 初始化容器并注冊到JMX后啟動容器。
- 設置關閉鉤子。
- 循環(huán)等待關閉命令。
等一下。好像缺了點什么??? Tomcat 啟動后就只接受關閉命令,接受的http請求怎么處理,還要不要做一個合格的服務器了??? 別急,實際上,這個是主線程,負責生命周期等事情。處理Http請求的線程在初始化容器和啟動容器的時候由子容器做了,這塊的邏輯我們下次再講。大家不要疑惑。
9. 我們知道了Tomcat 是怎么啟動的,那么是怎么關閉的呢?
順便說說關閉的邏輯:
shutdown.sh 腳本同樣會調(diào)用 Bootstrap的main 方法,不同是傳遞 stop參數(shù), 我們看看如果傳遞stop參數(shù)會怎么樣:
ry {
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) {
可以看到調(diào)用的是 stopServer 方法,實際上就是 Catalina的stopServer 方法,我們看看該方法實現(xiàn):
10. Catalina.stopServer 方法
public void stopServer(String[] arguments) {
if (arguments != null) {
arguments(arguments);
}
Server s = getServer();
if( s == null ) {
// Create and execute our Digester
Digester digester = createStopDigester();
digester.setClassLoader(Thread.currentThread().getContextClassLoader());
File file = configFile();
FileInputStream fis = null;
try {
InputSource is =
new InputSource(file.toURI().toURL().toString());
fis = new FileInputStream(file);
is.setByteStream(fis);
digester.push(this);
digester.parse(is);
} catch (Exception e) {
log.error("Catalina.stop: ", e);
System.exit(1);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// Ignore
}
}
}
} else {
// Server object already present. Must be running as a service
try {
s.stop();
} catch (LifecycleException e) {
log.error("Catalina.stop: ", e);
}
return;
}
// Stop the existing server
s = getServer();
if (s.getPort()>0) {
Socket socket = null;
OutputStream stream = null;
try {
socket = new Socket(s.getAddress(), s.getPort());
stream = socket.getOutputStream();
String shutdown = s.getShutdown();
for (int i = 0; i < shutdown.length(); i++) {
stream.write(shutdown.charAt(i));
}
stream.flush();
} catch (ConnectException ce) {
log.error(sm.getString("catalina.stopServer.connectException",
s.getAddress(),
String.valueOf(s.getPort())));
log.error("Catalina.stop: ", ce);
System.exit(1);
} catch (IOException e) {
log.error("Catalina.stop: ", e);
System.exit(1);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// Ignore
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// Ignore
}
}
}
} else {
log.error(sm.getString("catalina.stopServer"));
System.exit(1);
}
}
注意,該停止命令的虛擬機和啟動的虛擬機不是一個虛擬機,因此,沒有初始化 Server , 進入 IF 塊,解析 server.xml 文件,獲取文件中端口,用以創(chuàng)建Socket。然后像啟動服務器發(fā)送 SHUTDOWN 命令,關閉啟動服務器,啟動服務器退出剛剛的循環(huán),執(zhí)行后面的 stop 方法,最后退出虛擬機,就是這么簡單。
11. 總結
我們從整體上解析了Tomcat的啟動和關閉過程,發(fā)現(xiàn)不是很難,為什么?因為我們之前已經(jīng)分析過很多遍了,有些邏輯我們已經(jīng)清除了,這次分析只是來掃尾。復雜的Tomcat的啟動過程我們基本就分析完了。我們知道了啟動和關閉都依賴Socket。只是我們驚奇的發(fā)現(xiàn)他的關閉竟然是如此實現(xiàn)。很牛逼。我原以為會像我們平時一樣,直接kill。哈哈哈。
好吧。今天我們就到這里 ,tomcat 這座大山我們已經(jīng)啃的差不多了,還剩一個 URL 請求過程和連接器,這兩個部分是高度關聯(lián)的,因此,樓主也會將他們放在一起分析。透過源碼看真相。
連接器,等著我們來撕開你的衣服?。。?!
good luck !!?。?/p>