总结

王朝学院·作者佚名  2016-08-27
窄屏简体版  字體: 小  |  中  |  大  |  超大  

前言熟悉Tomcat的工程师们,肯定都知道Tomcat是如何启动与停止的。对于startup.sh、startup.bat、shutdown.sh、shutdown.bat等脚本或者批处理命令,大家一定知道改如何使用它,但是它们究竟是如何实现的,尤其是shutdown.sh脚本(或者shutdown.bat)究竟是如何和Tomcat进程通信的呢?本文将通过对Tomcat7.0的源码阅读,深入剖析这一过程。

由于在生产环境中,Tomcat一般部署在linux系统下,所以本文将以startup.sh和shutdown.sh等shell脚本为准,对Tomcat的启动与停止进行分析。

启动过程分析我们启动Tomcat的命令如下:

sh startup.sh

所以,将从shell脚本startup.sh开始分析Tomcat的启动过程。startup.sh的脚本代码见代码清单1。

代码清单1

os400=falsecase"`uname`"inOS400*) os400=true;;esac# resolve links- $0may be a softlinkPRG="$0"while[ -h"$PRG"] ;dols=`ls-ld"$PRG"`

link=`expr"$ls":'.*-> \(.*\)$'`ifexpr"$link":'/.*'> /dev/null;thenPRG="$link"elsePRG=`dirname"$PRG"`/"$link"fidonePRGDIR=`dirname"$PRG"`

EXECUTABLE=catalina.sh# Check that target executable existsif$os400;then#-x will Only work on the os400ifthe files are:

#1. owned by the user

#2. owned by the PRIMARY group of the user

# this will not workifthe user belongsinsecondarygroupsevalelseif[ ! -x"$PRGDIR"/"$EXECUTABLE"];thenecho"Cannot find $PRGDIR/$EXECUTABLE"echo"The file is absent or does not have execute permission"echo"This file is needed to run this program"exit1fifiexec"$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# 此处省略参数校验的脚本shifttouch"$CATALINA_OUT"if["$1"="-security"] ;thenif[ $have_tty -eq1];thenecho"Using Security Manager"fishifteval"\"$_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"&"elseeval"\"$_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"&"fiif[ ! -z"$CATALINA_PID"];thenecho$! >"$CATALINA_PID"fiecho"Tomcat started."

从代码清单2可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数也是start。Bootstrap的main方法的实现见代码清单3。

代码清单3

/*** Main method, used for testing only.

*

*@paramargs Command line arguments to be processed*/publicstaticvoidmain(String args[]) {if(daemon ==null) {//Don't set daemon until init() has completedBootstrap bootstrap =newBootstrap();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();

}elseif(command.equals("stopd")) {

args[args.length- 1] = "stop";

daemon.stop();

}elseif(command.equals("start")) {

daemon.setAwait(true);

daemon.load(args);

daemon.start();

}elseif(command.equals("stop")) {

daemon.stopServer(args);

}else{

log.warn("Bootstrap: command \"" + command + "\" does not exist.");

}

}catch(Throwable t) {

t.printStackTrace();

}

}

从代码清单3可以看出,当传递参数start的时候,command等于start,此时main方法的执行步骤如下:

步骤一 初始化Bootstrap

Bootstrap的init方法(见代码清单4)的执行步骤如下:

设置Catalina路径,默认为Tomcat的根目录;初始化Tomcat的类加载器,并设置线程上下文类加载器(具体实现细节,读者可以参考《TOMCAT源码分析——类加载体系》一文);用反射实例化org.apache.catalina.startup.Catalina对象,并且使用反射调用其setParentClassLoader方法,给Catalina对象设置Tomcat类加载体系的顶级加载器(Java自带的三种类加载器除外)。代码清单4

/*** Initialize daemon.*/publicvoidinit()throwsException

