前言
熟悉Tomcat的project师们。肯定都知道Tomcat是怎样启动与停止的。
对于startup.sh、startup.bat、shutdown.sh、shutdown.bat等脚本或者批处理命令,大家一定知道改怎样使用它,可是它们到底是怎样实现的,尤其是shutdown.sh脚本(或者shutdown.bat)到底是怎样和Tomcat进程通信的呢?本文将通过对Tomcat7.0的源代码阅读,深入剖析这一过程。
因为在生产环境中。Tomcat一般部署在Linux系统下。所以本文将以startup.sh和shutdown.sh等shell脚本为准,对Tomcat的启动与停止进行分析。
启动过程分析
我们启动Tomcat的命令例如以下:
sh startup.sh
所以,将从shell脚本startup.sh開始分析Tomcat的启动过程。startup.sh的脚本代码见代码清单1。
代码清单1
os400=falsecase "`uname`" inOS400*) os400=true;;esac# resolve links - $0 may be a softlinkPRG="$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" fidonePRGDIR=`dirname "$PRG"`EXECUTABLE=catalina.sh# Check that target executable existsif $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 evalelse 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 fifiexec "$PRGDIR"/"$EXECUTABLE" start "$@"
代码清单1中有两个基本的变量。各自是:
- PRGDIR:当前shell脚本所在的路径。
- EXECUTABLE:脚本catalina.sh。
依据最后一行代码:exec "$PRGDIR"/"$EXECUTABLE" start "$@",我们知道运行了shell脚本catalina.sh,而且传递參数start。catalina.sh中接收到start參数后的运行的脚本分支见代码清单2。
代码清单2
elif [ "$1" = "start" ] ; then # 此处省略參数校验的脚本 shift touch "$CATALINA_OUT" if [ "$1" = "-security" ] ; then if [ $have_tty -eq 1 ]; then echo "Using Security Manager" fi shift eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \ -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \ -Djava.security.manager \ -Djava.security.policy=="\"$CATALINA_BASE/conf/catalina.policy\"" \ -Dcatalina.base="\"$CATALINA_BASE\"" \ -Dcatalina.home="\"$CATALINA_HOME\"" \ -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ org.apache.catalina.startup.Bootstrap "$@" start \ >> "$CATALINA_OUT" 2>&1 "&" else eval "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \ -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \ -Dcatalina.base="\"$CATALINA_BASE\"" \ -Dcatalina.home="\"$CATALINA_HOME\"" \ -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ org.apache.catalina.startup.Bootstrap "$@" start \ >> "$CATALINA_OUT" 2>&1 "&" fi if [ ! -z "$CATALINA_PID" ]; then echo $! > "$CATALINA_PID" fi echo "Tomcat started."
从代码清单2能够看出,终于使用java命令运行了org.apache.catalina.startup.Bootstrap类中的main方法。參数也是start。Bootstrap的main方法的实现见代码清单3。
代码清单3
/** * Main method, used for testing only. * * @param args Command line arguments to be processed */ public static void main(String args[]) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { t.printStackTrace(); return; } daemon = bootstrap; } try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); } else if (command.equals("stop")) { daemon.stopServer(args); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { t.printStackTrace(); } }
从代码清单3能够看出,当传递參数start的时候,command等于start,此时main方法的运行过程例如以下:
步骤一 初始化Bootstrap
Bootstrap的init方法(见代码清单4)的运行过程例如以下:
- 设置Catalina路径,默觉得Tomcat的根文件夹;
- 初始化Tomcat的类载入器,并设置线程上下文类载入器(详细实现细节,读者能够參考一文)。
- 用反射实例化org.apache.catalina.startup.Catalina对象,而且使用反射调用其setParentClassLoader方法,给Catalina对象设置Tomcat类载入体系的顶级载入器(Java自带的三种类载入器除外)。
/** * Initialize daemon. */ public void init() throws Exception { // Set Catalina path setCatalinaHome(); setCatalinaBase(); initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method if (log.isDebugEnabled()) log.debug("Loading startup class"); Class
> startupClass = catalinaLoader.loadClass ("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.newInstance(); // Set the shared extensions class loader if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class<?
> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance; }
步骤二 载入、解析server.xml配置文件
当传递參数start的时候,会调用Bootstrap的load方法(见代码清单5),其作用是用反射调用catalinaDaemon(类型是Catalina)的load方法载入和解析server.xml配置文件,详细细节已在一文中详细介绍。有兴趣的朋友能够选择阅读。
代码清单5
/** * Load daemon. */ private void load(String[] arguments) throws Exception { // Call the load() method String methodName = "load"; Object param[]; Class
> paramTypes[]; if (arguments==null || arguments.length==0) { paramTypes = null; param = null; } else { paramTypes = new Class[1]; paramTypes[0] = arguments.getClass(); param = new Object[1]; param[0] = arguments; } Method method = catalinaDaemon.getClass().getMethod(methodName, paramTypes); if (log.isDebugEnabled()) log.debug("Calling startup class " + method); method.invoke(catalinaDaemon, param); }
步骤三 启动Tomcat
当传递參数start的时候,调用Bootstrap的load方法之后会接着调用start方法(见代码清单6)启动Tomcat。此方法实际是用反射调用了catalinaDaemon(类型是Catalina)的start方法。
代码清单6
/** * Start the Catalina daemon. */ public void start() throws Exception { if( catalinaDaemon==null ) init(); Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null); method.invoke(catalinaDaemon, (Object [])null); }
Catalina的start方法(见代码清单7)的运行过程例如以下:
- 验证Server容器是否已经实例化。假设没有实例化Server容器,还会再次调用Catalina的load方法载入和解析server.xml,这也说明Tomcat仅仅同意Server容器通过配置在server.xml的方式生成。用户也能够自己实现Server接口创建自己定义的Server容器以代替默认的StandardServer。
- 启动Server容器,有关容器的启动过程的分析能够參考一文的内容。
- 设置关闭钩子。
这么说可能有些不好理解,那就换个说法。
Tomcat本身可能因为所在机器断点,程序bug甚至内存溢出导致进程退出,可是Tomcat可能须要在退出的时候做一些清理工作,比方:内存清理、对象销毁等。这些清理动作须要封装在一个Thread的实现中,然后将此Thread对象作为參数传递给Runtime的addShutdownHook方法就可以。
- 最后调用Catalina的await方法循环等待接收Tomcat的shutdown命令。
- 假设Tomcat运行正常且没有收到shutdown命令,是不会向下运行stop方法的。当接收到shutdown命令,Catalina的await方法会退出循环等待,然后顺序运行stop方法停止Tomcat。
/** * Start a new server instance. */ public void start() { if (getServer() == null) { load(); } if (getServer() == null) { log.fatal("Cannot start server. Server instance is not configured."); return; } long t1 = System.nanoTime(); // Start the new server try { getServer().start(); } catch (LifecycleException e) { log.error("Catalina.start: ", e); } long t2 = System.nanoTime(); if(log.isInfoEnabled()) log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms"); try { // Register shutdown hook if (useShutdownHook) { if (shutdownHook == null) { shutdownHook = new CatalinaShutdownHook(); } Runtime.getRuntime().addShutdownHook(shutdownHook); // If JULI is being used, disable JULI's shutdown hook since // shutdown hooks run in parallel and log messages may be lost // if JULI's hook completes before the CatalinaShutdownHook() LogManager logManager = LogManager.getLogManager(); if (logManager instanceof ClassLoaderLogManager) { ((ClassLoaderLogManager) logManager).setUseShutdownHook( false); } } } catch (Throwable t) { // This will fail on JDK 1.2. Ignoring, as Tomcat can run // fine without the shutdown hook. } if (await) { await(); stop(); } }Catalina的await方法(见代码清单8)实际仅仅是代理运行了Server容器的await方法。
/** * Await and shutdown. */ public void await() { getServer().await(); }以Server的默认实现StandardServer为例,其await方法(见代码清单9)的运行过程例如以下:
- 创建socket连接的服务端对象ServerSocket。
- 循环等待接收client发出的命令,假设接收到的命令与SHUTDOWN匹配(因为使用了equals,所以shutdown命令必须是大写的),那么退出循环等待。
public void await() { // Negative values - don't wait on port - tomcat is embedded or we just don't like ports gja if( port == -2 ) { // undocumented yet - for embedding apps that are around, alive. return; } if( port==-1 ) { while( true ) { try { Thread.sleep( 10000 ); } catch( InterruptedException ex ) { } if( stopAwait ) return; } } // Set up a server socket to wait on ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName(address)); } catch (IOException e) { log.error("StandardServer.await: create[" + address + ":" + port + "]: ", e); System.exit(1); } // Loop waiting for a connection and a valid command while (true) { // Wait for the next connection Socket socket = null; InputStream stream = null; try { socket = serverSocket.accept(); socket.setSoTimeout(10 * 1000); // Ten seconds stream = socket.getInputStream(); } catch (AccessControlException ace) { log.warn("StandardServer.accept security exception: " + ace.getMessage(), ace); continue; } catch (IOException e) { log.error("StandardServer.await: accept: ", e); System.exit(1); } // Read a set of characters from the socket StringBuilder command = new StringBuilder(); int expected = 1024; // Cut off to avoid DoS attack while (expected < shutdown.length()) { if (random == null) random = new Random(); expected += (random.nextInt() % 1024); } while (expected > 0) { int ch = -1; try { ch = stream.read(); } catch (IOException e) { log.warn("StandardServer.await: read: ", e); ch = -1; } if (ch < 32) // Control character or EOF terminates loop break; command.append((char) ch); expected--; } // Close the socket now that we are done with it try { socket.close(); } catch (IOException e) { // Ignore } // Match against our command string boolean match = command.toString().equals(shutdown); if (match) { log.info(sm.getString("standardServer.shutdownViaPort")); break; } else log.warn("StandardServer.await: Invalid command '" + command.toString() + "' received"); } // Close the server socket and return try { serverSocket.close(); } catch (IOException e) { // Ignore } }至此,Tomcat启动完成。非常多人可能会问,运行sh shutdown.sh脚本时,是怎样与Tomcat进程通信的呢?假设要与Tomcat的ServerSocket通信。socketclient怎样知道服务端的连接地址与端口呢?以下会慢慢说明。
停止过程分析
sh shutdown.sh所以,将从shell脚本shutdown.sh開始分析Tomcat的停止过程。shutdown.sh的脚本代码见代码清单10。
os400=falsecase "`uname`" inOS400*) os400=true;;esac# resolve links - $0 may be a softlinkPRG="$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" fidonePRGDIR=`dirname "$PRG"`EXECUTABLE=catalina.sh# Check that target executable existsif $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 evalelse 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 fifiexec "$PRGDIR"/"$EXECUTABLE" stop "$@"代码清单10和代码清单1非常类似。当中也有两个基本的变量,各自是:
- PRGDIR:当前shell脚本所在的路径;
- EXECUTABLE:脚本catalina.sh。
elif [ "$1" = "stop" ] ; then #省略參数校验脚本 eval "\"$_RUNJAVA\"" $LOGGING_MANAGER $JAVA_OPTS \ -Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\"" \ -Dcatalina.base="\"$CATALINA_BASE\"" \ -Dcatalina.home="\"$CATALINA_HOME\"" \ -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \ org.apache.catalina.startup.Bootstrap "$@" stop从代码清单11能够看出,终于使用java命令运行了org.apache.catalina.startup.Bootstrap类中的main方法,參数是stop。从代码清单3能够看出,当传递參数stop的时候,command等于stop,此时main方法的运行过程例如以下:
步骤一 初始化Bootstrap
步骤二 停止服务
/** * Stop the standalone server. */ public void stopServer(String[] arguments) throws Exception { Object param[]; Class paramTypes[]; if (arguments==null || arguments.length==0) { paramTypes = null; param = null; } else { paramTypes = new Class[1]; paramTypes[0] = arguments.getClass(); param = new Object[1]; param[0] = arguments; } Method method = catalinaDaemon.getClass().getMethod("stopServer", paramTypes); method.invoke(catalinaDaemon, param); }Catalina的stopServer方法(见代码清单13)的运行过程例如以下:
- 创建Digester解析server.xml文件(此处仅仅解析标签),以构造出Server容器(此时Server容器的子容器没有被实例化);
- 从实例化的Server容器获取Server的socket监听端口和地址。然后创建Socket对象连接启动Tomcat时创建的ServerSocket,最后向ServerSocket发送SHUTDOWN命令。依据代码清单9的内容,ServerSocket循环等待接收到SHUTDOWN命令后,终于调用stop方法停止Tomcat。
public void stopServer() { stopServer(null); } public void stopServer(String[] arguments) { if (arguments != null) { arguments(arguments); } if( getServer() == null ) { // Create and execute our Digester Digester digester = createStopDigester(); digester.setClassLoader(Thread.currentThread().getContextClassLoader()); File file = configFile(); try { InputSource is = new InputSource("file://" + file.getAbsolutePath()); FileInputStream fis = new FileInputStream(file); is.setByteStream(fis); digester.push(this); digester.parse(is); fis.close(); } catch (Exception e) { log.error("Catalina.stop: ", e); System.exit(1); } } // Stop the existing server try { if (getServer().getPort()>0) { Socket socket = new Socket(getServer().getAddress(), getServer().getPort()); OutputStream stream = socket.getOutputStream(); String shutdown = getServer().getShutdown(); for (int i = 0; i < shutdown.length(); i++) stream.write(shutdown.charAt(i)); stream.flush(); stream.close(); socket.close(); } else { log.error(sm.getString("catalina.stopServer")); System.exit(1); } } catch (IOException e) { log.error("Catalina.stop: ", e); System.exit(1); } }最后,我们看看Catalina的stop方法(见代码清单14)的实现,其运行过程例如以下:
- 将启动过程中加入的关闭钩子移除。
Tomcat启动过程辛辛苦苦加入的关闭钩子为什么又要去掉呢?因为关闭钩子是为了在JVM异常退出后,进行资源的回收工作。主动停止Tomcat时调用的stop方法里已经包括了资源回收的内容,所以不再须要这个钩子了。
- 停止Server容器。
有关容器的停止内容。请阅读一文。
/** * Stop an existing server instance. */ public void stop() { try { // Remove the ShutdownHook first so that server.stop() // doesn't get invoked twice if (useShutdownHook) { Runtime.getRuntime().removeShutdownHook(shutdownHook); // If JULI is being used, re-enable JULI's shutdown to ensure // log messages are not lost jiaan LogManager logManager = LogManager.getLogManager(); if (logManager instanceof ClassLoaderLogManager) { ((ClassLoaderLogManager) logManager).setUseShutdownHook( true); } } } catch (Throwable t) { // This will fail on JDK 1.2. Ignoring, as Tomcat can run // fine without the shutdown hook. } // Shut down the server try { getServer().stop(); } catch (LifecycleException e) { log.error("Catalina.stop", e); } }
总结
后记:个人总结整理的《深入理解Spark:核心思想与源代码分析》一书如今已经正式出版上市,眼下京东、当当、天猫等站点均有销售。欢迎感兴趣的同学购买。
京东:
当当: