前言
熟悉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 - $ 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:
# . owned by the user
# . 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
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 ]; 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" >& "&" 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" >& "&" 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的类加载器,并设置线程上下文类加载器(具体实现细节,读者可以参考《TOMCAT源码分析——类加载体系》一文);
- 用反射实例化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配置文件,具体细节已在《TOMCAT源码分析——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容器,有关容器的启动过程的分析可以参考《TOMCAT源码分析——生命周期管理》一文的内容。
- 设置关闭钩子。这么说可能有些不好理解,那就换个说法。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非常相似,其中也有两个主要的变量,分别是:
- PRGDIR:当前shell脚本所在的路径;
- EXECUTABLE:脚本catalina.sh。
根据最后一行代码: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
已经在启动过程分析中介绍, 不再赘述。
步骤二 停止服务
通过调用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容器获取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容器。有关容器的停止内容,请阅读《TOMCAT源码分析——生命周期管理》一文。
代码清单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的通信。
如需转载,请标明本文作者及出处——作者:jiaan.gja,本文原创首发:博客园,原文链接:http://www.cnblogs.com/jiaan-geng/p/4872550.html