熟悉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中有两个主要的变量,分别是:
根据最后一行代码: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的init方法(见代码清单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;
}当传递参数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);
}当传递参数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)的执行步骤如下:
/**
* 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方法。 /**
* Await and shutdown.
*/
public void await() {
getServer().await();
}以Server的默认实现StandardServer为例,其await方法(见代码清单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客户端如何知道服务端的连接地址与端口呢?下面会慢慢说明。sh shutdown.sh所以,将从shell脚本shutdown.sh开始分析Tomcat的停止过程。shutdown.sh的脚本代码见代码清单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非常相似,其中也有两个主要的变量,分别是: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方法的执行步骤如下:
/**
* 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)的执行步骤如下: 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)的实现,其执行步骤如下: /**
* 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);
}
}后记:个人总结整理的《深入理解Spark:核心思想与源码分析》一书现在已经正式出版上市,目前京东、当当、天猫等网站均有销售,欢迎感兴趣的同学购买。

京东:http://item.jd.com/11846120.html
当当:http://product.dangdang.com/23838168.html
原文:http://blog.csdn.net/beliefer/article/details/51585006