{//Set Catalina pathsetCatalinaHome();

setCatalinaBase();

initClassLoaders();

Thread.currentThread().setContextClassLoader(catalinaLoader);

SecurityClassLoad.securityClassLoad(catalinaLoader);//Load our startup class and call its process() methodif(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 loaderif(log.isDebugEnabled())

log.debug("Setting startup class properties");

String methodName= "setParentClassLoader";

Class<?> paramTypes[] =newClass[1];

paramTypes[0] = Class.forName("java.lang.ClassLoader");

Object paramValues[]=newObject[1];

paramValues[0] =sharedLoader;

Method method=startupInstance.getClass().getMethod(methodName, paramTypes);

method.invoke(startupInstance, paramValues);

catalinaDaemon=startupInstance;

}

步骤二 加载、解析server.xml配置文件

当传递参数start的时候,会调用Bootstrap的load方法(见代码清单5),其作用是用反射调用catalinaDaemon(类型是Catalina)的load方法加载和解析server.xml配置文件,具体细节已在《TOMCAT源码分析——SERVER.XML文件的加载与解析》一文中详细介绍,有兴趣的朋友可以选择阅读。

代码清单5

/*** Load daemon.*/privatevoidload(String[] arguments)throwsException {//Call the load() methodString methodName = "load";

Object param[];

Class<?>paramTypes[];if(arguments==null|| arguments.length==0) {

paramTypes=null;

param=null;

}else{

paramTypes=newClass[1];

paramTypes[0] =arguments.getClass();

param=newObject[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.*/publicvoidstart()throwsException {if( catalinaDaemon==null) init();

Method method= catalinaDaemon.getClass().getMethod("start", (Class [] )null);

method.invoke(catalinaDaemon, (Object [])null);

}

Catalina的start方法(见代码清单7)的执行步骤如下:

验证Server容器是否已经实例化。如果没有实例化Server容器,还会再次调用Catalina的load方法加载和解析server.xml,这也说明Tomcat只允许Server容器通过配置在server.xml的方式生成,用户也可以自己实现Server接口创建自定义的Server容器以取代默认的StandardServer。启动Server容器,有关容器的启动过程的分析可以参考《TOMCAT源码分析——生命周期管理》一文的内容。设置关闭钩子。这么说可能有些不好理解,那就换个说法。Tomcat本身可能由于所在机器断点,程序bug甚至内存溢出导致进程退出,但是Tomcat可能需要在退出的时候做一些清理工作,比如:内存清理、对象销毁等。这些清理动作需要封装在一个Thread的实现中,然后将此Thread对象作为参数传递给Runtime的addShutdownHook方法即可。最后调用Catalina的await方法循环等待接收Tomcat的shutdown命令。如果Tomcat运行正常且没有收到shutdown命令,是不会向下执行stop方法的,当接收到shutdown命令,Catalina的await方法会退出循环等待,然后顺序执行stop方法停止Tomcat。代码清单7

/*** Start a new server instance.*/publicvoidstart() {if(getServer() ==null) {

load();

}if(getServer() ==null) {

log.fatal("Cannot start server. Server instance is not configured.");return;

}longt1 =System.nanoTime();//Start the new servertry{

getServer().start();

}catch(LifecycleException e) {

log.error("Catalina.start: ", e);

}longt2 =System.nanoTime();if(log.isInfoEnabled())

log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");try{//Register shutdown hookif(useShutdownHook) {if(shutdownHook ==null) {

shutdownHook=newCatalinaShutdownHook();

}

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(logManagerinstanceofClassLoaderLogManager) {

((ClassLoaderLogManager) logManager).setUseShutdownHook(false);

}

}

}catch(Throwable t) {//This will fail on JDK 1.2. Ignoring, as Tomcat can run//fine without the shutdown hook.}if(await) {

await();

stop();

}

}

Catalina的await方法(见代码清单8)实际只是代理执行了Server容器的await方法。

代码清单8

/*** Await and shutdown.*/publicvoidawait() {

getServer().await();

}

以Server的默认实现StandardServer为例,其await方法(见代码清单9)的执行步骤如下:

