zl程序教程

您现在的位置是:首页 >  工具

当前栏目

TOMCAT源码分析——启动服务

Tomcat源码服务 分析 启动
2023-09-14 08:57:29 时间

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

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

启动过程分析

我们启动Tomcat的命令如下:

sh startup.sh

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

代码清单1

os400=false

case "`uname`" in

OS400*) os400=true;;

# resolve links - $0 may be a softlink

PRG="$0"

while [ -h "$PRG" ] ; do

 ls=`ls -ld "$PRG"`

 link=`expr "$ls" : .*- \(.*\)$`

 if expr "$link" : /.* /dev/null; then

 PRG="$link"

 else

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

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

 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

exec "$PRGDIR"/"$EXECUTABLE" start "$@"

代码清单1中有两个主要的变量,分别是:

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

代码清单2

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

# 此处省略参数校验的脚本

 shift

 touch "$CATALINA_OUT"

 if [ "$1" = "-security" ] ; then

 if [ $have_tty -eq 1 ]; then

 echo "Using Security Manager"

 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 " "

 if [ ! -z "$CATALINA_PID" ]; then

 echo $! "$CATALINA_PID"

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

 // Dont 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)的执行步骤如下:

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

 /**

 * Initialize daemon.

 public void init()

 throws Exception

 // Set Catalina path

 setCatalinaHome();

 setCatalinaBase();

 initClassLoaders();

 Thread.currentThread().setContextClassLoader(catalinaLoader);

 SecurityClassLoad.securityClassLoad(catalinaLoader);

 // Load our startup class and call its process() method

 if (log.isDebugEnabled())

 log.debug("Loading startup class");

 Class ? startupClass =

 catalinaLoader.loadClass

 ("org.apache.catalina.startup.Catalina");

 Object startupInstance = startupClass.newInstance();

 // Set the shared extensions class loader

 if (log.isDebugEnabled())

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

 String methodName = "setParentClassLoader";

 Class ? paramTypes[] = new Class[1];

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

 Object paramValues[] = new Object[1];

 paramValues[0] = sharedLoader;

 Method method =

 startupInstance.getClass().getMethod(methodName, paramTypes);

 method.invoke(startupInstance, paramValues);

 catalinaDaemon = startupInstance;

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

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

代码清单5

/**

 * Load daemon.

 private void load(String[] arguments)

 throws Exception {

 // Call the load() method

 String methodName = "load";

 Object param[];

 Class ? paramTypes[];

 if (arguments==null || arguments.length==0) {

 paramTypes = null;

 param = null;

 } else {

 paramTypes = new Class[1];

 paramTypes[0] = arguments.getClass();

 param = new Object[1];

 param[0] = arguments;

 Method method = 

 catalinaDaemon.getClass().getMethod(methodName, paramTypes);

 if (log.isDebugEnabled())

 log.debug("Calling startup class " + method);

 method.invoke(catalinaDaemon, param);

 }
步骤三 启动Tomcat

当传递参数start的时候,调用Bootstrap的load方法之后会接着调用start方法(见代码清单6)启动Tomcat,此方法实际是用反射调用了catalinaDaemon(类型是Catalina)的start方法。

代码清单6

/**

 * Start the Catalina daemon.

 public void start()

 throws Exception {

 if( catalinaDaemon==null ) init();

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

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

 }

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

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

/**

 * Start a new server instance.

 public void start() {

 if (getServer() == null) {

 load();

 if (getServer() == null) {

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

 return;

 long t1 = System.nanoTime();

 // Start the new server

 try {

 getServer().start();

 } catch (LifecycleException e) {

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

 long t2 = System.nanoTime();

 if(log.isInfoEnabled())

 log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");

 try {

 // Register shutdown hook

 if (useShutdownHook) {

 if (shutdownHook == null) {

 shutdownHook = new CatalinaShutdownHook();

 Runtime.getRuntime().addShutdownHook(shutdownHook);

 // If JULI is being used, disable JULIs shutdown hook since

 // shutdown hooks run in parallel and log messages may be lost

 // if JULIs hook completes before the CatalinaShutdownHook()

 LogManager logManager = LogManager.getLogManager();

 if (logManager instanceof ClassLoaderLogManager) {

 ((ClassLoaderLogManager) logManager).setUseShutdownHook(

 false);

 } catch (Throwable t) {

 // This will fail on JDK 1.2. Ignoring, as Tomcat can run

 // fine without the shutdown hook.

 if (await) {

 await();

 stop();

 }

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

代码清单8

/**

 * Await and shutdown.

 public void await() {

 getServer().await();

 }

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

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

public void await() {

 // Negative values - dont wait on port - tomcat is embedded or we just dont like ports gja

 if( port == -2 ) {

 // undocumented yet - for embedding apps that are around, alive.

 return;

 if( port==-1 ) {

 while( true ) {

 try {

 Thread.sleep( 10000 );

 } catch( InterruptedException ex ) {

 if( stopAwait ) return;

 // Set up a server socket to wait on

 ServerSocket serverSocket = null;

 try {

 serverSocket =

 new ServerSocket(port, 1,

 InetAddress.getByName(address));

 } catch (IOException e) {

 log.error("StandardServer.await: create[" + address

 + ":" + port

 + "]: ", e);

 System.exit(1);

 // Loop waiting for a connection and a valid command

 while (true) {

 // Wait for the next connection

 Socket socket = null;

 InputStream stream = null;

 try {

 socket = serverSocket.accept();

 socket.setSoTimeout(10 * 1000); // Ten seconds

 stream = socket.getInputStream();

 } catch (AccessControlException ace) {

 log.warn("StandardServer.accept security exception: "

 + ace.getMessage(), ace);

 continue;

 } catch (IOException e) {

 log.error("StandardServer.await: accept: ", e);

 System.exit(1);

 // Read a set of characters from the socket

 StringBuilder command = new StringBuilder();

 int expected = 1024; // Cut off to avoid DoS attack

 while (expected shutdown.length()) {

 if (random == null)

 random = new Random();

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

 while (expected 0) {

 int ch = -1;

 try {

 ch = stream.read();

 } catch (IOException e) {

 log.warn("StandardServer.await: read: ", e);

 ch = -1;

 if (ch 32) // Control character or EOF terminates loop

 break;

 command.append((char) ch);

 expected--;

 // Close the socket now that we are done with it

 try {

 socket.close();

 } catch (IOException e) {

 // Ignore

 // Match against our command string

 boolean match = command.toString().equals(shutdown);

 if (match) {

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

 break;

 } else

 log.warn("StandardServer.await: Invalid command " +

 command.toString() + " received");

 // Close the server socket and return

 try {

 serverSocket.close();

 } catch (IOException e) {

 // Ignore

 }

至此,Tomcat启动完毕。很多人可能会问,执行sh shutdown.sh脚本时,是如何与Tomcat进程通信的呢?如果要与Tomcat的ServerSocket通信,socket客户端如何知道服务端的连接地址与端口呢?请阅读《TOMCAT源码分析——停止服务》一文。


SpringBoot源码分析系列之四:如何启动内嵌Tomcat SpringBoot相信很多同学都非常了解,实际工作中也经常使用到。但是不知道大家在使用过程中有没有想过一个问题,SpringBoot内嵌tomcat到底是怎么启动的?内嵌tomcat启动服务的好处又是什么呢?本文将结合SpringBoot源码探讨下这些问题。
http://www.cnblogs.com/fangjian0423/p/servletContainer-tomcat-urlPattern.html#springmvc