为什么我想研究Tomcat源码,我们现在都用的是SpringBoot开发项目,而SpringBoot对错Tomcat集成,导致现在基本上看不到Tomcat的身影了,但是Tomcat不存在吗?只要我们用Java开发Web项目,而它又无处不在,我看了《Tomcat内核设计剖析》 , 《Tomcat与Java Web开发技术详解(第3版)》,《How Tomcat Works 》,都让我觉得意由未尽, 你说他不好嘛,其实理论也讲得非常好了,你说他好嘛,感觉意由未尽,看完之后,过几天后,感觉又忘记了, 《How Tomcat Works 》这本书呢?也非常好,让你知道Tomcat 中的代码为什么这样写,就是版本老了一些,因为我们基本上用的都是Tomcat7 以上了,而这本书讲的是Tomcat4 ,Tomcat5 相关的东西,感觉他讲的和Tomcat 7 ,8 源码中的对应不上,我个人最喜欢的还是《Spring源码深度解析》这本书的写作方式,通过举例子,再深入源码研究, 这本书我觉得即使有不足之处,但也不影响我对这本书的喜欢,为什么我说有不足之处呢?因为在讲Bean的扫描的时候,他只讲了传统的xml解析并读取的方式,这作为写作来说,肯定是很好写的,因为其中用了大量的设置模式,但并不实用,我们现在都是基于注解的方式去注册Bean,在《Spring源码深度解析》书中并没有讲解到,但是如果要讲注解注册Bean,一定要去理解ASM的实现机制,去理解字节码的结构,才能理解这些注解是怎样读取出来的,其中代码中用了大量的递归,源码分析想通过书面清晰准确的表达出来还是非常不容易的,可能也是我对《Spring源码深度解析》这本书的一些遗憾的地方吧,但是这本书的写作方式,确实我再喜欢不过,今天我们分析Tomcat源码,也用这种方式来分析,希望你有所收获 。
我们在服务器环境下启动tomcat的情况比较多,因此,我们就分析linux环境下tomcat 的启动脚本和关闭脚本
tomcat 的启动,我们用两种方式,第一种方式,直接运行startup脚本,在看脚本之前,我们先了解一下下面的 参数在脚本中的含义。
先来看startup.sh 脚本
os400=false
case "`uname`" in # 当前操作系统是不是os400开头的,如果是,则将os400变量设置为true
OS400*) os400=true;;
esac
# resolve links - $0 may be a softlink
# 如果startup 后面不接任何参数,PRG = startup.sh文件名
PRG="$0"
# 判断startup.sh 文件是不是软链接,如果是软链接,则递归找到真实的文件名
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
# dirname 命名的作用是获取文件的目录,如dirname /a/b/c.txt ,则结果为/a/b
PRG=`dirname "$PRG"`/"$link"
fi
done
// 如果 $PRG startup.sh,则dirname "$PRG"命令结果为startup.sh文件所在目录
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
# 检查catalina.sh文件是否存在,并且可执行,如果不可执行,或不存在,则打印提示
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
# 执行tomcat_home/bin/catalina.sh start "$@" 命令
exec "$PRGDIR"/"$EXECUTABLE" start "$@"
其实上面的代码看上去很麻烦,但理解也还是很简单的,先看当前startup.sh 是不是一个软链接,如果是软链接,则直到其真实文件路径,在startup.sh文件相同的目录下,肯定有一个catalina.sh,如果catalina.sh文件存在并且可执行,则执行catalina.sh start 命令。
既然将文件执行指向了catalina.sh,那我们就进入catalina.sh文件中,看做了哪些事情 。
cygwin=false
darwin=false
os400=false
hpux=false
# 显示系统内核名称,如果以HP-UX开头,则hpux为true
case "`uname`" in
CYGWIN*) cygwin=true;;
Darwin*) darwin=true;;
OS400*) os400=true;;
HP-UX*) hpux=true;;
esac
# resolve links - $0 may be a softlink
# 如果catalina.sh是软链接文件,找到其真实文件
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
# Get standard environment variables
# 获得catalina.sh文件所在目录
PRGDIR=`dirname "$PRG"`
# Only set CATALINA_HOME if not already set
# 如果CATALINA_HOME的长度为0,则设置catalina.sh文件所在目录为CATALINA_HOME
[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd`
# Copy CATALINA_BASE from CATALINA_HOME if not already set ,
#如果 CATALINA_BASE长度为0,则设置CATALINA_BASE为CATALINA_HOME
[ -z "$CATALINA_BASE" ] && CATALINA_BASE="$CATALINA_HOME"
# Ensure that any user defined CLASSPATH variables are not used on startup,
# but allow them to be specified in setenv.sh, in rare case when it is needed.
CLASSPATH=
# 如果$CATALINA_BASE/bin/setenv.sh 文件可读,则执行$CATALINA_BASE/bin/setenv.sh
if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then
. "$CATALINA_BASE/bin/setenv.sh"
# 否则$CATALINA_HOME/bin/setenv.sh为可读文件,则执行它
elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then
. "$CATALINA_HOME/bin/setenv.sh"
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$JRE_HOME" ] && JRE_HOME=`cygpath --unix "$JRE_HOME"`
[ -n "$CATALINA_HOME" ] && CATALINA_HOME=`cygpath --unix "$CATALINA_HOME"`
[ -n "$CATALINA_BASE" ] && CATALINA_BASE=`cygpath --unix "$CATALINA_BASE"`
[ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# Ensure that neither CATALINA_HOME nor CATALINA_BASE contains a colon
# as this is used as the separator in the classpath and Java provides no
# mechanism for escaping if the same character appears in the path.
case $CATALINA_HOME in
*:*) echo "Using CATALINA_HOME: $CATALINA_HOME";
echo "Unable to start as CATALINA_HOME contains a colon (:) character";
exit 1;
esac
case $CATALINA_BASE in
*:*) echo "Using CATALINA_BASE: $CATALINA_BASE";
echo "Unable to start as CATALINA_BASE contains a colon (:) character";
exit 1;
esac
#如果CATALINA_HOME或CATALINA_BASE路径中有冒号,则给出提示并退出
# For OS400
if $os400; then
# Set job priority to standard for interactive (interactive - 6) by using
# the interactive priority - 6, the helper threads that respond to requests
# will be running at the same priority as interactive jobs.
COMMAND='chgjob job('$JOBNAME') runpty(6)'
system $COMMAND
# Enable multi threading
export QIBM_MULTI_THREADED=Y
fi
# Get standard Java environment variables
if $os400; then
# -r 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
. "$CATALINA_HOME"/bin/setclasspath.sh
else
# 调用setclasspath脚本,设置JAVA_HOME,JRE_HOME,JAVA_PATH,JAVA_ENDORSED_DIRS,_RUNJAVA等
if [ -r "$CATALINA_HOME"/bin/setclasspath.sh ]; then
. "$CATALINA_HOME"/bin/setclasspath.sh
else
如果没有setclasspath.sh,则打印提示
echo "Cannot find $CATALINA_HOME/bin/setclasspath.sh"
echo "This file is needed to run this program"
exit 1
fi
fi
# Add on extra jar files to CLASSPATH
if [ ! -z "$CLASSPATH" ] ; then
CLASSPATH="$CLASSPATH":
fi
# 重新设置classpath
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar
# 设置日志输出文件
if [ -z "$CATALINA_OUT" ] ; then
CATALINA_OUT="$CATALINA_BASE"/logs/catalina.out
fi
# 设置临时目录
if [ -z "$CATALINA_TMPDIR" ] ; then
# Define the java.io.tmpdir to use for Catalina
CATALINA_TMPDIR="$CATALINA_BASE"/temp
fi
# Add tomcat-juli.jar to classpath
# tomcat-juli.jar can be over-ridden per instance
# CATALINA_BASE/bin/tomcat-juli.jar可读,则加入到classpath中
if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar
else
CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
fi
# Bugzilla 37848: When no TTY is available, don't output to console
have_tty=0
if [ -t 0 ]; then
have_tty=1
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
JAVA_HOME=`cygpath --absolute --windows "$JAVA_HOME"`
JRE_HOME=`cygpath --absolute --windows "$JRE_HOME"`
CATALINA_HOME=`cygpath --absolute --windows "$CATALINA_HOME"`
CATALINA_BASE=`cygpath --absolute --windows "$CATALINA_BASE"`
CATALINA_TMPDIR=`cygpath --absolute --windows "$CATALINA_TMPDIR"`
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$JAVA_ENDORSED_DIRS" ] && JAVA_ENDORSED_DIRS=`cygpath --path --windows "$JAVA_ENDORSED_DIRS"`
fi
if [ -z "$JSSE_OPTS" ] ; then
JSSE_OPTS="-Djdk.tls.ephemeralDHKeySize=2048"
fi
# 设置JAVA_OPTS
JAVA_OPTS="$JAVA_OPTS $JSSE_OPTS"
# Set juli LogManager config file if it is present and an override has not been issued
# 如果日志文件没有配置,并且 CATALINA_BASE/conf/logging.properties可读,则
# 设置java.util.logging.config.file为CATALINA_BASE/conf/logging.properties
if [ -z "$LOGGING_CONFIG" ]; then
if [ -r "$CATALINA_BASE"/conf/logging.properties ]; then
LOGGING_CONFIG="-Djava.util.logging.config.file=$CATALINA_BASE/conf/logging.properties"
else
# Bugzilla 45585
LOGGING_CONFIG="-Dnop"
fi
fi
# 设置日志管理器
if [ -z "$LOGGING_MANAGER" ]; then
LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager"
fi
# Set UMASK unless it has been overridden
# 掩码默认为0027 ,掩码和chmod是相反的
# umask 命令允许你设定文件创建时的缺省模式,对应每一类用户(文件属主、同组用户、其他用户)存在一个
# 相应的umask值中的数字。对于文件来说,这一数字的最 大值分别是6。系统不允
# 许你在创建一个文本文件时就赋予它执行权限,必须在创建后用chmod命令增加这
# 一权限。目录则允许设置执行权限,这样针对目录来 说,umask中各个数字最大可以到7。
# 该命令的一般形式为:
# umask nnn
# 其中nnn为umask置000-777。
# 如 其中umask值为002:
# 文件的最大权限 rwx rwx rwx (777)
# umask值为002 --- --- -w-
# 目录权限 rwx rwx r-x (775) 这就是目录创建缺省权限
# 文件权限 rw- rw- r-- (664) 这就是文件创建缺省权限
if [ -z "$UMASK" ]; then
UMASK="0027"
fi
umask $UMASK
# Java 9 no longer supports the java.endorsed.dirs
# system property. Only try to use it if
# JAVA_ENDORSED_DIRS was explicitly set
# or CATALINA_HOME/endorsed exists.
ENDORSED_PROP=ignore.endorsed.dirs
if [ -n "$JAVA_ENDORSED_DIRS" ]; then
ENDORSED_PROP=java.endorsed.dirs
fi
if [ -d "$CATALINA_HOME/endorsed" ]; then
ENDORSED_PROP=java.endorsed.dirs
fi
# Make the umask available when using the org.apache.catalina.security.SecurityListener
JAVA_OPTS="$JAVA_OPTS -Dorg.apache.catalina.security.SecurityListener.UMASK=`umask`"
Tomcat 是否后台启动
if [ -z "$USE_NOHUP" ]; then
if $hpux; then
USE_NOHUP="true"
else
USE_NOHUP="false"
fi
fi
unset _NOHUP
if [ "$USE_NOHUP" = "true" ]; then
_NOHUP="nohup"
fi
# Add the JAVA 9 specific start-up parameters required by Tomcat
JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.base/java.lang=ALL-UNNAMED"
JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.base/java.io=ALL-UNNAMED"
JDK_JAVA_OPTIONS="$JDK_JAVA_OPTIONS --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED"
export JDK_JAVA_OPTIONS
# ----- Execute The Requested Command -----------------------------------------
# Bugzilla 37848: only output this if we have a TTY
if [ $have_tty -eq 1 ]; then
echo "Using CATALINA_BASE: $CATALINA_BASE"
echo "Using CATALINA_HOME: $CATALINA_HOME"
echo "Using CATALINA_TMPDIR: $CATALINA_TMPDIR"
if [ "$1" = "debug" ] ; then
echo "Using JAVA_HOME: $JAVA_HOME"
else
echo "Using JRE_HOME: $JRE_HOME"
fi
echo "Using CLASSPATH: $CLASSPATH"
if [ ! -z "$CATALINA_PID" ]; then
echo "Using CATALINA_PID: $CATALINA_PID"
fi
fi
if [ "$1" = "jpda" ] ; then
if [ -z "$JPDA_TRANSPORT" ]; then
JPDA_TRANSPORT="dt_socket"
fi
if [ -z "$JPDA_ADDRESS" ]; then
JPDA_ADDRESS="localhost:8000"
fi
if [ -z "$JPDA_SUSPEND" ]; then
JPDA_SUSPEND="n"
fi
if [ -z "$JPDA_OPTS" ]; then
JPDA_OPTS="-agentlib:jdwp=transport=$JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND"
fi
CATALINA_OPTS="$JPDA_OPTS $CATALINA_OPTS"
shift
fi
if [ "$1" = "debug" ] ; then
if $os400; then
echo "Debug command not available on OS400"
exit 1
else
shift
if [ "$1" = "-security" ] ; then
if [ $have_tty -eq 1 ]; then
echo "Using Security Manager"
fi
shift
exec "$_RUNJDB" "$LOGGING_CONFIG" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="$JAVA_ENDORSED_DIRS" \
-classpath "$CLASSPATH" \
-sourcepath "$CATALINA_HOME"/../../java \
-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
else
exec "$_RUNJDB" "$LOGGING_CONFIG" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="$JAVA_ENDORSED_DIRS" \
-classpath "$CLASSPATH" \
-sourcepath "$CATALINA_HOME"/../../java \
-Dcatalina.base="$CATALINA_BASE" \
-Dcatalina.home="$CATALINA_HOME" \
-Djava.io.tmpdir="$CATALINA_TMPDIR" \
org.apache.catalina.startup.Bootstrap "$@" start
fi
fi
elif [ "$1" = "run" ]; then
shift
if [ "$1" = "-security" ] ; then
if [ $have_tty -eq 1 ]; then
echo "Using Security Manager"
fi
shift
eval exec "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="\"$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
else
eval exec "\"$_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
fi
# 如果catalina.sh后面的第一个参数是start ,则进入下面代码
elif [ "$1" = "start" ] ; then
if [ ! -z "$CATALINA_PID" ]; then #
if [ -f "$CATALINA_PID" ]; then
if [ -s "$CATALINA_PID" ]; then
# 如果CATALINA_PID不为空,并且CATALINA_PID是一个常规文件,并且文件长度不为0
echo "Existing PID file found during start."
# 如果CATALINA_PID文件可读
if [ -r "$CATALINA_PID" ]; then
# 读取CATALINA_PID文件的内容
PID=`cat "$CATALINA_PID"`
ps -p $PID >/dev/null 2>&1
# 如果在启动过程中发现pid文件存在,并且pid对应的进程还是启动的,
# 则打印出pid进行对应的相关信息,提示用户去删除pid相关文件
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
# 如果CATALINA_PID文件中记录的pid对应的进程不存在了,则直接删除CATALINA_PID文件
echo "Removing/clearing stale PID file."
rm -f "$CATALINA_PID" >/dev/null 2>&1
# $?上一个命令的返回值,1 表示执行失败,0 表示执行成功
if [ $? != 0 ]; then
# 如果删除文件失败,则看CATALINA_PID文件是否可写,如果可写,则清空掉CATALINA_PID文件的内容
if [ -w "$CATALINA_PID" ]; then
cat /dev/null > "$CATALINA_PID"
else
# 如果CATALINA_PID既不能删除,也不能写,则提示下面内容
echo "Unable to remove or clear stale PID file. Start aborted."
exit 1
fi
fi
fi
else
# 如果CATALINA_PID文件不能读,则提示下面内容
echo "Unable to read PID file. Start aborted."
exit 1
fi
else
# CATALINA_PID 文件长度为0 ,则直接删除掉CATALINA_PID文件即可
rm -f "$CATALINA_PID" >/dev/null 2>&1
if [ $? != 0 ]; then
# 如果CATALINA_PID文件删除失败,并且还不可写,则提示下面内容
if [ ! -w "$CATALINA_PID" ]; then
echo "Unable to remove or write to empty PID file. Start aborted."
exit 1
fi
fi
fi
fi
fi
# catalina.sh start aa bb cc dd 的第一个参数是start ,因此调用shift 指令,参数后移
# catalina.sh aa bb cc dd ,如果参数中带有-security
shift
touch "$CATALINA_OUT"
if [ "$1" = "-security" ] ; then
if [ $have_tty -eq 1 ]; then
echo "Using Security Manager"
fi
# 移除掉-security参数
shift
eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
-D$ENDORSED_PROP="\"$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\"" \
# 将剩下的参数作为Bootstrap类的启动参数
org.apache.catalina.startup.Bootstrap "$@" start \
>> "$CATALINA_OUT" 2>&1 "&"
else
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 "&"
fi
# 将上述执行结果pid 存入到CATALINA_PID文件中
if [ ! -z "$CATALINA_PID" ]; then
echo $! > "$CATALINA_PID"
fi
# 打印tomcat 启动成功
echo "Tomcat started."
# 如果执行的是 catalina.sh stop 命令
elif [ "$1" = "stop" ] ; then
# 将stop命令从参数中移除掉
shift
# 默认stop 尝试5秒
SLEEP=5
if [ ! -z "$1" ]; then
echo $1 | grep "[^0-9]" >/dev/null 2>&1
# 如果stop参数后是一个数字 ,则将该值设置到SLEEP 中,并且将参数下标右移1
if [ $? -gt 0 ]; then
SLEEP=$1
shift
fi
fi
# 如果输入的命令是 catalina.sh stop 5 -force 或 catalina.sh stop -force
FORCE=0
if [ "$1" = "-force" ]; then
# 参数下标右移,并且FORCE = 1
shift
FORCE=1
fi
if [ ! -z "$CATALINA_PID" ]; then
if [ -f "$CATALINA_PID" ]; then
if [ -s "$CATALINA_PID" ]; then
# 如果 CATALINA_PID 文件size大于0,并且CATALINA_PID为常规文件,
# kill -0 pid 不发送任何信号,但是系统会进行错误检查。所以经常用来检查一个进程是否存在,存在返回0;不存在返回1
# 如果pid 不存在,或者没有权限去停止,则给出下面提示
kill -0 `cat "$CATALINA_PID"` >/dev/null 2>&1
if [ $? -gt 0 ]; then
echo "PID file found but either no matching process was found or the current user does not have permission to stop the process. Stop aborted."
exit 1
fi
else
echo "PID file is empty and has been ignored."
fi
# 如果CATALINA_PID是一个特殊文件,给出下面提示
else
echo "\$CATALINA_PID was set but the specified file does not exist. Is Tomcat running? Stop aborted."
exit 1
fi
fi
# 向Bootstrap类发送stop 命令
eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_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 "$@" stop
# stop failed. Shutdown port disabled? Try a normal kill.
if [ $? != 0 ]; then
if [ ! -z "$CATALINA_PID" ]; then
# 如果停止失败,但CATALINA_PID文件存在,则给出下面提示
echo "The stop command failed. Attempting to signal the process to stop through OS signal."
# 调用kill -15 温柔的杀死进程
kill -15 `cat "$CATALINA_PID"` >/dev/null 2>&1
fi
fi
if [ ! -z "$CATALINA_PID" ]; then
if [ -f "$CATALINA_PID" ]; then
while [ $SLEEP -ge 0 ]; do
# 如果睡眠时间大于0,并且CATALINA_PID的文件大小大于零,并且是一个常规文件
kill -0 `cat "$CATALINA_PID"` >/dev/null 2>&1
# 如果$? > 0,则 CATALINA_PID文件内的进程id ,目前不存在
if [ $? -gt 0 ]; then
# 因为pid进程已经不存在了,尝试删除CATALINA_PID文件
rm -f "$CATALINA_PID" >/dev/null 2>&1
if [ $? != 0 ]; then
# 如果删除CATALINA_PID文件失败,而CATALINA_PID文件有写权限,则
if [ -w "$CATALINA_PID" ]; then
# 将空字符串写入到CATALINA_PID文件中
cat /dev/null > "$CATALINA_PID"
# If Tomcat has stopped don't try and force a stop with an empty PID file
FORCE=0
else
# 如果CATALINA_PID文件是不可写的,则给出下面提示
echo "The PID file could not be removed or cleared."
fi
fi
# 只要pid进程在系统中不存在,则一定会打印出tomcat 已经停止,并且退出循环
echo "Tomcat stopped."
break
fi
# 如果睡眠时间大于0 ,则先睡眠一秒
if [ $SLEEP -gt 0 ]; then
sleep 1
fi
# SLEEP默认值是5,如果SLEEP仍然大于0
if [ $SLEEP -eq 0 ]; then
echo "Tomcat did not stop in time."
if [ $FORCE -eq 0 ]; then
echo "PID file was not removed."
fi
echo "To aid diagnostics a thread dump has been written to standard out."
# kill -3可以打印进程各个线程的堆栈信息,kill -3 pid 后文件的保存路径为:/proc/${pid}/cwd,文件名为:antBuilderOutput.log
# 如果是让进程自己执行退出离场程序就使用 kill 命令,这样进程可以自己执行一些清理动作然后退出。如果进程卡死,你需要记录当时的事故现场,
# 那么应该用 kill -3 来记录事故现场的信息然后退出。如果你什么也不需要,就是要杀死一个进程那么就是用 kill -9 命令,很暴力的杀死它。
kill -3 `cat "$CATALINA_PID"`
fi
# SLEEP 变量减1
SLEEP=`expr $SLEEP - 1 `
done
fi
fi
KILL_SLEEP_INTERVAL=5
if [ $FORCE -eq 1 ]; then # 如果命令中有-force
# 如果CATALINA_PID所在文件名为空,提出下面内容
if [ -z "$CATALINA_PID" ]; then
echo "Kill failed: \$CATALINA_PID not set"
else
# 如果CATALINA_PID文件是一个常规文件
if [ -f "$CATALINA_PID" ]; then
# 获取pid
PID=`cat "$CATALINA_PID"`
echo "Killing Tomcat with the PID: $PID"
# 使用kill -9 杀死pid进程
# kill -9代表的信号是SIGKILL,表示进程被终止,需要立即退出,强制杀死该进程,这个信号不能被捕获也不能被忽略
kill -9 $PID
while [ $KILL_SLEEP_INTERVAL -ge 0 ]; do
# 查看pid进程是否被杀死了,如果被杀死,则执行结果 $? == 0 ,否则 $? 大于0
kill -0 `cat "$CATALINA_PID"` >/dev/null 2>&1
if [ $? -gt 0 ]; then
# 如果pid进程已经被杀死了,则删除CATALINA_PID文件
rm -f "$CATALINA_PID" >/dev/null 2>&1
if [ $? != 0 ]; then
# 如果删除失败,并且CATALINA_PID文件是可写的,将空字符串写入到CATALINA_PID文件中
if [ -w "$CATALINA_PID" ]; then
cat /dev/null > "$CATALINA_PID"
else
# 如果CATALINA_PID文件没有写权限,则提示下面内容
echo "The PID file could not be removed."
fi
fi
# 如果被杀死了,则提示tomcat 进程已经被杀死
echo "The Tomcat process has been killed."
break
fi
# 如果KILL_SLEEP_INTERVAL变量仍然大于0,则睡眠1秒
if [ $KILL_SLEEP_INTERVAL -gt 0 ]; then
sleep 1
fi
# KILL_SLEEP_INTERVAL 变量减1 ,如果KILL_SLEEP_INTERVAL大于0,则继续循环
# 按道理kill -9 命令是能立即杀死进程的,但为什么这里要做一个循环呢?即使是立即杀死,在pid进程彻底消亡过程中
# 也是需要时间的,因此这里等了5秒钟,目的就是为了保证CATALINA_PID文件的内容被清除掉,方便下次启动
KILL_SLEEP_INTERVAL=`expr $KILL_SLEEP_INTERVAL - 1 `
done
# 如果等了5秒,进程还没有消亡,提示下面内容,需要命令调用者自己去做处理了
if [ $KILL_SLEEP_INTERVAL -lt 0 ]; then
echo "Tomcat has not been killed completely yet. The process might be waiting on some system call or might be UNINTERRUPTIBLE."
fi
fi
fi
fi
elif [ "$1" = "configtest" ] ; then
eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_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 configtest
result=$?
if [ $result -ne 0 ]; then
echo "Configuration error detected!"
fi
exit $result
elif [ "$1" = "version" ] ; then
"$_RUNJAVA" \
-classpath "$CATALINA_HOME/lib/catalina.jar" \
org.apache.catalina.util.ServerInfo
else
echo "Usage: catalina.sh ( commands ... )"
echo "commands:"
if $os400; then
echo " debug Start Catalina in a debugger (not available on OS400)"
echo " debug -security Debug Catalina with a security manager (not available on OS400)"
else
echo " debug Start Catalina in a debugger"
echo " debug -security Debug Catalina with a security manager"
fi
echo " jpda start Start Catalina under JPDA debugger"
echo " run Start Catalina in the current window"
echo " run -security Start in the current window with security manager"
echo " start Start Catalina in a separate window"
echo " start -security Start in a separate window with security manager"
echo " stop Stop Catalina, waiting up to 5 seconds for the process to end"
echo " stop n Stop Catalina, waiting up to n seconds for the process to end"
echo " stop -force Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running"
echo " stop n -force Stop Catalina, wait up to n seconds and then use kill -KILL if still running"
echo " configtest Run a basic syntax check on server.xml - check exit code for result"
echo " version What version of tomcat are you running?"
echo "Note: Waiting for the process to end and use of the -force option require that \$CATALINA_PID is defined"
exit 1
fi
linux ps命令详解(unix风格)
我们分析了Tomcat 的start 和 stop 脚本,run命令和start大同小异,只是start命令比run 命令多考虑了CATALINA_PID文件的存在性以及文件内容pid在当前操作系统中是否存活。 但最终都是启动org.apache.catalina.startup.Bootstrap 类,并将命令行中剩余的参数作为Bootstrap类的main方法的参数传入。 当然在启动过程中考虑到-security 参数的使用,如果使用了-security参数,则在启动过程中环境变量中加入
-Djava.security.manager
-Djava.security.policy==“”$CATALINA_BASE/conf/catalina.policy""
两个参数 。
接下来,看stop 命令做的事情 , 先向org.apache.catalina.startup.Bootstrap类中传入stop 命令,如果向Bootstrap类中传入stop 参数后不生效,调用kill -15 这是告诉进程你需要被关闭,请自行停止运行并退出,进程可以清理缓存自行结束,也可以拒绝结束。之后偿试5秒钟,如果进程已经关闭了, 则清除 CATALINA_PID文件的内容,如果进程仍然没有关闭,并且在stop 命令中加了-force ,则用kill -9 命令来杀死进程,并且再次偿试5秒钟的时间去清除CATALINA_PID文件内容,如果5秒后,进程仍然没有被关闭,则提示Tomcat has not been killed completely yet. The process might be waiting on some system call or might be UNINTERRUPTIBLE.
我相信,读者读到这里,对tomcat的启动和停止脚本已经有了深入理解,读好的代码,如读唐诗宋词,意境深远,意味深长。
既然将所有的矛头都指向了Bootstrap类,那我们进入 org.apache.catalina.startup.Bootstrap类的main方法分析 。
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init(); // catalinaaemon
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to prevent
// a range of class not found exceptions.
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); // 解析server.xml,初始化Catalina
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} 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);
}
}
我们先来看Tomcat初始化做了哪些事情。
/** * Initialize daemon. * 主要初始化类加载器,在Tomcat的设计中,使用了很多自定义的类加载器,包括Tomcat自己本身的类会由CommonClassLoader来加载,每个wabapp由特定的类加载器来加载 */ public void init() throws Exception { // Set Catalina path // catalina.home表示安装目录 // catalina.base表示工作目录 setCatalinaHome(); setCatalinaBase(); // 初始化commonLoader、catalinaLoader、sharedLoader // 其中catalinaLoader、sharedLoader默认其实就是commonLoader initClassLoaders(); // 设置线程的所使用的类加载器,默认情况下就是commonLoader Thread.currentThread().setContextClassLoader(catalinaLoader); // 如果开启了SecurityManager,那么则要提前加载一些类 SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method // 加载Catalina类,并生成instance 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 // 设置Catalina实例的父级类加载器为sharedLoader(默认情况下就是commonLoader) 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; }
我们看加粗代码的意图Thread.currentThread().setContextClassLoader(catalinaLoader);
前面提到的Tomcat 会创建Common类加载器,Catalina类加载器和共享类加载器三个类加载器供自己使用, 这三个其实是同一个类加载器对象 , Tomcat 在创建类加载器后马上将其设置成当前类加载器,即Thread.currentThread().setContextClassLoader(CatalinaLoader) , 这里主要是为了避免后面加载类时加载不成功,下面将举一个典型的例子说明如何利用URLClassLoader 加载指定Jar 包,并且解析由此引起的加载失败问题。
首先,定义一个提供服务接口,并且打包成TestInterface.jar
public interface TestInterface {
public String display();
}
其次创建一个名为TestClassLoader 的类,它的实现TestInterface.jar 包里面的TestInterface 接口,路径为com.test ,该类包含一个display方法,将这个类编译并打包成test.jar 包,放在D 盘目录下。
public class TestClassLoader implements TestInterFace{
public String display(){
return “I can load this class and execute the method .”;
}
}
最后利用URLClassLoader 加载并运行TestClassLoader 类的display方法,创建一个测试类,如下图所示 。
public class Test{
public static void main (String [] args ) throw Exception{
URL url = new URL(“file:D/test.jar”);
URLClassLoader myClassLoader = new URL(new URL[]{url});
Class myClass = myClassLoader.loadClass(“com.test.TestClassLoader”);
TestInterface testClassLoader = (TestInterface)myClass.newInstance();
System.out.println(testclassLoader.display());
}
}
测试类的main方法中首先用URLClassLoader 指定加载test.jar ,然后再将com.test.TestClassLoader 类加载到内存中,最后用newInstance 方法生成一个TestClassLoader 实例,即可调用它的display方法 。 运行这个测试类,能够达到预期的效果,输出 “I can load this class and execute the method” ,语句 ,看起来一切都那么顺其自然,但当你把TestInterface.jar 包移植到Web 应用中时,竟然抛出了java.lang.ClassNotFoundException:com.test.TestInterface异常,报错的位置正是代码中加粗的语句 , 怎么会抛出找不到的这个类异常呢?要明白为什么会报这样的错,需要搞清楚这几点 。
也解了以上三点,再对前面的加载时抛出找不到类异常进行分析 。
针对上面的错误 , 有两种解决方法 。
即因为两个类加载器被加载而导致找不到类,那么最简单的解决方法就是使这两个类统一由一个类加载器来加载,即在加载testclassloader.jar 时用当前线程类加载器加载,只须稍微的修改代码。
URLClassLoader myClassLoader = new URLClassLoader(new URL[]{} ,Thread.currentThread().getContextClassLoader());
重点是加粗部分,即在创建URLClassLoader对象时将当前类加载器作为父类的加载器传入,WebAPP 当前线程类加载器是WebAppClassLoader ,那么当加载testclassloader.jar 时,将优先交给WebAppClassLoader 加载,这样就保证了两个类在同一个类加载器中,不会再报找不到异常类。
讨论了这么多,回归到Tomcat中的Thread.currentThread().setContextClassLoader(catalinaLoader),上面讨论了典型的类加载器错误在Tomcat 中同样存在 ,因此Tomcat 正是通过设置了线程上下文类加载器来解决的,在Tomcat 中类加载器同样存在以下三种情况。
如图13.5 所示 ,先看默认的情况,ContextClassLoader 被赋为系统类加载器,系统类加载器看不见Common 类加载器加载的类,即如果在过程中引用就会报找不到类的错误,所以启动Tomcat 的过程中肯定会报错,接着看改进后的情况,把ContextClassLoader 赋为Common 类加载器,此时,Tomcat 在启动过程中如果用到了$CATALINA_BASE/lib 或$CATALINA_HOME/lib中的类,就不会报错了,同时,它也能看到系统类加载器及其父类加载器所有的加载类,简单的说,解决方法就是把Common 类加载器设置为线程上下文类加载器。
为了避免类加载错误,应该尽早设置线程上下文类加载器,所以在Tomcat 中启动初始化就马上设置,即初始化时马上通过Thread.currentThread().setContextClassLoader(catalinaLoader)设置线程上下文类加载器,此后线程运行默认由Common类加载器载入类。

接着看设置catalinaHome帮我们做了哪些事情。
private void setCatalinaHome() { // 如果catalina.home已经设置好了,直接返回 if (System.getProperty(Globals.CATALINA_HOME_PROP) != null) return; File bootstrapJar = new File(System.getProperty("user.dir"), "bootstrap.jar"); if (bootstrapJar.exists()) { //如果工作目录下存在bootstrap.jar文件 try { // 如果user.dir = /Users/quyixiao/gitlab/tomcat,并且/Users/quyixiao/gitlab/tomcat/bootstrap.jar文件存在 // 此时设置 catalina.home为 /Users/quyixiao/gitlab System.setProperty(Globals.CATALINA_HOME_PROP, (new File(System.getProperty("user.dir"), "..")) .getCanonicalPath()); } catch (Exception e) { // 如果抛出异常,则设置 catalina.home 为user.dir=/Users/quyixiao/gitlab/tomcat System.setProperty(Globals.CATALINA_HOME_PROP, System.getProperty("user.dir")); } } else { //设置catalina.home 为user.dir 工作目录 System.setProperty(Globals.CATALINA_HOME_PROP, System.getProperty("user.dir")); } }
其实在设置catalina.home的原理也很简单,首先看catalina.home是否已经设置,如果没有设置,则看工作目录下是否有bootstrap.jar文件,如果有,则设置工作目录的上一级目录为catalina.home,否则,默认设置 user.dir 为catalina.home。catalina.base 的设置原理和catalina.home原理相似,如果catalina.home存在,则设置catalina.base 为catalina.home,如果不存在,则默认设置catalina.base 为工作目录。
接下来,我们先来了解一下Java的类加载器。
Java 的设计初衷主要是面向嵌入式领域,对于自定义的一些类, 考虑使用按需加载原则,即在程序使用时才加载类, 节省内存消耗,这时即可通过类加载器来动态加载 。
如果平时只做Web 开发,那应该很少会跟类加载器打交道,但如果想深入学习Tomcat 的构架,那它是必不可少的, 所谓类加载器,就是用于加载Java 类到Java 虚拟机中的组件,它负责读取Java 字节码,并转换成java.lang.Class类的一个实例,使字节码.class 文件得以运行,一般类加载器负责根据一个指定的类找到对应的字节码,然后根据这些字节码定义一个Java 类。 另外,它还可以加载资源,包括图像文件和配置文件 。
类加载器在实际使用中给我们带来的好处是,它可以使Java 类动态的加载到JVM 中并运行,即可以在程序运行时再加载类,提供了很灵活的动态加载方式,例如 Applect ,从远程服务器下载字节码到客户端再动态的加载到JVM 中便可以运行。
在Java体系中,可以将系统分为三种类型加载器。
假如想自己写一个类加载器,那么只需要继承java.lang.ClassLoader 类即可, 可以用图 13.1来清楚的表示出各种类加载器的关系,启动类加载器是最根本的类加载器,其不存在父类加载器,扩展类加载器由启动类加载器加载,所以它的父类加载器是启动类加载器,应用程序类加载器也由启动类加载器加载,但它的父类加载器指向扩展类加载器,而其他用户自定义的类加载器由应用程序类加载器加载。

由此可以看出,越重要的类加载器就越早被JVM 载入, 这是考虑到安全性问题,因为先被加载的类加载器会充当一下个类加载器的父类加载器,在双亲委派模型机制下,就能确保安全性,双亲委派模型会在类加载器加载类时首先委托给父类加载器加载,除非父类加载器不能加载才自己加载 。
这种模型要求,除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器,假如有一个类要加载进来,一个类加载器并不会马上尝试自己将其加载,而是委派给父类加载器加载 , 父类加载器收到后又尝试委派给其父类加载器加载 ,以此类推,直到委派给启动类加载器加载 ,这样一层一层的往上委派,只有当父类加载器反馈给自己没法完成这个类加载时,子加载器才会尝试自己加载,通过这个机制,保证了Java 应用所使用的都是同一个版本的Java 核心库的类, 同时这个机制也保证了安全性,试想,如果应用程序类加载器想要加载一个有破坏性的java.lang.System类,双亲委派模型会一层一层的向上委派,最终委派给启动类加载器,而启动类加载器检查到缓存中已经有了这个类加载器,并不会再加载这个有破坏性的System类。
另外,类加载器还拥有全盘负责机制,即当一个类加载器加载一个类时,这个类所依赖的,引用的其他所有的类都由这个类加载器加载,除非在程序中显示的指定另外一个类加载器加载 。
在Java 中,我们用完全匹配的类来标识一个类,即用包名和类名,而在JVM 中,一个类由完全匹配类名和一个类加载器实例ID 作为唯一的标识,也就是说,同一个虚拟机可以有两个包名 , 类名相同的类,只要它们由两个不同的类加载器加载,当我们在Java 中说两个类是否相等时,必须在针对同一个类加载器加载的前提下才有意义,否则,就算是同样的字节码由不同的类加载器加载,这两个类也不是相等的,这种特征为我们提供了隔离机制,在Tomcat 服务器中它是非常有用的。
了解了JVM 的类加载器的各种机制后,看看一个类是怎样被类加载器载入进来的,如图1.32 所示 ,要加载一个类,类加载器先判断已经加载过(加载过的类会缓存在内存中),如果缓存中存在此类,则直接返回这个类,否则,获取父类的加载器,如果父类加载器为null, 则由启动类加载器载入并返回Class,如果父类加载器不为null,则由父类加载器载入,载入成功就返回Class ,载入失败则根据类路径查找Class 文件,找到就加载此Class文件并返回Class,找不到就抛出ClassNotFundException 。

类加载器属于JVM 级别的设计,我们很多的时候基本不会与它打交道,假如你想深入了解Tomcat 内核或设计开发的构架和中间伯,那么你必须熟悉加载器相关的机制,在现实的设计中,根据实际情况利用类加载器可以提供类库在隔离及共享,保证软件不同级别的逻辑分割程序不会互相影响,提供更好的安全性。
一般的场景中使用Java 默认的类加载器即可,但有时为了达到某种目的,又不得不实现自己的类加载器,例如为了使类库互相隔离,为了实现热部署重加载功能,这时就需要自己定义类加载器,每个类加载器加载各自的资源,以此达到资源隔离的效果,在对资源的加载上可以沿用双亲委派机制,也可以打破双亲委派机制 。
public class Test {
public Test() {
System.out.println(this.getClass().getClassLoader().toString());
}
public static void main(String[] args) {
System.out.println("===============");
}
}
定义一个TomcatClassLoader类(它继承ClassLoader),重写了findClass方法,此方法要做的事情就是读取Test.class 字节流并传入父类的defineClass方法,然后,就可以通过自定义类加载器TomcatClassLoader 对Test.class 进行加载了,完成加载后输出 “TomcatLoader”。
public class TomcatClassLoader extends ClassLoader {
private String name;
public TomcatClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
public String toString() {
return this.name;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
byte [] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(new File("/Users/quyixiao/github/pitpat-server/pitpat-admin/target/classes/com/test/xxx/Test.class"));
int c = 0 ;
while ( -1 != (c = is.read())){
baos.write(c);
}
data = baos.toByteArray();
}catch (Exception e ){
e.printStackTrace();
}finally {
try {
is.close();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return this.defineClass(name,data,0 ,data.length);
}
public static void main(String[] args) {
TomcatClassLoader loader = new TomcatClassLoader(TomcatClassLoader.class.getClassLoader() , "TomcatClassLoader");
Class clazz ;
try {
clazz = loader.loadClass("com.test.xxx.Test");
Object object =clazz.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
}
2) 打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法,下面给出一个例子。
定义Test类。
public class Test {
public Test() {
System.out.println(this.getClass().getClassLoader().toString());
}
public static void main(String[] args) {
System.out.println("===============");
}
}
重新定义一个继承ClassLoader 的TomcatClassLoaderN 类,这个类与前面的TomcatClassLoader 类很相似,但它除了重写findClass 方法外,还重写了loadClass方法,默认的loadClass 方法实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时,才由自己加载器去加载,这里为了破坏双亲委派机制必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败才会由自己加载,它并没有优先交给父类加载器,这就打破了双亲委派机制 。
public class TomcatClassLoaderN extends ClassLoader {
private String name;
public TomcatClassLoaderN(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
@Override
public String toString() {
return this.name;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
ClassLoader system = getSystemClassLoader();
try {
clazz = system.loadClass(name);
} catch (Exception e) {
e.printStackTrace();
}
if (clazz != null) {
return clazz;
}
clazz = findClass(name);
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(new File("/Users/quyixiao/github/pitpat-server/pitpat-admin/target/classes/com/test/xxx/Test.class"));
int c = 0;
while (-1 != (c = is.read())) {
baos.write(c);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
baos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return this.defineClass(name, data, 0, data.length);
}
public static void main(String[] args) {
TomcatClassLoaderN loader = new TomcatClassLoaderN(TomcatClassLoaderN.class.getClassLoader(), "TomcatLoaderN");
Class clazz;
try {
clazz = loader.loadClass("com.luban.classloadtest.Test");
Object o = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果输出 :
sun.misc.Launcher$AppClassLoader@18b4aac2
Tomcat 拥有不同的自定义类加载器,以实现各种资源库的控制 , 一般来说,Tomcat 主要用类加载器解决以下4个问题。
对于以上的几个问题,如果单独使用一个类加载器明显是达不到效果的,必须根据具体的情况使用若干个自定义加载器。
下面来看看Tomcat 的类加载器是怎样定义的,如图13.3 所示,启动类加载器,扩展类加载器,应用程序类加载器这三个类加载器属于JDK 级别的加载器。它们是唯一的,我们一般不会对其做任何更改,接下来,则是Tomcat的类加载器,在Tomcat 中,最重要的一个类加载器是Common类加载器,它的父类加载器是应用程序类加载器,负责加载$CATALINA_BASE/lib ,$CATALINA_HOME/lib两个目录下的所有.class 文件和.jar文件,而下面的虚线框的两个类加载器主要用在Tomcat5 版本中这两个类加载器实例默认与常见的类加载器实例不同,Common 类加载器是它们的父类加载器,而在Tomcat 7 版本中,这两个实例变量也存在,是catalina.properties配置文件没有对server.loader 和share.loader 两项进行配置,所以在程序里这两个类加载器实例就被赋值为Common 类加载器实例,即一个Tomcat 7 版本的实例其实就只有Common 类加载器实例。

首先创建一个Common 类加载器,再把Common 类加载器作为参数传进createClassLoader方法里,这个方法里面根据catalina.properties中的server.loader 和share.loader 属性是否为空判断是否另外创建新的类加载器,如果属性为空,则把常见的类加载器直接赋值给Catalina类加载器和共享类加载器,如果默认配置满足不了你的需求,可以通过修改catalina.properties 配置文件满足需求 。
从图13.3 中的WebApp ClassLoader 来看,就大概知道它主要的加载Web 应用程序,它的父类加载器是Common 类加载器 ,Tomcat 中一般会有多个WebApp 类加载器实例,每个类加载器负责加载一个Web 程序 。
对照这样的一个类加载器结构,看看上面需要解决的问题是否解决 。由于每个Web 应用项目都有自己的WebApp 类加载器,很多的使用多个 Web 应用项目都有自己的WebApp 类加载器,很好的使用了Web 应用程序之间的互相隔离且能通过创建新的WebApp 类加载器达到热部署。这种类加载器的结构能使有效的Tomcat 不受Web 应用程序影响 ,而Common类加载器在存在使用多个Web应用程序能够互相共享类库。
private void initClassLoaders() { try { // CommonClassLoader是一个公共的类加载器,默认加载${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar下的class // 虽然这个地方parent是null,实际上是appclassloader commonLoader = createClassLoader("common", null); // System.out.println("commonLoader的父类加载器===="+commonLoader.getParent()); if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. commonLoader=this.getClass().getClassLoader(); } // 下面这个两个类加载器默认情况下就是commonLoader catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } }
前面提到过Tomcat 会创建Common类加载器,Catalina类加载器和共享类加载器三个类加载器供自己使用,这三个其实是同一个类加载器对象,Tomcat 在创建类加载器后马上就将其设置成当前线程类加载器,即Thread.currentThread().setContenxtClassLoader(CatalinaLoader) , 这里主要是为了避免后面加载时加载不成功,下面列举一个典型的例子说明如何利用URLClassLoader 加载指定的Jar 包,并且解析由此引起的加载失败问题。
创建ClassLoader的时候,传入父classLoader 。 接着看代码 。
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { // 到catalina.properties找common.loader 或 server.loader 或 shared.loader 配置 String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; // 如果 common.loader 中配置了\${}或\${${}},则从环境变量中找到具体的属性值,并替换它 value = replace(value); List<Repository> repositories = new ArrayList<Repository>(); StringTokenizer tokenizer = new StringTokenizer(value, ","); while (tokenizer.hasMoreElements()) { String repository = tokenizer.nextToken().trim(); if (repository.length() == 0) { continue; } // Check for a JAR URL repository try { // 从URL上获取Jar包资源 @SuppressWarnings("unused") URL url = new URL(repository); repositories.add( new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith("*.jar")) { // 表示目录下所有的jar包资源 repository = repository.substring (0, repository.length() - "*.jar".length()); repositories.add( new Repository(repository, RepositoryType.GLOB)); } else if (repository.endsWith(".jar")) { // 表示目录下当个的jar包资源 repositories.add( new Repository(repository, RepositoryType.JAR)); } else { // 表示目录下所有资源,包括jar包、class文件、其他类型资源 repositories.add( new Repository(repository, RepositoryType.DIR)); } } // 基于类仓库类创建一个ClassLoader return ClassLoaderFactory.createClassLoader(repositories, parent); }
根据catalina.properties的配置内容,分为以*.jar ,.jar ,或者目录三种情况来处理。现在,下面我们看看这三种情况处理的代码 。
Java 虚拟机利用加载器将类载入内存的过程中,类加载器需要做很多事情,例如 ,读取字节数组,验证,解析初始化等。而Java提供了URLClassLoader 类能方便的将Jar ,Class或网络资源加载到内存中,Tomcat 中则用一个工厂类ClassLoaderFactory 把创建的类加载的细节进行封装,通过它可以很方便的创建自定义类加载器。
如图13.4 所示 , 利用createClassLoader 方法并传入资源路径和父类的加载器即可创建一个自定义类加载器,此类加载器负责加载传入的所有资源 。

ClassLoaderFactory 有个内部类Repository ,它就是表示资源的类,资源的类型用一个RepositoryType枚举表示 。
public static enum RepositoryType {DIR,GLOB,JAR ,URL };
每个类型代表的意思如下 :
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent) throws Exception { if (log.isDebugEnabled()) log.debug("Creating new class loader"); // Construct the "class path" for this class loader Set<URL> set = new LinkedHashSet<URL>(); if (repositories != null) { for (Repository repository : repositories) { if (repository.getType() == RepositoryType.URL) { URL url = buildClassLoaderUrl(repository.getLocation()); if (log.isDebugEnabled()) log.debug(" Including URL " + url); set.add(url); // 如果是一个目录 } else if (repository.getType() == RepositoryType.DIR) { File directory = new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.DIR)) { continue; } // 将目录构成一个URL URL url = buildClassLoaderUrl(directory); if (log.isDebugEnabled()) log.debug(" Including directory " + url); set.add(url); } else if (repository.getType() == RepositoryType.JAR) { //以 .jar 结尾 File file=new File(repository.getLocation()); file = file.getCanonicalFile(); if (!validateFile(file, RepositoryType.JAR)) { continue; } // 以 jar 包构成一个 URL URL url = buildClassLoaderUrl(file); if (log.isDebugEnabled()) log.debug(" Including jar file " + url); set.add(url); } else if (repository.getType() == RepositoryType.GLOB) { // 以*.jar 结尾 File directory=new File(repository.getLocation()); directory = directory.getCanonicalFile(); if (!validateFile(directory, RepositoryType.GLOB)) { continue; } if (log.isDebugEnabled()) log.debug(" Including directory glob " + directory.getAbsolutePath()); String filenames[] = directory.list(); if (filenames == null) { continue; } // 遍历目录下的所有文件 for (int j = 0; j < filenames.length; j++) { String filename = filenames[j].toLowerCase(Locale.ENGLISH); if (!filename.endsWith(".jar")) continue; File file = new File(directory, filenames[j]); file = file.getCanonicalFile(); // 如果不以.jar文件结尾,过滤掉 if (!validateFile(file, RepositoryType.JAR)) { continue; } if (log.isDebugEnabled()) log.debug(" Including glob jar file " + file.getAbsolutePath()); URL url = buildClassLoaderUrl(file); set.add(url); } } } } // Construct the class loader itself final URL[] array = set.toArray(new URL[set.size()]); if (log.isDebugEnabled()) for (int i = 0; i < array.length; i++) { log.debug(" location " + i + " is " + array[i]); } return AccessController.doPrivileged( new PrivilegedAction<URLClassLoader>() { @Override public URLClassLoader run() { if (parent == null) // URLClassLoader是一个可以从指定目录或网络地址加载class的一个类加载器 return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); } private static URL buildClassLoaderUrl(File file) throws MalformedURLException { String fileUrlString = file.toURI().toString(); fileUrlString = fileUrlString.replaceAll("!/", "%21/"); return new URL(fileUrlString); }
通过上面的介绍,读者已经对ClassLoaderFactory 类有所了解,下面用一个简单的例子展示Tomcat中的常见类加载器是如何利用ClassLoaderFactory 工厂类来创建的,代码如下 :
List<Repository> repositorys = new ArrayList<>();
repositorys.add(new Repository(“$catalina.home}/lib”,RepositoryType.DIR));
repositorys.add(new Repository(“$catalina.home}/lib”,RepositoryType.GLOB);
repositorys.add(new Repository(“$catalina.base}/lib”,RepositoryType.DIR));
repositorys.add(new Repository(“]$catalina.base}/lib”,RepositoryType.GLOB);
ClassLoaderParent = null;
ClassLoader commonLoader = ClassLoaderFactory.createClassLoader(repositories, parent);
到此Common 类加载器创建完毕,其中,${catalina.home} 与 ${catalina.base} 表示变量 ,它的值分别是Tomcat的安装目录与Tomcat 的工作目录,Parent 为父类加载器,如果它设置为null,ClassLoaderFactory 创建时会使用默认的父类加载器,即系统类加载器,总结起来,只需要以下的几步就能完成一个类加载器的创建,首先, 把要加载的资源 加载到一个列表中,其次确定父类的加载器,默认就设置为null, 最后,把这些作为参数传入ClassLoaderFactory工厂类。
如果我们不确定要加载的资源是网络上的还是本地上的,那么可以用以下的方式进行处理。
try {
URL url = new URL("路径 ");
repositories.add(new Repository("路径 ",RepositoryType.URL));
}catch( MalformedURLException e ){
}
这种方式处理得比较巧妙,URL 在实例化时可以检查到这个路径有效性,假如为本地资源或者网上不存在的路径资源 ,那么将抛出异常,不会把路径添加到资源列表中。
ClassLoaderFactory 工厂类最终将资源转换成URL[] 数组,因为ClassLoaderFactory 生成的类加载器继承于URLClassLoader 的,而URLClassLoader 的构造函数只支持URL[] 数组,从Repository 类转换成URL[] 数组可分为以下几种情况 。
我相信经过上面这段代码的分析,你对Tomcat类加载器这一块代码已经有了深入理解,接下来,我们来看init()方法后面的代码 。
// Load our startup class and call its process() method
// 加载Catalina类,并生成instance
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
// 设置Catalina实例的父级类加载器为sharedLoader(默认情况下就是commonLoader)
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;
其实这段代码的原理也很简单,通过反射创建org.apache.catalina.startup.Catalina对象,设置Catalina实例的父级类加载器为sharedLoader(默认情况下就是commonLoader)。
我们继续接着看Bootstrap的main 方法后面的代码 。
try {
String command = "start";
// 在Tomcat 启动停止脚本中,默认最后一个参数为命令(如stop ,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); // 解析server.xml,初始化Catalina
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} 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);
}
我们先分析tomcat的启动,所以着重看加粗这一块的代码。先看daemon.setAwait(true); 这一行代码,我们从Bootstrap的main 方法就知道,daemon即为Bootstrap对象,而setAwait方法,即调用Bootstrap的setAwait方法,进入代码 。
public void setAwait(boolean await) throws Exception { Class<?> paramTypes[] = new Class[1]; paramTypes[0] = Boolean.TYPE; Object paramValues[] = new Object[1]; paramValues[0] = Boolean.valueOf(await); Method method = catalinaDaemon.getClass().getMethod("setAwait", paramTypes); method.invoke(catalinaDaemon, paramValues); }
在之前的分析中,我们知道catalinaDaemon即为Catalina对象,因此setAwait()方法实际上是调用了Catalina的setAwait方法,将Catalina对象属性await设置为true。
我们继续看load()方法。
private void load(String[] arguments) throws Exception { 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); }
关于load方法的实现,其实原理也很简单,实际上也是通过反射调用了Catalina的load()方法,分为两种情况,有参数和无参数,接下来,我们进入Catalina中看load()方法帮我们做了哪些事情,不过我们先看有参的load()方法 。
public void load(String args[]) {
try {
if (arguments(args)) {
load();
}
} catch (Exception e) {
e.printStackTrace(System.out);
}
}
// 解析命令行输入的参数,所以我们能从这里知道命令行里能添加哪些参数
protected boolean arguments(String args[]) {
boolean isConfig = false;
if (args.length < 1) {
usage();
return (false);
}
for (int i = 0; i < args.length; i++) {
if (isConfig) {
configFile = args[i];
isConfig = false;
} else if (args[i].equals("-config")) {
isConfig = true;
} else if (args[i].equals("-nonaming")) {
setUseNaming( false );
} else if (args[i].equals("-help")) {
usage();
return (false);
} else if (args[i].equals("start")) {
starting = true;
stopping = false;
} else if (args[i].equals("configtest")) {
starting = true;
stopping = false;
} else if (args[i].equals("stop")) {
starting = false;
stopping = true;
} else {
usage();
return (false);
}
}
return (true);
}
从上述方法中可以看出 ,带参数的load()方法最终还是会调没有带参数的load()方法 ,而在此之前,却调用了arguments()方法,解析参数,例如 start 命令,则会将starting设置为true , stopping设置为false 。如果命令是-help ,则会调用usage()方法,而usage()方法打印出命令行参数的使用,默认也是调用usage()方法。
System.out.println
(“usage: java org.apache.catalina.startup.Catalina”
+ " [ -config {pathname} ]"
+ " [ -nonaming ] "
+ " { -help | start | stop }");
接下来看Catalina中无参的load()方法帮我们做了哪些事情 。
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
// 如果catalinaHome和catalinaBase是相对路径,那么在这里会转化为绝对路径
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
// 初始化server.xml文件解析器
Digester digester = createStartDigester();
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
// 先从文件系统获取server.xml
try {
file = configFile(); // 获取catalina.base目录下的conf/server.xml文件
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
// 如果文件系统没有,则从classloader中获取server.xml
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
}
// This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
// 如果没找到server.xml,那么则从classloader中找server-embed.xml
if( inputStream==null ) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
}
// 如果没找到server.xml或server-embed.xml,那么告警
// 如果文件存在,判断文件没有可读权限
if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
}
try {
// 解析server.xml或server-embed.xml文件
inputSource.setByteStream(inputStream);
digester.push(this);
// 比较重要的一行代码,当digester已经建立好xml标签之间的关系后,这里就开始解析xml 了
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
// 解析完server.xml或server-embed.xml后,将catalina设置到StandardServer中
getServer().setCatalina(this);
// Stream redirection
// 把System.out和System.err替换成SystemLogHandler对象
initStreams();
// Start the new server
// 解析完配置文件,开始初始化Server,而从初始化Server开始,就包括了一系列的子组件的初始化
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}
如果catalinaHome和catalinaBase是相对路径,那么在这里会转化为绝对路径,我们看其具体实现。
protected void initDirs() { String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP); if (catalinaHome == null) { // Backwards compatibility patch for J2EE RI 1.3 String j2eeHome = System.getProperty("com.sun.enterprise.home"); if (j2eeHome != null) { catalinaHome=System.getProperty("com.sun.enterprise.home"); } else if (System.getProperty(Globals.CATALINA_BASE_PROP) != null) { catalinaHome = System.getProperty(Globals.CATALINA_BASE_PROP); } } // last resort - for minimal/embedded cases. // 如果环境变量中catalina.home为空,则设置catalina.home为工作目录 if(catalinaHome==null) { catalinaHome=System.getProperty("user.dir"); } if (catalinaHome != null) { File home = new File(catalinaHome); // 如果是相对路径,则转化为绝对路径 if (!home.isAbsolute()) { try { catalinaHome = home.getCanonicalPath(); } catch (IOException e) { catalinaHome = home.getAbsolutePath(); } } System.setProperty(Globals.CATALINA_HOME_PROP, catalinaHome); } if (System.getProperty(Globals.CATALINA_BASE_PROP) == null) { // 如果环境变量中catalina.base为空,则设置catalina.base为catalinaHome // 此时catalinaHome 已经是绝对路径了 System.setProperty(Globals.CATALINA_BASE_PROP,catalinaHome); } else { String catalinaBase = System.getProperty(Globals.CATALINA_BASE_PROP); File base = new File(catalinaBase); // 如果环境变量中catalina.base依然为相对路径,则转化为绝对路径 if (!base.isAbsolute()) { try { catalinaBase = base.getCanonicalPath(); } catch (IOException e) { catalinaBase = base.getAbsolutePath(); } } // 重新设置catalina.base的路径 System.setProperty(Globals.CATALINA_BASE_PROP, catalinaBase); } // 如果临时目录不存在,或不是一个目录,则打印异常 String temp = System.getProperty("java.io.tmpdir"); if (temp == null || (!(new File(temp)).exists()) || (!(new File(temp)).isDirectory())) { log.error(sm.getString("embedded.notmp", temp)); } }
其实initDirs()方法写了一大堆,但是实现原理还是很简单的,就是判断catalina.home在环境变量中是否存在,如果不存在设置当前工作目录为catalina.home,而catalina.base也是相同的套路,最后判断一下java.io.tmpdir是否存在并且是一个目录,否则打印一下错误信息。
protected void initNaming() { // 默认情况下 useNaming = true if (!useNaming) { log.info( "Catalina naming disabled"); System.setProperty("catalina.useNaming", "false"); } else { System.setProperty("catalina.useNaming", "true"); String value = "org.apache.naming"; String oldValue = System.getProperty("java.naming.factory.url.pkgs"); if (oldValue != null) { value = value + ":" + oldValue; } System.setProperty("java.naming.factory.url.pkgs", value); if( log.isDebugEnabled() ) { log.debug("Setting naming prefix=" + value); } value = System.getProperty ("java.naming.factory.initial"); if (value == null) { System.setProperty ("java.naming.factory.initial", "org.apache.naming.java.javaURLContextFactory"); } else { log.debug( "INITIAL_CONTEXT_FACTORY already set " + value ); } } }
initNaming()主要设置catalina.useNaming的值,有什么用呢?我们在后续的博客中分析Tomcat 集成JNDI 再来分析 。
我们回到lCatalina的load方法,继续看Digester的创建代码 。
/** * Create and configure the Digester we will be using for startup. */ protected Digester createStartDigester() { long t1=System.currentTimeMillis(); // Initialize the digester Digester digester = new Digester(); digester.setValidating(false); digester.setRulesValidation(true); Map<Class<?>, List<String>> fakeAttributes = new HashMap<Class<?>, List<String>>(); List<String> objectAttrs = new ArrayList<String>(); objectAttrs.add("className"); fakeAttributes.put(Object.class, objectAttrs); // Ignore attribute added by Eclipse for its internal tracking List<String> contextAttrs = new ArrayList<String>(); contextAttrs.add("source"); fakeAttributes.put(StandardContext.class, contextAttrs); digester.setFakeAttributes(fakeAttributes); digester.setUseContextClassLoader(true); // Configure the actions we will be using // 将<Server>节点,解析为一个org.apache.catalina.core.StandardServer对象,如果配置了className属性,则会解析对应的类对象。 digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className"); // 将<Server>节点中的属性,使用StandardServer对象对应的set方法进行属性初始化 digester.addSetProperties("Server"); // 将<Server>节点对应的对象,调用<Server>节点的父节点对象的setServer(org.apache.catalina.Server params)方法,Server的父对象为this,后面会设置,也就Catalina对象。 digester.addSetNext("Server", "setServer", "org.apache.catalina.Server"); digester.addObjectCreate("Server/GlobalNamingResources", "org.apache.catalina.deploy.NamingResources"); digester.addSetProperties("Server/GlobalNamingResources"); digester.addSetNext("Server/GlobalNamingResources", "setGlobalNamingResources", "org.apache.catalina.deploy.NamingResources"); // 对于Server/Listener节点,比如配置对于的实现类。 digester.addObjectCreate("Server/Listener", null, // MUST be specified in the element "className"); digester.addSetProperties("Server/Listener"); digester.addSetNext("Server/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); digester.addObjectCreate("Server/Service", "org.apache.catalina.core.StandardService", "className"); digester.addSetProperties("Server/Service"); digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service"); digester.addObjectCreate("Server/Service/Listener", null, // MUST be specified in the element "className"); digester.addSetProperties("Server/Service/Listener"); digester.addSetNext("Server/Service/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); //Executor digester.addObjectCreate("Server/Service/Executor", "org.apache.catalina.core.StandardThreadExecutor", "className"); digester.addSetProperties("Server/Service/Executor"); digester.addSetNext("Server/Service/Executor", "addExecutor", "org.apache.catalina.Executor"); // 创建Connector对象,在里面会初始化executor digester.addRule("Server/Service/Connector", new ConnectorCreateRule()); // 根据Connector节点的属性,调用set方法进行初始化,除开executor属性。 digester.addRule("Server/Service/Connector", new SetAllPropertiesRule(new String[]{"executor"})); // 将Connector对象通过调用Service.addConnector方法添加到Service中去,addConnector方法并不是简单的实现,还有其他逻辑,后面在详细的介绍。 digester.addSetNext("Server/Service/Connector", "addConnector", "org.apache.catalina.connector.Connector"); digester.addObjectCreate("Server/Service/Connector/Listener", null, // MUST be specified in the element "className"); digester.addSetProperties("Server/Service/Connector/Listener"); digester.addSetNext("Server/Service/Connector/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); // Add RuleSets for nested elements // addRuleSet方法实现也不复杂,就是调用NamingRuleSet、EngineRuleSet这些类的addRuleInstances方法 digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/")); digester.addRuleSet(new EngineRuleSet("Server/Service/")); digester.addRuleSet(new HostRuleSet("Server/Service/Engine/")); digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/")); addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/"); digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/")); // When the 'engine' is found, set the parentClassLoader. // 在解析Engine节点的时候,设置parentClassLoader为Catalina.class的类加载器, parentClassLoader为sharedClassLoader digester.addRule("Server/Service/Engine", new SetParentClassLoaderRule(parentClassLoader)); // shareClassLoader addClusterRuleSet(digester, "Server/Service/Engine/Cluster/"); long t2=System.currentTimeMillis(); if (log.isDebugEnabled()) { log.debug("Digester for server.xml created " + ( t2-t1 )); } return (digester); }
上面写了那么多,但是什么意思呢?我们先来看addObjectCreate方法。
public void addObjectCreate(String pattern, String className, String attributeName) { addRule(pattern, new ObjectCreateRule(className, attributeName)); } public void addRule(String pattern, Rule rule) { rule.setDigester(this); getRules().add(pattern, rule); }
从上面来看, 看不出什么东西,只知道以字符串如Server为key , ObjectCreateRule为值加入到规则中,不急继续看,addSetProperties和SetNextRule的实现。
public void addSetProperties(String pattern) { addRule(pattern, new SetPropertiesRule()); } public void addSetNext(String pattern, String methodName, String paramType) { addRule(pattern, new SetNextRule(methodName, paramType)); }
如果addSetProperties的参数为Server, 我们可能还是无法理解addSetProperties()的功能是将<Server>节点中的属性,使用StandardServer对象对应的set方法进行属性初始化,也无法理解addSetNext(“Server”, “setServer”, “org.apache.catalina.Server”); 是将<Server>节点对应的对象,调用<Server>节点的父节点对象的setServer(org.apache.catalina.Server params)方法,Server的父对象为this,后面会设置,也就Catalina对象。那么我们接下来看ObjectCreateRule和SetPropertiesRule及SetNextRule他们之间的特性。

发现一个共同的特点,他们都实现了Rule接口,或多或少的实现了start方法和end方法,实现这个方法有何用呢?我们再来看Digester的类结构。

看到了Digester实现了org.xml.sax.helpers.DefaultHandler,证明最终xml由Digester来解析。
@Override
public void startElement(String namespaceURI, String localName, String qName, Attributes list)
throws SAXException {
boolean debug = log.isDebugEnabled();
if(debug){
saxLog.info("startElement(" + namespaceURI + "," + localName + "," + qName + ")");
}
// Parse system properties
// 将${xxx} 替换成系统中的环境变量
list = updateAttributes(list);
// Save the body text accumulated for our surrounding element
bodyTexts.push(bodyText);
bodyText = new StringBuilder();
// the actual element name is either in localName or qName, depending
// on whether the parser is namespace aware
String name = localName;
if ((name == null) || (name.length() < 1)) {
name = qName;
}
// Compute the current matching rule
StringBuilder sb = new StringBuilder(match);
if (match.length() > 0) {
sb.append('/');
}
sb.append(name);
match = sb.toString();
if (debug) {
log.debug(" New match='" + match + "'");
}
// Fire "begin" events for all relevant rules
// 获取规则,如解析<Server>节点,则会获得ObjectCreateRule和SetPropertiesRule及SetNextRule解析器规则
List<Rule> rules = getRules().match(namespaceURI, match);
matches.push(rules);
if ((rules != null) && (rules.size() > 0)) {
for (int i = 0; i < rules.size(); i++) {
try {
Rule rule = rules.get(i);
if (debug) {
log.debug(" Fire begin() for " + rule);
}
// 调用规则的begin方法
rule.begin(namespaceURI, name, list);
} catch (Exception e) {
log.error("Begin event threw exception", e);
throw createSAXException(e);
} catch (Error e) {
log.error("Begin event threw error", e);
throw e;
}
}
} else {
if (debug) {
log.debug(" No rules found matching '" + match + "'.");
}
}
}

接着分别看ObjectCreateRule和SetPropertiesRule及SetNextRule的begin方法做了哪些事情 。先看ObjectCreateRule的begin方法 。
@Override
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
// Identify the name of the class to instantiate
String realClassName = className;
if (attributeName != null) {
String value = attributes.getValue(attributeName);
if (value != null) {
realClassName = value;
}
}
if (digester.log.isDebugEnabled()) {
digester.log.debug("[ObjectCreateRule]{" + digester.match +
"}New " + realClassName);
}
if (realClassName == null) {
throw new NullPointerException("No class name specified for " +
namespace + " " + name);
}
// Instantiate the new object and push it on the context stack
Class<?> clazz = digester.getClassLoader().loadClass(realClassName);
Object instance = clazz.newInstance();
digester.push(instance);
}
果然,ObjectCreateRule对象的功能和他的名字一样,先用digester.getClassLoader()加载类,然后实例化它,最后放到digester的stack中。接着看SetPropertiesRule的begin方法 。
@Override
public void begin(String namespace, String theName, Attributes attributes)
throws Exception {
// Populate the corresponding properties of the top object
// 之前在调用ObjectCreateRule的begin方法将对象放入到操作数栈顶中,此时取出
Object top = digester.peek();
if (digester.log.isDebugEnabled()) {
if (top != null) {
digester.log.debug("[SetPropertiesRule]{" + digester.match +
"} Set " + top.getClass().getName() +
" properties");
} else {
digester.log.debug("[SetPropertiesRule]{" + digester.match +
"} Set NULL properties");
}
}
// set up variables for custom names mappings
int attNamesLength = 0;
if (attributeNames != null) {
attNamesLength = attributeNames.length;
}
int propNamesLength = 0;
if (propertyNames != null) {
propNamesLength = propertyNames.length;
}
for (int i = 0; i < attributes.getLength(); i++) {
String name = attributes.getLocalName(i);
if ("".equals(name)) {
name = attributes.getQName(i);
}
String value = attributes.getValue(i);
// we'll now check for custom mappings
// 给开发者提供了自定义支持
for (int n = 0; n<attNamesLength; n++) {
if (name.equals(attributeNames[n])) {
if (n < propNamesLength) {
// set this to value from list
name = propertyNames[n];
} else {
// set name to null
// we'll check for this later
name = null;
}
break;
}
}
if (digester.log.isDebugEnabled()) {
digester.log.debug("[SetPropertiesRule]{" + digester.match +
"} Setting property '" + name + "' to '" +
value + "'");
}
if (!digester.isFakeAttribute(top, name)
// 利用反射调用setXXX方法向给top 对象的属性中设置值
&& !IntrospectionUtils.setProperty(top, name, value)
&& digester.getRulesValidation()) {
digester.log.warn("[SetPropertiesRule]{" + digester.match +
"} Setting property '" + name + "' to '" +
value + "' did not find a matching property.");
}
}
}


我们依然以<Server port=“8005” shutdown=“SHUTDOWN”>为例子,Server标签有两个属性port和shutdown,属性值分别为8005和SHUTDOWN,最终将调用org.apache.catalina.core.StandardServer的setPort和setShutdown方法将server.xml中配置的值设置到StandardServer的port和shutdown属性中。接着我们看SetNextRule的begin方法 。
public void begin(String namespace, String name, Attributes attributes)
throws Exception {
begin(attributes);
}
@Deprecated
public void begin(Attributes attributes) throws Exception {
// The default implementation does nothing
}
发现SetNextRule的begin方法只是一个空实现,什么也没有做,接下来,我们来分析,当解析到结束标签时,Tomcat 又帮我们做了哪些事情呢?
@Override
public void endElement(String namespaceURI, String localName, String qName)
throws SAXException {
boolean debug = log.isDebugEnabled();
if (debug) {
if (saxLog.isDebugEnabled()) {
saxLog.debug("endElement(" + namespaceURI + "," + localName + "," + qName + ")");
}
log.debug(" match='" + match + "'");
log.debug(" bodyText='" + bodyText + "'");
}
// Parse system properties
// 将body中的${} 用系统环境变量替换掉
bodyText = updateBodyText(bodyText);
// the actual element name is either in localName or qName, depending
// on whether the parser is namespace aware
String name = localName;
if ((name == null) || (name.length() < 1)) {
name = qName;
}
// Fire "body" events for all relevant rules
// 出于性能考虑,此时并不像startElement一样,从cache中取,而是从stack中弹出即可
List<Rule> rules = matches.pop();
if ((rules != null) && (rules.size() > 0)) {
String bodyText = this.bodyText.toString().intern();
for (int i = 0; i < rules.size(); i++) {
try {
Rule rule = rules.get(i);
if (debug) {
log.debug(" Fire body() for " + rule);
}
// 如果元素中有body 根据不同的规则,做不同的处理
rule.body(namespaceURI, name, bodyText);
} catch (Exception e) {
log.error("Body event threw exception", e);
throw createSAXException(e);
} catch (Error e) {
log.error("Body event threw error", e);
throw e;
}
}
} else {
if (debug) {
log.debug(" No rules found matching '" + match + "'.");
}
if (rulesValidation) {
log.warn(" No rules found matching '" + match + "'.");
}
}
// Recover the body text from the surrounding element
bodyText = bodyTexts.pop();
// Fire "end" events for all relevant rules in reverse order
// 这里需要注意的一点是end 方法的执行顺序和begin方法的执行顺序刚好相反 。
// 【注意】 Server标签的 begin方法的执行顺序分别是ObjectCreateRule和SetPropertiesRule及SetNextRule
// 那么end 方法的执行顺序即为SetNextRule,SetPropertiesRule , ObjectCreateRule
if (rules != null) {
for (int i = 0; i < rules.size(); i++) {
int j = (rules.size() - i) - 1;
try {
Rule rule = rules.get(j);
if (debug) {
log.debug(" Fire end() for " + rule);
}
rule.end(namespaceURI, name);
} catch (Exception e) {
log.error("End event threw exception", e);
throw createSAXException(e);
} catch (Error e) {
log.error("End event threw error", e);
throw e;
}
}
}
// Recover the previous match expression
int slash = match.lastIndexOf('/');
if (slash >= 0) {
match = match.substring(0, slash);
} else {
match = "";
}
}
因为end方法的执行顺序刚好相反,我们先来看SetNextRule的end方法 。
@Override
public void end(String namespace, String name) throws Exception {
// Identify the objects to be used
// 弹出当前标签对象
Object child = digester.peek(0);
// 弹出当前标签的父亲标签对象
Object parent = digester.peek(1);
if (digester.log.isDebugEnabled()) {
if (parent == null) {
digester.log.debug("[SetNextRule]{" + digester.match +
"} Call [NULL PARENT]." +
methodName + "(" + child + ")");
} else {
digester.log.debug("[SetNextRule]{" + digester.match +
"} Call " + parent.getClass().getName() + "." +
methodName + "(" + child + ")");
}
}
// 通过反射调用父亲标签类的setXXX方法,将当前标签的对象设置到父亲标签所在类中
IntrospectionUtils.callMethod1(parent, methodName,
child, paramType, digester.getClassLoader());
}
我们继续看SetPropertiesRule的end 方法,发现SetPropertiesRule并没有重写Rule的end方法,而Rule的end方法即是一个空实现。接着看ObjectCreateRule的end方法 。
@Override
public void end(String namespace, String name) throws Exception {
Object top = digester.pop();
if (digester.log.isDebugEnabled()) {
digester.log.debug("[ObjectCreateRule]{" + digester.match +
"} Pop " + top.getClass().getName());
}
}
ObjectCreateRule 的 end 方法的实现原理是不是很简单,直接将当前标签所对应的对象从栈中弹出即可 。此时我相信大家对Digester的套路有所理解了。

首先解析Server标签,通过ObjectCreateRule 创建org.apache.catalina.core.StandardServer对象,并且将其推入到栈中,然后通过SetPropertiesRule的begin方法将port以及shutdown属性通过反射设置到StandardServer对象属性中,解析GlobalNamingResources标签时,创建org.apache.catalina.deploy.NamingResources对象,并且将GlobalNamingResources对象推入到栈中,解析Resource标签,创建org.apache.catalina.deploy.ContextResource对象,并将ContextResource对象推入到栈中,并将name,auth , type , description 这些参数通过反射设置到ContextResource对象中,再通过SetNextRule的end方法,通过反射调用addResource方法将ContextResource对象添加到NamingResources的resources属性中,在ObjectCreateRule的end 方法中将ContextResource从栈中弹出,而NamingResources对象则被反射调用setGlobalNamingResources添加到StandardServer对象的globalNamingResources属性中,同样也被ObjectCreateRule的end方法将NamingResources对象从栈中弹出 ,最终解析完其他标签后,StandardServer也被从栈中弹出,这只是最简单也是最基本的套路,我相信大家肯定看明白了。
假如,有相同的父亲标签的子标识,也重复的写addObjectCreate(),addSetProperties(),addSetNext()方法不?显然不是,我们接下来看另外一种实现方式 。
digester.addRuleSet(new EngineRuleSet(“Server/Service/”));
先来看看addRuleSet()方法的实现。
public void addRuleSet(RuleSet ruleSet) {
String oldNamespaceURI = getRuleNamespaceURI();
// 如果ruleSet设置了namespaceURI,此时用新的namespaceURI
String newNamespaceURI = ruleSet.getNamespaceURI();
if (log.isDebugEnabled()) {
if (newNamespaceURI == null) {
log.debug("addRuleSet() with no namespace URI");
} else {
log.debug("addRuleSet() with namespace URI " + newNamespaceURI);
}
}
setRuleNamespaceURI(newNamespaceURI);
ruleSet.addRuleInstances(this);
// 恢复之前的namespaceURI
setRuleNamespaceURI(oldNamespaceURI);
}
重要的方法,就是上面加粗的代码。那这一行代码做了哪些事情呢?我们以EngineRuleSet为例来看。
@Override public void addRuleInstances(Digester digester) { // prefix是Server/Service/ // 创建StandardEngine digester.addObjectCreate(prefix + "Engine", "org.apache.catalina.core.StandardEngine", "className"); // set方式初始化属性 digester.addSetProperties(prefix + "Engine"); digester.addRule(prefix + "Engine", new LifecycleListenerRule ("org.apache.catalina.startup.EngineConfig", "engineConfigClass")); // 将StandardEngine通过调用Service的setContainer方法设置进Service中 digester.addSetNext(prefix + "Engine", "setContainer", "org.apache.catalina.Container"); //Cluster configuration start digester.addObjectCreate(prefix + "Engine/Cluster", null, // MUST be specified in the element "className"); digester.addSetProperties(prefix + "Engine/Cluster"); digester.addSetNext(prefix + "Engine/Cluster", "setCluster", "org.apache.catalina.Cluster"); //Cluster configuration end digester.addObjectCreate(prefix + "Engine/Listener", null, // MUST be specified in the element "className"); digester.addSetProperties(prefix + "Engine/Listener"); digester.addSetNext(prefix + "Engine/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener"); digester.addRuleSet(new RealmRuleSet(prefix + "Engine/")); // Valve也必须指定className digester.addObjectCreate(prefix + "Engine/Valve", null, // MUST be specified in the element "className"); digester.addSetProperties(prefix + "Engine/Valve"); digester.addSetNext(prefix + "Engine/Valve", "addValve", "org.apache.catalina.Valve"); }
大家发现没有,依然是相同的套路,addObjectCreate(),addSetProperties(),addRule()方法 。只是有一个特点,他们有相同的前缀Server/Service/,这里就不再缀述了,既然如此,那整个Tomcat 的server.xml 的结构是怎样子的呢? 请看下面。
<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN" className="org.apache.catalina.core.StandardServer" addParentMethod="setServer">
<!--所有的listener 都要实现org.apache.catalina.LifecycleListener接口-->
<Listener className="org.apache.catalina.startup.VersionLoggerListener" addParentMethod="addLifecycleListener" paramType="org.apache.catalina.LifecycleListener"/>
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on"/>
<Listener className="org.apache.catalina.core.JasperListener"/>
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener"/>
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
<GlobalNamingResources className="org.apache.catalina.deploy.NamingResources" addParentMethod="setGlobalNamingResources" paramType="org.apache.catalina.deploy.NamingResources">
<Resource name="UserDatabase" auth="Container" addParentMethod="addResource" paramType="org.apache.catalina.deploy.ContextResource"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml"/>
<Ejb className="org.apache.catalina.deploy.ContextEjb" addParentMethod="addEjb" paramType="org.apache.catalina.deploy.ContextEjb"></Ejb>
<Environment className="org.apache.catalina.deploy.ContextEnvironment" addParentMethod="addEnvironment" paramType="org.apache.catalina.deploy.ContextEnvironment"></Environment>
<LocalEjb className="org.apache.catalina.deploy.ContextLocalEjb" addParentMethod="addLocalEjb" paramType="org.apache.catalina.deploy.ContextLocalEjb"></LocalEjb>
<ResourceEnvRef className="org.apache.catalina.deploy.ContextResourceEnvRef" addParentMethod="addResourceEnvRef" paramType="org.apache.catalina.deploy.ContextResourceEnvRef"></ResourceEnvRef>
<ServiceRef className="org.apache.catalina.deploy.ContextService" addParentMethod="addService" paramType="org.apache.catalina.deploy.ContextService" ></ServiceRef>
<Transaction className="org.apache.catalina.deploy.ContextTransaction" addParentMethod="setTransaction" paramType="org.apache.catalina.deploy.ContextTransaction" ></Transaction>
</GlobalNamingResources>
<Service name="Catalina" className="org.apache.catalina.core.StandardService" addParentMethod="addService" paramType="org.apache.catalina.Service" >
<Listener className="org.apache.catalina.core.JasperListener" addParentMethod="addLifecycleListener" paramType="org.apache.catalina.LifecycleListener"/>
<Executor name="executor" className="org.apache.catalina.core.StandardThreadExecutor" addParentMethod="addExecutor" paramType="org.apache.catalina.Executor" ></Executor>
<Connector port="8080" protocol="HTTP/1.1" addParentMethod="addConnector" paramType="org.apache.catalina.connector.Connector"
connectionTimeout="20000"
redirectPort="8443" >
<Listener className="org.apache.catalina.core.xxx" addParentMethod="addLifecycleListener" paramType="org.apache.catalina.LifecycleListener"/>
</Connector>
<Engine name="Catalina" defaultHost="localhost" className="org.apache.catalina.core.StandardEngine" addParentMethod="setContainer" paramType="org.apache.catalina.Container" >
<Cluster className="xxx" addParentMethod="setCluster" paramType="org.apache.catalina.Cluster"></Cluster>
<Listener className="xxx" addParentMethod="addLifecycleListener" paramType="org.apache.catalina.LifecycleListener"/>
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context className="org.apache.catalina.core.StandardContext" addParentMethod="addChild" paramType="org.apache.catalina.Container">
<InstanceListener addParentMethod="addInstanceListener" ></InstanceListener>
<Listener className="xxx" addParentMethod="addLifecycleListener" paramType="org.apache.catalina.LifecycleListener"/>
<Loader className="org.apache.catalina.loader.WebappLoader" addParentMethod="setLoader" paramType="org.apache.catalina.Loader"> </Loader>
<Manager className="org.apache.catalina.session.StandardManager" addParentMethod="setManager" paramType="org.apache.catalina.Manager">
<Store className="xxx" addParentMethod="setStore" paramType="org.apache.catalina.Store"></Store>
<SessionIdGenerator className="org.apache.catalina.util.StandardSessionIdGenerator" addParentMethod="setSessionIdGenerator" paramType="org.apache.catalina.SessionIdGenerator"></SessionIdGenerator>
</Manager>
<Parameter className="org.apache.catalina.deploy.ApplicationParameter" addParentMethod="addApplicationParameter" paramType="org.apache.catalina.deploy.ApplicationParameter"></Parameter>
<Resources className="org.apache.naming.resources.FileDirContext" addParentMethod="setResources" paramType="javax.naming.directory.DirContext"></Resources>
<ResourceLink className="org.apache.catalina.deploy.ContextResourceLink" addParentMethod="addResourceLink" paramType="org.apache.catalina.deploy.ContextResourceLink"></ResourceLink>
<Valve className="xxx" addParentMethod="addValve" paramType="org.apache.catalina.Valve"></Valve>
<WatchedResource className="xxx" addParentMethod="addValve"></WatchedResource>
<WrapperLifecycle className="xxx" addParentMethod="addWrapperLifecycle" ></WrapperLifecycle>
<WrapperListener addParentMethod="addWrapperListener"></WrapperListener>
<JarScanner className="org.apache.tomcat.util.scan.StandardJarScanner" addParentMethod="setJarScanner" paramType="org.apache.tomcat.JarScanner"></JarScanner>
<Ejb className="org.apache.catalina.deploy.ContextEjb" addParentMethod="addEjb" paramType="org.apache.catalina.deploy.ContextEjb"></Ejb>
<Environment className="org.apache.catalina.deploy.ContextEnvironment" addParentMethod="addEnvironment" paramType="org.apache.catalina.deploy.ContextEnvironment"></Environment>
<LocalEjb className="org.apache.catalina.deploy.ContextLocalEjb" addParentMethod="addLocalEjb" paramType="org.apache.catalina.deploy.ContextLocalEjb"></LocalEjb>
<ResourceEnvRef className="org.apache.catalina.deploy.ContextResourceEnvRef" addParentMethod="addResourceEnvRef" paramType="org.apache.catalina.deploy.ContextResourceEnvRef"></ResourceEnvRef>
<ServiceRef className="org.apache.catalina.deploy.ContextService" addParentMethod="addService" paramType="org.apache.catalina.deploy.ContextService" ></ServiceRef>
<Transaction className="org.apache.catalina.deploy.ContextTransaction" addParentMethod="setTransaction" paramType="org.apache.catalina.deploy.ContextTransaction" ></Transaction>
</Context>
<Cluster className="org.apache.catalina.ha.ClusterRuleSet" >
<Manager className="xxx" addParentMethod="setManagerTemplate" paramType="org.apache.catalina.ha.ClusterManager">
<SessionIdGenerator className="org.apache.catalina.util.StandardSessionIdGenerator" addParentMethod="setSessionIdGenerator" paramType="org.apache.catalina.SessionIdGenerator"></SessionIdGenerator>
</Manager>
<Channel className="xxx" addParentMethod="setChannel" paramType="org.apache.catalina.tribes.Channel">
<Membership className="xxx" addParentMethod="setMembershipService" paramType="org.apache.catalina.tribes.MembershipService"></Membership>
<MembershipListener className="xxx" addParentMethod="addMembershipListener" paramType="org.apache.catalina.tribes.MembershipListener"></MembershipListener>
<Sender className="xxx" addParentMethod="setChannelSender" paramType="org.apache.catalina.tribes.ChannelSender">
<Transport className="xxx" addParentMethod="setTransport"
paramType="org.apache.catalina.tribes.transport.MultiPointSender"></Transport>
</Sender>
<Receiver className="xxx" addParentMethod="setChannelReceiver"
paramType="org.apache.catalina.tribes.ChannelReceiver"></Receiver>
<Interceptor className="xxx" addParentMethod="addInterceptor"
paramType="org.apache.catalina.tribes.ChannelInterceptor">
<LocalMember className="xxx" addParentMethod="setLocalMember"
paramType="org.apache.catalina.tribes.Member"></LocalMember>
<Member className="xxx" addParentMethod="addStaticMember"
paramType="org.apache.catalina.tribes.Member"></Member>
</Interceptor>
<ChannelListener className="xxx" addParentMethod="addChannelListener"
paramType="org.apache.catalina.tribes.ChannelListener"></ChannelListener>
</Channel>
<Valve className="xxx" addParentMethod="addValve" paramType="org.apache.catalina.Valve"></Valve>
<Deployer className="xxx" addParentMethod="setClusterDeployer"
paramType="org.apache.catalina.ha.ClusterDeployer"></Deployer>
<Listener className="xxx" addParentMethod="addLifecycleListener"
paramType="org.apache.catalina.LifecycleListener"></Listener>
<ClusterListener className="xxx" addParentMethod="addClusterListener"
paramType="org.apache.catalina.ha.ClusterListener"></ClusterListener>
</Cluster>
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b"/>
</Host>
<Valve className="xx" addParentMethod="addValve" paramType="org.apache.catalina.Valve"></Valve>
</Engine>
</Service>
</Server>
addParentMethod方法表示调用父类的xxx方法将自身对象加入到父类中, paramType表示 xxx方法的参数类型。我们先对server.xml文件的结构有一个大体的认识,在后面的博客中,我们再来逐一分析 。
public Object parse(InputSource input) throws IOException, SAXException { configure(); getXMLReader().parse(input); return root; } public XMLReader getXMLReader() throws SAXException { if (reader == null) { reader = getParser().getXMLReader(); } reader.setDTDHandler(this); // 这里的this 表示Degister自身 reader.setContentHandler(this); if (entityResolver == null) { reader.setEntityResolver(this); } else { reader.setEntityResolver(entityResolver); } reader.setProperty("http://xml.org/sax/properties/lexical-handler", this); reader.setErrorHandler(this); return reader; }
parse方法的原理很简单,指定xml 处理器为Digester 本身 ,如果没有reader,则创建一个SAXParser解析器。
最终会解析成为以StandardServer为根节点的一棵树。形成这样一棵树有什么用途呢?

像Tomcat 这么大的系统,必然需要对生命周期进行统一管理 , 那么Tomcat 是怎样管理自己的生命周期的呢? 本章将对Tomcat 的生命周期进行介绍 。
生命周期统一接口-Lifecycle 。
Tomcat的架构设计是清晰的,模块化的,它拥有很多的组件,假如在启动Tomcat时一个一个的组件启动,它不仅麻烦而且容易遗漏组件,还会对后面的动态组件扩展带来麻烦 , 对于这个问题,Tomcat 的设计者提供了一个解决方案,用Lifecycle管理启动,停止 ,关闭。
Tomcat 内部架构中各个核心组件有包含与被包含的关系,例如 , Server 包含Service , Service 包含Container 和Connector ,往下一层一层的包含,Tomcat 就是以容器的方式来组织整个系统的构架,就像数据结构的树, 树的根结点没有父亲节点,其他节点有且仅有一个父亲节点,每个父亲节点有零个或多个子节点,对于此,可以通过父容器启动它的子容器,这样只要启动容器,即可把其他的容器都启动,达到统一启动,停止,关闭的效果 。
作为统一的接口,Lifecycle把所有的启动,停止,关闭,生命周期相关的方法都组织在一起,就可以很方便的管理Tomcat各个容器组件的生命周期,下面是Lifecycle 接口的详细定义 。
public interface Lifecycle {
public static final String BEFORE_INIT_EVENT = "before_init";
public static final String AFTER_INIT_EVENT = "after_init";
public static final String START_EVENT = "start";
public static final String BEFORE_START_EVENT = "before_start";
public static final String AFTER_START_EVENT = "after_start";
public static final String STOP_EVENT = "stop";
public static final String BEFORE_STOP_EVENT = "before_stop";
public static final String AFTER_STOP_EVENT = "after_stop";
public static final String AFTER_DESTROY_EVENT = "after_destroy";
public static final String BEFORE_DESTROY_EVENT = "before_destroy";
public static final String PERIODIC_EVENT = "periodic";
public static final String CONFIGURE_START_EVENT = "configure_start";
public static final String CONFIGURE_STOP_EVENT = "configure_stop";
public void addLifecycleListener(LifecycleListener listener);
public LifecycleListener[] findLifecycleListeners();
public void removeLifecycleListener(LifecycleListener listener);
public void init() throws LifecycleException;
public void start() throws LifecycleException;
public void stop() throws LifecycleException;
public void destroy() throws LifecycleException;
public LifecycleState getState();
public String getStateName();
public interface SingleUse {
}
}
从上面可以看出,Lifecycle其实就是定义了一些状态常量和几个方法,这里主要看init ,start , stop ,三个方法,所以需要被生命周期管理的容器都要实现这个接口,并且各自被父容器相应的方法调用,例如,在初始化阶段,根据容器Server组件会调用init()方法,而在init()方法这里会调用它的子容器Service 组件和init方法,以此类推。
Tomcat的初始化到结束,期间必定会经历很多的其他的状态,每一个状态都标志着Tomcat 现在处于什么阶段,另外,事件的触发也通过这些状态来进行判定。
Lifecycle有个返回状态方法getState() ,返回的是LifecycleState枚举类型, 此枚举包含了生命周期的所有状态,供组件之间的转换使用, LifecycleState 类型的详细定义如下 :
public enum LifecycleState {
NEW(false, null),
INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT),
INITIALIZED(false, Lifecycle.AFTER_INIT_EVENT),
STARTING_PREP(false, Lifecycle.BEFORE_START_EVENT),
STARTING(true, Lifecycle.START_EVENT),
STARTED(true, Lifecycle.AFTER_START_EVENT),
STOPPING_PREP(true, Lifecycle.BEFORE_STOP_EVENT),
STOPPING(false, Lifecycle.STOP_EVENT),
STOPPED(false, Lifecycle.AFTER_STOP_EVENT),
DESTROYING(false, Lifecycle.BEFORE_DESTROY_EVENT),
DESTROYED(false, Lifecycle.AFTER_DESTROY_EVENT),
FAILED(false, null),
@Deprecated
MUST_STOP(true, null),
@Deprecated
MUST_DESTROY(false, null);
private final boolean available;
private final String lifecycleEvent;
private LifecycleState(boolean available, String lifecycleEvent) {
this.available = available;
this.lifecycleEvent = lifecycleEvent;
}
public boolean isAvailable() {
return available;
}
public String getLifecycleEvent() {
return lifecycleEvent;
}
}
上述常量从NEW 到DESTROYED 中间经历了生命周期的各个状态,这样就可以把整个生命周期划分为了多个阶段,每个阶段完成的每个阶段的任务 , 假如一个容器调用init() 后,状态的转化为NEW->INITIALIZING->INITIALIZED,其中 INITIALIZING->INITIALIZED是自动变化的,并不需要人为操作,接着调用start() ,状态则变化为INITIALIZED->STARTING_PREP -> STARTING_STARTED , 这个过程也是自动完成的, 接下来,如果调用Stop()方法,状态变化就是STARTED->STOPPING_PREP ->STOPPING-STOPPED。如果在生命周期的某个阶段发生了意外 ,则可能经历xx->DESTROYING->DESTROYED ,整个生命周期内状态的转化相对比较复杂,更多详细的转换情况下图11.1 所示 。

接下来,我们看StandServer的init()方法。
@Override public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { // 初始化之前的事件 before_init setStateInternal(LifecycleState.INITIALIZING, null, false); // 初始化 initInternal(); // 初始化之后的事件处理 after_init setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); setStateInternal(LifecycleState.FAILED, null, false); throw new LifecycleException( sm.getString("lifecycleBase.initFail",toString()), t); } }
我们先看setStateInternal()方法帮我们做了哪些事情。
private synchronized void setStateInternal(LifecycleState state, Object data, boolean check) throws LifecycleException { if (log.isDebugEnabled()) { log.debug(sm.getString("lifecycleBase.setState", this, state)); } if (check) { // Must have been triggered by one of the abstract methods (assume // code in this class is correct) // null is never a valid state if (state == null) { invalidTransition("null"); // Unreachable code - here to stop eclipse complaining about // a possible NPE further down the method return; } // Any method can transition to failed // startInternal() permits STARTING_PREP to STARTING // stopInternal() permits STOPPING_PREP to STOPPING and FAILED to // STOPPING if (!(state == LifecycleState.FAILED || (this.state == LifecycleState.STARTING_PREP && state == LifecycleState.STARTING) || (this.state == LifecycleState.STOPPING_PREP && state == LifecycleState.STOPPING) || (this.state == LifecycleState.FAILED && state == LifecycleState.STOPPING))) { // No other transition permitted invalidTransition(state.name()); } } this.state = state; String lifecycleEvent = state.getLifecycleEvent(); if (lifecycleEvent != null) { fireLifecycleEvent(lifecycleEvent, data); } } protected void fireLifecycleEvent(String type, Object data) { lifecycle.fireLifecycleEvent(type, data); } public void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(lifecycle, type, data); LifecycleListener interested[] = listeners; // ContextCOnfig for (int i = 0; i < interested.length; i++) interested[i].lifecycleEvent(event); }
如果我们面对这么多的状态之间的转换, 我们肯定会有这样的需求 , 我们希望在某某状态的事情发生之前和之后做点什么事情,Tomcat 在这里使用了事件监听器模式来实现这样的功能。 一般来说,事件监听器需要三个参考者。
事件源提供注册事件监听器的方法 , 监听器一般实现java.util.EventListener接口。
事件源提供注册事件监听器的方法,维护多个事件监听器对象,同时可以向事件监听器对象发送事件对象,伴随着事件发生,相应的状态信息都封装在事件对象中,事件源将事件对象发给已经注册的所有事件监听器,这里其实就是调用事件监听器的统一方法,把事件对象作为参数传过去,接着会在这个统一的方法里根据事件对象做出相应的处理。
Tomcat 中的事件监听器也类似 , 如图11.2 所示 。 LifecycleEvent 类就是事件对象,继承了EventObject类,LifecycleListener 为事件监听器接口,里面只定义了一个方法的lifecycleEvent(LifecycleEvent event) ,很明显,LifecycleEvent 作为这个方法的参数,最后缺一个事件源,一般来说,组件和容器就是事件源,Tomcat 提供了一个辅助类LifecycleSupport ,用于帮助管理该组件或容器上的监听器,里面维护了一个监听器数组,并提供了注册,移除,触发监听器等方法 , 这样整个监听器框架就完成了, 假如,想要实现一个监听器功能,比如 XXXLifecycleListener ,只要扩展LifecycleListener 接口并重写里面的LifecycleEvent 方法,然后调用LifecycleSupport的addLifecycleListener 方法注册即可,后面,当发生某些事件时,就可以监听了。

上面这段代码中最重要的就是加粗这一行代码 interested[i].lifecycleEvent(event); ,遍历这个组件的所有监听器,并调用其lifecycleEvent方法。 而type 参数可能是before_init,after_init 或start 等等 。
我们以org.apache.catalina.startup.VersionLoggerListener 为例子,看其lifecycleEvent()方法做了哪些处理。

@Override public void lifecycleEvent(LifecycleEvent event) { if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) { log(); } } private void log() { log.info(sm.getString("versionLoggerListener.serverInfo.server.version", ServerInfo.getServerInfo())); log.info(sm.getString("versionLoggerListener.serverInfo.server.built", ServerInfo.getServerBuilt())); log.info(sm.getString("versionLoggerListener.serverInfo.server.number", ServerInfo.getServerNumber())); log.info(sm.getString("versionLoggerListener.os.name", System.getProperty("os.name"))); log.info(sm.getString("versionLoggerListener.os.version", System.getProperty("os.version"))); log.info(sm.getString("versionLoggerListener.os.arch", System.getProperty("os.arch"))); log.info(sm.getString("versionLoggerListener.java.home", System.getProperty("java.home"))); log.info(sm.getString("versionLoggerListener.vm.version", System.getProperty("java.runtime.version"))); log.info(sm.getString("versionLoggerListener.vm.vendor", System.getProperty("java.vm.vendor"))); log.info(sm.getString("versionLoggerListener.catalina.base", System.getProperty("catalina.base"))); log.info(sm.getString("versionLoggerListener.catalina.home", System.getProperty("catalina.home"))); if (logArgs) { List<String> args = ManagementFactory.getRuntimeMXBean().getInputArguments(); for (String arg : args) { log.info(sm.getString("versionLoggerListener.arg", arg)); } } if (logEnv) { SortedMap<String, String> sortedMap = new TreeMap<String, String>(System.getenv()); for (Map.Entry<String, String> e : sortedMap.entrySet()) { log.info(sm.getString("versionLoggerListener.env", e.getKey(), e.getValue())); } } if (logProps) { SortedMap<String, String> sortedMap = new TreeMap<String, String>(); for (Map.Entry<Object, Object> e : System.getProperties().entrySet()) { sortedMap.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); } for (Map.Entry<String, String> e : sortedMap.entrySet()) { log.info(sm.getString("versionLoggerListener.prop", e.getKey(), e.getValue())); } } }

而同理,setStateInternal(LifecycleState.INITIALIZED, null, false);的实现原理一样,这里就不再赘述 。
上面提到了自己实现事件监听器,那我们自己写一个事件监听器试试 。
public class MyTestLifecycleListener implements LifecycleListener {
private static final Log log = LogFactory.getLog(VersionLoggerListener.class);
@Override
public void lifecycleEvent(LifecycleEvent event) {
log.info("MyTestLifecycleListener type = " + event.getType() + ", data = " + event.getData());
}
}
在server.xml 文件中添加事件监听器。

启动tomcat

我们可以根据当前处于不同的状态做相应的处理。
接下来,我们来看StandardServer的initInternal的实现逻辑 。
@Override
protected void initInternal() throws LifecycleException {
super.initInternal(); // 将StandardServer实例注册到jmx
// Register global String cache
// Note although the cache is global, if there are multiple Servers
// present in the JVM (may happen when embedding) then the same cache
// will be registered under multiple names
// 每个Server下都有一个全局的StringCache
onameStringCache = register(new StringCache(), "type=StringCache");
// Register the MBeanFactory
// MBeanFactory是JMX中用来管理Server的一个对象,通过MBeanFactory可以创建、移除Connector、Host等待
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");
// Register the naming resources
globalNamingResources.init();
// Populate the extension validator with JARs from common and shared
// class loaders
// 将common和shared类加载器中的jar包中的清单文件添加到容器的清单文件资源池中
if (getCatalina() != null) {
ClassLoader cl = getCatalina().getParentClassLoader();
// Walk the class loader hierarchy. Stop at the system class loader.
// This will add the shared (if present) and common class loaders
while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
for (URL url : urls) {
if (url.getProtocol().equals("file")) {
try {
File f = new File (url.toURI());
if (f.isFile() &&
f.getName().endsWith(".jar")) {
ExtensionValidator.addSystemResource(f);
}
} catch (URISyntaxException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
}
}
}
cl = cl.getParent();
}
}
// Initialize our defined Services
// service初始化
for (int i = 0; i < services.length; i++) {
services[i].init();
}
}
接下来,我们来看StandardServer的initInternal的实现逻辑也很简单,直接调用所有配置了globalNamingResources的init()方法,以及配置的所有service标签的init()方法 ,我们再进一步,看service的init()方法做了哪些事情 。 进入StandardService的init()方法 ,发现StandardService并没有实现init()方法,而是使用了父类继承的init()方法 。 init(),然后又是走了setStateInternal() , initInternal() , setStateInternal()方法,之前分析过setStateInternal()方法,这里就不再赘述,进入StandardService的initInternal()方法 。
@Override
protected void initInternal() throws LifecycleException {
super.initInternal(); // 将StandardService注册到jmx中
// 将Service下的容器进行初始化,默认情况下是StandardEngine
if (container != null) {
container.init(); // 注意:这里是Engine,这个流程只会初始化StandardEngine,并没有去初始话Engine下的Host,那么Host是在哪初始化的呢?
// 实际上,对于Host容器,并不需要进行初始化
}
// Initialize any Executors
// 初始化线程池
// 可以在Service下配置定义executor,默认实现类为org.apache.catalina.core.StandardThreadExecutor
// 这个初始化只是走了一下生命周期的初始化流程,没有其他作用
for (Executor executor : findExecutors()) {
if (executor instanceof LifecycleMBeanBase) {
((LifecycleMBeanBase) executor).setDomain(getDomain());
}
executor.init();
}
// Initialize our defined Connectors
// 初始化连接器
// 为什么这里要同步,而上面的container和executor不同步?
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}
protected void initInternal() throws LifecycleException {
// If oname is not null then registration has already happened via
// preRegister().
if (oname == null) {
mserver = Registry.getRegistry(null, null).getMBeanServer();
oname = register(this, getObjectNameKeyProperties());
}
}
看到没有,先是container初始化,再是executor和connector初始化 。
接下来,我们来看Bootstrap的start()方法 。
public void start()
throws Exception {
if( catalinaDaemon==null ) init();
Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
method.invoke(catalinaDaemon, (Object [])null);
}
而启动,也是通过反射调用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");
}
// 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);
}
}
// 是否需要阻塞,await标记是在通过Bootstrap类启动时设置为true的
if (await) { // true
await(); // 使用ServerSocket来监听shutdown命令来阻塞
stop(); // 如果阻塞被解开,那么开始停止流程
}
}
启动过程中,最重要的是加粗这一行代码。 接下来,我们进入start()方法 。
public final synchronized void start() throws LifecycleException {
// 如果容器已经启动,则不再调用启动过程,可能我们在命令行中调用两次start命令
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}
return;
}
// 如果还没有初始化,则先进行初始化
if (state.equals(LifecycleState.NEW)) {
init();
// 如果启动失败,调用stop销毁方法
} else if (state.equals(LifecycleState.FAILED)) {
stop();
// 如果当前状态既不是before_start, start, after_start,
// 也不是 after_init ,after_stop,
// 也就是当前状态为before_init, 或before_stop,如果容器正在初始化,或容器正在停止,但是还没有停止完全 , 则抛出异常
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
// 发送启动前事件
setStateInternal(LifecycleState.STARTING_PREP, null, false);
startInternal();
// 如果启动失败,则销毁
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
// 如果启动失败,则抛出异常
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
// 发出启动完成事件
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t);
}
}
上面的实现套路和init()方法一样, 都是从父组件向下调用,一级一级,最终达到整个容器初始化,启动,停止,销毁的效果 。
同样启动和步骤也是通过类似的调用机制实现统一的启动,统一的关闭,至此,我们对Tomcat 生命周期的统一初始化,启动,关闭机制有了比较清晰的认识。
其实Tomcat的整个启动和停止的过程,就像政府部分发布政策一样,先由中央发布,一级一级的向下传递,最终落实到乡镇,假如新增加一个乡镇,只需要将其并到他的上一级,并且设置相同的机构,那么中央下达的指令,依然会到达这个乡镇,和其他乡镇相同的反映机制,因此,Tomcat 也像一个宠大的组织机构,所有的子组件和父亲组件有一样的步调,当父组件init()时,子组件也init() , 父组件stop时,子组件也stop。
在这篇博客中,我们并没有具体去分析某个组件的具体功能,而是从宏观的角度去分析了一下Tomcat 初始化,启动,停止(实现套路一样)。希望对你有所帮助 。
tomcat 的github地址: https://github.com/quyixiao/tomcat
手动将lib包加入到Libraries中,就可以启动Tomcat 了
