作为一个成熟的中间件产品,tomcat有很多值得我们学习的地方。下面我将从tomcat的运行脚本catalina.sh入手,分析tomcat的启动过程,以及tomcat脚本是如何实现停止tomcat服务的。
注意:此处分析的是linux环境下的脚本文件
tomcat目录结构如下:
bin目录的内容如下:
我会介绍每个脚本的主要逻辑,并对涉及到的linux命令做简要介绍。脚本中做了大部分的注释,请结合注释理解。毕竟是代码,干说是很难描述的,注释效果更好。
#!/bin/sh
# 1)检测是否为os400操作系统
os400=false
case "`uname`" in
OS400*) os400=true;;
esac
# 2)PRG表示脚本路径,如果当前脚本文件为软链接,则会解析出PRG真正文件所在路径
# resolve links - $0 may be a softlink
PRG="$0"
while [ -h "$PRG" ] ; do # -h 判断是否为软链接
ls=`ls -ld "$PRG"` # 如果为软链接,输出中含有 link -> source 的字串
link=`expr "$ls" : '.*-> \(.*\)$'` # 模式匹配出源文件的路径,对这里感觉模糊请搜索“expr模式匹配”
if expr "$link" : '/.*' > /dev/null; then # 匹配 /.*,这里expr会输出匹配个数,如果不为0,说明$link包含目录
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
# 3)执行catalina.sh start
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
脚本主要逻辑如下:
1. 检测操作系统,以兼容特定操作系统的特性
2. 获取脚本路径
这段代码几乎在所有启动脚本、停止脚本的开头都会出现。为了防止获取到的路径是软链接的路径,需要对路径做解析。
1)expr string : regex
这种形式下,会用:后面的正则表达式匹配前面的字符串,输出匹配的个数
2)expr string : xxx\(zzz\ )
这种形式下,\(zzz\)中,zzz匹配的字符串会被输出
3.执行catalina.sh start
总结:startup.sh 调用 catalina.sh start
逻辑和startup.sh一样,最终调用 catalina.sh stop
从上面的分析我们知道,其他的脚本最终会以不同的参数调用catalina脚本,那么,catalina脚本的主要逻辑是什么呢?
由于catalina.sh脚本较长,截取一些片段来分析:
1.检测操作系统
2.获取脚本路径$PRG
3.设置两个重要的环境变量,CATALINA_HOME、CATALINA_BASE
一般情况下,这两个变量的值相同,都是tomcat根目录
# Get standard environment variables
PRGDIR=`dirname "$PRG"`
# Only set CATALINA_HOME if not already set
[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd` # $CATALINA_HOME即tomcat根目录
# Copy CATALINA_BASE from CATALINA_HOME if not already set
[ -z "$CATALINA_BASE" ] && CATALINA_BASE="$CATALINA_HOME" # $CATALINA_BASE等同$CATALINA_HOME
4.设置CLASSPATH变量
#在当前shell环境执行setenv.sh,设置CLASSPATH环境变量,setenv.sh默认不存在,如果用户需要添加额外的CLASSPATH,在这个文件添加
if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then
. "$CATALINA_BASE/bin/setenv.sh"
elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then
. "$CATALINA_HOME/bin/setenv.sh"
fi
5.在CLASSPATH后追加Bootstrap.jar、Tomcat-juli.jar
# Add on extra jar files to CLASSPATH
if [ ! -z "$CLASSPATH" ] ; then
CLASSPATH="$CLASSPATH":
fi
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar #将bootstrap.jar作为CLASSPATH
# Add tomcat-juli.jar to classpath
# tomcat-juli.jar can be over-ridden per instance
if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar #添加tomcat-juli.jar到classpath
else
CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
fi
6.解析脚本参数,执行Bootstrap类的main方法,并传入相应的参数
java命令执行Bootstrap类的main方法,将start作为参数传入
eval $_NOHUP "\"$_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 \
>> "$CATALINA_OUT" 2>&1 "&"
java命令执行Bootstrap类的main方法,将stop作为参数传入
至此,整个过程结束。
脚本中多次出现对$CATALINA_PID这个文件的操作,这里简单介绍一下这个文件的作用:
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
1.$CATALINA变量默认tomcat没有启用,用户如果要用,就要在脚本中自己定义该变量,他表示的记录tomcat进程id的文件路径
2.start时,会先检测$CATALINA文件是否存在,如果存在并且内容不为空,说明tomcat进程已经启动,则启动失败
3.start成功,如果定义了$CATALINA变量,则将进程id写入该文件
4.stop时,先执行 Bootstrap.main stop,如果不成功并且$CATALINA存在,则尝试使用kill命令杀死进程
5.stop成功,如果定义了$CATALINA变量,则删除$CATALINA文件
由catalina.sh我们知道,启动和停止tomcat都是以Bootstrap类作为主类运行。
这里,我们主要关注一个问题:
执行Bootstrap stop,为何可以停止另一个进程中的tomcat呢?要知道每次执行脚本中的java命令,我们都启动了一个新的进程。我们下面源码的分析只关注这个问题。
以下是main方法代码片段:
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) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
主要注意“start”和“stop"的部分,在Bootstrap中对daemon变量的操作,最终都变为对Catalina的操作,方法名映射是一致的。那么我们主要关注的是Catalina.start()和Catalina.stopServer()方法了。
这里主要是调用了Server.start,其中我们要注意的片段是:
if (await) {
await();
stop();
}
我们可以大致猜到代码的意思:等待,等待结束则执行stop()。
到底在等待什么呢?看StandardServer.await,谜团解开了:
// 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;
}
此处建立了ServerSocket,监听的端口是 conf/server.xml中配置的shutdown端口,默认是8005,当该端口接收到"SHUTDOWN"请求,await()结束,进而执行stop()方法。
server.xml中shutdown端口:
通过网络通信的方式,tomcat实现了从一个Java进程关闭另一个Java进程。
从对Catalina.start的分析,我们知道这个方法会向另一个Java进程发送shutdown命令,验证我们的假设:
Catalina.stopServer代码片段:
// 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
}
}
}
我们分析了tomcat是如何通过catalina.sh脚本启动和停止tomcat进程的,而catalina.sh的逻辑基本上适用任何的Java程序。
Java程序的脚本,最终的逻辑基本上是调用java命令执行Java程序,总结几个注意的点:
1.脚本路径的获取
2.CLASSPATH的获取
3.PID文件记录启动的Java进程id
4.如何停止Java进程
注意:通过Runtime.getRuntime().addShutdownHook设置钩子线程,可相应kill命令,从而在关闭进程前执行有效的清理工作。