创建socket连接的服务端对象ServerSocket;循环等待接收客户端发出的命令,如果接收到的命令与SHUTDOWN匹配(由于使用了equals,所以shutdown命令必须是大写的),那么退出循环等待。代码清单9

publicvoidawait() {//Negative values - don't wait on port - tomcat is embedded or we just don't like ports gjaif( 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 onServerSocket serverSocket =null;try{

serverSocket=newServerSocket(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 commandwhile(true) {//Wait for the next connectionSocket socket =null;

InputStream stream=null;try{

socket=serverSocket.accept();

socket.setSoTimeout(10 * 1000);//Ten secondsstream =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 socketStringBuilder command =newStringBuilder();intexpected = 1024;//Cut off to avoid DoS attackwhile(expected <shutdown.length()) {if(random ==null)

random=newRandom();

expected+= (random.nextInt() % 1024);

}while(expected > 0) {intch = -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 ittry{

socket.close();

}catch(IOException e) {//Ignore}//Match against our command stringbooleanmatch =command.toString().equals(shutdown);if(match) {

log.info(sm.getString("standardServer.shutdownViaPort"));

break;

}elselog.warn("StandardServer.await: Invalid command '" +command.toString()+ "' received");

}//Close the server socket and returntry{

serverSocket.close();

}catch(IOException e) {//Ignore}

}

至此,Tomcat启动完毕。很多人可能会问,执行sh shutdown.sh脚本时,是如何与Tomcat进程通信的呢?如果要与Tomcat的ServerSocket通信,socket客户端如何知道服务端的连接地址与端口呢?下面会慢慢说明。

停止过程分析我们停止Tomcat的命令如下:

sh shutdown.sh

所以,将从shell脚本shutdown.sh开始分析Tomcat的停止过程。shutdown.sh的脚本代码见代码清单10。

代码清单10

os400=falsecase"`uname`"in

OS400*) os400=true;;

esac

# resolve links- $0may be a softlink

PRG="$0"while[ -h "$PRG" ] ;dols=`ls -ld "$PRG"`

link=`expr "$ls" : '.*-> \(.*\)$'`ifexpr "$link" : '/.*' > /dev/null; then

PRG="$link"elsePRG=`dirname "$PRG"`/"$link"fi

done

PRGDIR=`dirname "$PRG"`

EXECUTABLE=catalina.sh

# Check that target executable existsif$os400; then

#-x will Only work on the os400ifthe files are:

#1. owned by the user

#2. owned by the PRIMARY group of the user

#thiswill not workifthe user belongs in secondary groups

evalelseif[ ! -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"exit1fi

fi

exec"$PRGDIR"/"$EXECUTABLE" stop "$@"

代码清单10和代码清单1非常相似,其中也有两个主要的变量,分别是:

PRGDIR:当前shell脚本所在的路径;EXECUTABLE:脚本catalina.sh。根据最后一行代码:exec "$PRGDIR"/"$EXECUTABLE" stop "$@",我们知道执行了shell脚本catalina.sh,并且传递参数stop。catalina.sh中接收到stop参数后的执行的脚本分支见代码清单11。

代码清单11

elif [ "$1" = "stop"] ; then

#省略参数校验脚本

eval"\"$_RUNJAVA\""$LOGGING_MANAGER $JAVA_OPTS \-Djava.endorsed.dirs="\"$JAVA_ENDORSED_DIRS\"" -classpath "\"$CLASSPATH\""\-Dcatalina.base="\"$CATALINA_BASE\""\-Dcatalina.home="\"$CATALINA_HOME\""\-Djava.io.tmpdir="\"$CATALINA_TMPDIR\""\

org.apache.catalina.startup.Bootstrap"$@" stop

从代码清单11可以看出,最终使用java命令执行了org.apache.catalina.startup.Bootstrap类中的main方法,参数是stop。从代码清单3可以看出,当传递参数stop的时候,command等于stop,此时main方法的执行步骤如下:

步骤一 初始化Bootstrap

已经在启动过程分析中介绍, 不再赘述。

步骤二 停止服务

通过调用Bootstrap的stopServer方法(见代码清单12)停止Tomcat,其实质是用反射调用catalinaDaemon(类型是Catalina)的stopServer方法。

代码清单12

/*** Stop the standalone server.*/publicvoidstopServer(String[] arguments)throwsException {

Object param[];

Class<?>paramTypes[];if(arguments==null|| arguments.length==0) {

paramTypes=null;

param=null;

}else{

paramTypes=newClass[1];

paramTypes[0] =arguments.getClass();

param=newObject[1];

param[0] =arguments;

}

Method method=catalinaDaemon.getClass().getMethod("stopServer", paramTypes);

method.invoke(catalinaDaemon, param);

}

Catalina的stopServer方法(见代码清单13)的执行步骤如下:

创建Digester解析server.xml文件(此处只解析<Server>标签),以构造出Server容器(此时Server容器的子容器没有被实例化);从实例化的Server容器获取Server的socket监听端口和地址,然后创建Socket对象连接启动Tomcat时创建的ServerSocket,最后向ServerSocket发送SHUTDOWN命令。根据代码清单9的内容,ServerSocket循环等待接收到SHUTDOWN命令后,最终调用stop方法停止Tomcat。代码清单13

publicvoidstopServer() {

stopServer(null);

}publicvoidstopServer(String[] arguments) {if(arguments !=null) {

arguments(arguments);

}if( getServer() ==null) {//Create and execute our DigesterDigester digester =createStopDigester();

digester.setClassLoader(Thread.currentThread().getContextClassLoader());

File file=configFile();try{

InputSource is=newInputSource("file://" +file.getAbsolutePath());

FileInputStream fis=newFileInputStream(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 servertry{if(getServer().getPort()>0) {

Socket socket=newSocket(getServer().getAddress(),

getServer().getPort());

OutputStream stream=socket.getOutputStream();

String shutdown=getServer().getShutdown();for(inti = 0; i < shutdown.length(); i++)

stream.write(shutdown.charAt(i));

stream.flush();

stream.close();

socket.close();

}else{

log.error(sm.getString("catalina.stopServer"));

System.exit(1);

}

}catch(IOException e) {

log.error("Catalina.stop: ", e);

System.exit(1);

}

}

最后,我们看看Catalina的stop方法(见代码清单14)的实现,其执行步骤如下:

将启动过程中添加的关闭钩子移除。Tomcat启动过程辛辛苦苦添加的关闭钩子为什么又要去掉呢?因为关闭钩子是为了在JVM异常退出后,进行资源的回收工作。主动停止Tomcat时调用的stop方法里已经包含了资源回收的内容,所以不再需要这个钩子了。停止Server容器。有关容器的停止内容,请阅读《TOMCAT源码分析——生命周期管理》一文。代码清单14

/*** Stop an existing server instance.*/publicvoidstop() {try{//Remove the ShutdownHook first so that server.stop()//doesn't get invoked twiceif(useShutdownHook) {

Runtime.getRuntime().removeShutdownHook(shutdownHook);//If JULI is being used, re-enable JULI's shutdown to ensure//log messages are not lost jiaanLogManager logManager =LogManager.getLogManager();if(logManagerinstanceofClassLoaderLogManager) {

((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 servertry{

getServer().stop();

}catch(LifecycleException e) {

log.error("Catalina.stop", e);

}

}

总结通过对Tomcat源码的分析我们了解到Tomcat的启动和停止都离不开org.apache.catalina.startup.Bootstrap。当停止Tomcat时,已经启动的Tomcat作为socket服务端,停止脚本启动的Bootstrap进程作为socket客户端向服务端发送shutdown命令,两个进程通过共享server.xml里Server标签的端口以及地址信息打通了socket的通信。

如需转载,请标明本文作者及出处——作者:jiaan.gja,本文原创首发:博客园,原文链接:http://www.cnblogs.com/jiaan-geng/p/4872550.html

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有 導航