版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/beliefer/article/details/51585006
前言
熟悉Tomcat的工程師們,肯定都知道Tomcat是如何啟動與停止的。對于startup.sh、startup.bat、shutdown.sh、shutdown.bat等腳本或者批處理指令,大家一定知道改如何使用它,但是它們究竟是如何實作的,尤其是shutdown.sh腳本(或者shutdown.bat)究竟是如何和Tomcat程序通信的呢?本文将通過對Tomcat7.0的源碼閱讀,深入剖析這一過程。
由于在生産環境中,Tomcat一般部署在Linux系統下,是以本文将以startup.sh和shutdown.sh等shell腳本為準,對Tomcat的啟動與停止進行分析。
啟動過程分析
我們啟動Tomcat的指令如下:
sh startup.sh
是以,将從shell腳本startup.sh開始分析Tomcat的啟動過程。startup.sh的腳本代碼見代碼清單1。
代碼清單1
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 "$@"
代碼清單1中有兩個主要的變量,分别是:
- PRGDIR:目前shell腳本所在的路徑;
- EXECUTABLE:腳本catalina.sh。
根據最後一行代碼:exec "$PRGDIR"/"$EXECUTABLE" start "$@",我們知道執行了shell腳本catalina.sh,并且傳遞參數start。catalina.sh中接收到start參數後的執行的腳本分支見代碼清單2。
代碼清單2
elif [ "$1" = "start" ] ; then
# 此處省略參數校驗的腳本
shift
touch "$CATALINA_OUT"
if [ "$1" = "-security" ] ; then
if [ $have_tty -eq 1 ]; then
echo "Using Security Manager"
fi
shift
eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -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 "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-Djava.endorsed.dirs="\"$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."
從代碼清單2可以看出,最終使用java指令執行了org.apache.catalina.startup.Bootstrap類中的main方法,參數也是start。Bootstrap的main方法的實作見代碼清單3。
代碼清單3
/**
* Main method, used for testing only.
*
* @param args Command line arguments to be processed
*/
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) {
t.printStackTrace();
return;
}
daemon = bootstrap;
}
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 {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
t.printStackTrace();
}
}
從代碼清單3可以看出,當傳遞參數start的時候,command等于start,此時main方法的執行步驟如下:
步驟一 初始化Bootstrap
Bootstrap的init方法(見代碼清單4)的執行步驟如下:
- 設定Catalina路徑,預設為Tomcat的根目錄;
- 初始化Tomcat的類加載器,并設定線程上下文類加載器(具體實作細節,讀者可以參考 《Tomcat7.0源碼分析——類加載體系》 一文);
- 用反射執行個體化org.apache.catalina.startup.Catalina對象,并且使用反射調用其setParentClassLoader方法,給Catalina對象設定Tomcat類加載體系的頂級加載器(Java自帶的三種類加載器除外)。
代碼清單4
/**
* Initialize daemon.
*/
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;
}
步驟二 加載、解析server.xml配置檔案
當傳遞參數start的時候,會調用Bootstrap的load方法(見代碼清單5),其作用是用反射調用catalinaDaemon(類型是Catalina)的load方法加載和解析server.xml配置檔案,具體細節已在
《Tomcat7.0源碼分析——server.xml檔案的加載與解析》一文中詳細介紹,有興趣的朋友可以選擇閱讀。
代碼清單5
/**
* Load daemon.
*/
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);
}
步驟三 啟動Tomcat
當傳遞參數start的時候,調用Bootstrap的load方法之後會接着調用start方法(見代碼清單6)啟動Tomcat,此方法實際是用反射調用了catalinaDaemon(類型是Catalina)的start方法。
代碼清單6
/**
* Start the Catalina daemon.
*/
public void start()
throws Exception {
if( catalinaDaemon==null ) init();
Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
method.invoke(catalinaDaemon, (Object [])null);
}
Catalina的start方法(見代碼清單7)的執行步驟如下:
- 驗證Server容器是否已經執行個體化。如果沒有執行個體化Server容器,還會再次調用Catalina的load方法加載和解析server.xml,這也說明Tomcat隻允許Server容器通過配置在server.xml的方式生成,使用者也可以自己實作Server接口建立自定義的Server容器以取代預設的StandardServer。
- 啟動Server容器,有關容器的啟動過程的分析可以參考 《Tomcat7.0源碼分析——生命周期管理》 一文的内容。
- 設定關閉鈎子。這麼說可能有些不好了解,那就換個說法。Tomcat本身可能由于所在機器斷點,程式bug甚至記憶體溢出導緻程序退出,但是Tomcat可能需要在退出的時候做一些清理工作,比如:記憶體清理、對象銷毀等。這些清理動作需要封裝在一個Thread的實作中,然後将此Thread對象作為參數傳遞給Runtime的addShutdownHook方法即可。
- 最後調用Catalina的await方法循環等待接收Tomcat的shutdown指令。
- 如果Tomcat運作正常且沒有收到shutdown指令,是不會向下執行stop方法的,當接收到shutdown指令,Catalina的await方法會退出循環等待,然後順序執行stop方法停止Tomcat。
代碼清單7
/**
* Start a new server instance.
*/
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.error("Catalina.start: ", e);
}
long t2 = System.nanoTime();
if(log.isInfoEnabled())
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
try {
// Register shutdown hook
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);
}
}
} catch (Throwable t) {
// This will fail on JDK 1.2. Ignoring, as Tomcat can run
// fine without the shutdown hook.
}
if (await) {
await();
stop();
}
}
Catalina的await方法(見代碼清單8)實際隻是代理執行了Server容器的await方法。
代碼清單8
/**
* Await and shutdown.
*/
public void await() {
getServer().await();
}
以Server的預設實作StandardServer為例,其await方法(見代碼清單9)的執行步驟如下:
- 建立socket連接配接的服務端對象ServerSocket;
- 循環等待接收用戶端發出的指令,如果接收到的指令與SHUTDOWN比對(由于使用了equals,是以shutdown指令必須是大寫的),那麼退出循環等待。
代碼清單9
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports gja
if( port == -2 ) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if( port==-1 ) {
while( true ) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
}
if( stopAwait ) return;
}
}
// Set up a server socket to wait on
ServerSocket serverSocket = null;
try {
serverSocket =
new ServerSocket(port, 1,
InetAddress.getByName(address));
} catch (IOException e) {
log.error("StandardServer.await: create[" + address
+ ":" + port
+ "]: ", e);
System.exit(1);
}
// Loop waiting for a connection and a valid command
while (true) {
// Wait for the next connection
Socket socket = null;
InputStream stream = null;
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) {
log.error("StandardServer.await: accept: ", e);
System.exit(1);
}
// Read a set of characters from the socket
StringBuilder command = new StringBuilder();
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--;
}
// Close the socket now that we are done with it
try {
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");
}
// Close the server socket and return
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
至此,Tomcat啟動完畢。很多人可能會問,執行sh shutdown.sh腳本時,是如何與Tomcat程序通信的呢?如果要與Tomcat的ServerSocket通信,socket用戶端如何知道服務端的連接配接位址與端口呢?下面會慢慢說明。
停止過程分析
我們停止Tomcat的指令如下:
sh shutdown.sh
是以,将從shell腳本shutdown.sh開始分析Tomcat的停止過程。shutdown.sh的腳本代碼見代碼清單10。
代碼清單10
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" stop "$@"
代碼清單10和代碼清單1非常相似,其中也有兩個主要的變量,分别是:
根據最後一行代碼:exec "$PRGDIR"/"$EXECUTABLE" stop "$@",我們知道執行了shell腳本catalina.sh,并且傳遞參數stop。catalina.sh中接收到stop參數後的執行的腳本分支見代碼清單11。
代碼清單11
elif [ "$1" = "stop" ] ; then
#省略參數校驗腳本
eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_OPTS \
-Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
org.apache.catalina.startup.Bootstrap "$@" stop
從代碼清單11可以看出,最終使用java指令執行了org.apache.catalina.startup.Bootstrap類中的main方法,參數是stop。從代碼清單3可以看出,當傳遞參數stop的時候,command等于stop,此時main方法的執行步驟如下:
已經在啟動過程分析中介紹, 不再贅述。
步驟二 停止服務
通過調用Bootstrap的stopServer方法(見代碼清單12)停止Tomcat,其實質是用反射調用catalinaDaemon(類型是Catalina)的stopServer方法。
代碼清單12
/**
* Stop the standalone server.
*/
public void stopServer(String[] arguments)
throws Exception {
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("stopServer", paramTypes);
method.invoke(catalinaDaemon, param);
}
Catalina的stopServer方法(見代碼清單13)的執行步驟如下:
- 建立Digester解析server.xml檔案(此處隻解析标簽),以構造出Server容器(此時Server容器的子容器沒有被執行個體化);
- 從執行個體化的Server容器擷取Server的socket監聽端口和位址,然後建立Socket對象連接配接啟動Tomcat時建立的ServerSocket,最後向ServerSocket發送SHUTDOWN指令。根據代碼清單9的内容,ServerSocket循環等待接收到SHUTDOWN指令後,最終調用stop方法停止Tomcat。
代碼清單13
public void stopServer() {
stopServer(null);
}
public void stopServer(String[] arguments) {
if (arguments != null) {
arguments(arguments);
}
if( getServer() == null ) {
// Create and execute our Digester
Digester digester = createStopDigester();
digester.setClassLoader(Thread.currentThread().getContextClassLoader());
File file = configFile();
try {
InputSource is =
new InputSource("file://" + file.getAbsolutePath());
FileInputStream fis = new FileInputStream(file);
is.setByteStream(fis);
digester.push(this);
digester.parse(is);
fis.close();
} catch (Exception e) {
log.error("Catalina.stop: ", e);
System.exit(1);
}
}
// Stop the existing server
try {
if (getServer().getPort()>0) {
Socket socket = new Socket(getServer().getAddress(),
getServer().getPort());
OutputStream stream = socket.getOutputStream();
String shutdown = getServer().getShutdown();
for (int i = 0; i < shutdown.length(); i++)
stream.write(shutdown.charAt(i));
stream.flush();
stream.close();
socket.close();
} else {
log.error(sm.getString("catalina.stopServer"));
System.exit(1);
}
} catch (IOException e) {
log.error("Catalina.stop: ", e);
System.exit(1);
}
}
最後,我們看看Catalina的stop方法(見代碼清單14)的實作,其執行步驟如下:
- 将啟動過程中添加的關閉鈎子移除。Tomcat啟動過程辛辛苦苦添加的關閉鈎子為什麼又要去掉呢?因為關閉鈎子是為了在JVM異常退出後,進行資源的回收工作。主動停止Tomcat時調用的stop方法裡已經包含了資源回收的内容,是以不再需要這個鈎子了。
- 停止Server容器。有關容器的停止内容,請閱讀 一文。
代碼清單14
/**
* Stop an existing server instance.
*/
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 jiaan
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
true);
}
}
} catch (Throwable t) {
// This will fail on JDK 1.2. Ignoring, as Tomcat can run
// fine without the shutdown hook.
}
// Shut down the server
try {
getServer().stop();
} catch (LifecycleException e) {
log.error("Catalina.stop", e);
}
}
總結
通過對Tomcat源碼的分析我們了解到Tomcat的啟動和停止都離不開org.apache.catalina.startup.Bootstrap。當停止Tomcat時,已經啟動的Tomcat作為socket服務端,停止腳本啟動的Bootstrap程序作為socket用戶端向服務端發送shutdown指令,兩個程序通過共享server.xml裡Server标簽的端口以及位址資訊打通了socket的通信。
後記:個人總結整理的《深入了解Spark:核心思想與源碼分析》一書現在已經正式出版上市,目前京東、當當、天貓等網站均有銷售,歡迎感興趣的同學購買。
京東:
http://item.jd.com/11846120.html當當:
http://product.dangdang.com/23838168.html