博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Tomcat7.0源代码分析——启动与停止服务原理
阅读量:5952 次
发布时间:2019-06-19

本文共 18498 字,大约阅读时间需要 61 分钟。

前言

  熟悉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)的运行过程例如以下:

  1. 设置Catalina路径,默觉得Tomcat的根文件夹;
  2. 初始化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配置文件,详细细节已在一文中详细介绍。有兴趣的朋友能够选择阅读。

 代码清单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容器,有关容器的启动过程的分析能够參考一文的内容。
  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 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方法。
代码清单8
/**     * Await and shutdown.     */    public void await() {        getServer().await();    }
以Server的默认实现StandardServer为例,其await方法(见代码清单9)的运行过程例如以下:
  1. 创建socket连接的服务端对象ServerSocket。
  2. 循环等待接收client发出的命令,假设接收到的命令与SHUTDOWN匹配(因为使用了equals,所以shutdown命令必须是大写的),那么退出循环等待。

代码清单9
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怎样知道服务端的连接地址与端口呢?以下会慢慢说明。

停止过程分析

我们停止Tomcat的命令例如以下:
sh shutdown.sh
所以,将从shell脚本shutdown.sh開始分析Tomcat的停止过程。shutdown.sh的脚本代码见代码清单10。
代码清单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。

依据最后一行代码: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.     */    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)的运行过程例如以下:
  1. 创建Digester解析server.xml文件(此处仅仅解析标签),以构造出Server容器(此时Server容器的子容器没有被实例化);
  2. 从实例化的Server容器获取Server的socket监听端口和地址。然后创建Socket对象连接启动Tomcat时创建的ServerSocket,最后向ServerSocket发送SHUTDOWN命令。依据代码清单9的内容,ServerSocket循环等待接收到SHUTDOWN命令后,终于调用stop方法停止Tomcat。

代码清单13
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)的实现,其运行过程例如以下:
  1. 将启动过程中加入的关闭钩子移除。

    Tomcat启动过程辛辛苦苦加入的关闭钩子为什么又要去掉呢?因为关闭钩子是为了在JVM异常退出后,进行资源的回收工作。主动停止Tomcat时调用的stop方法里已经包括了资源回收的内容,所以不再须要这个钩子了。

  2. 停止Server容器。

    有关容器的停止内容。请阅读一文。

代码清单14
/**     * 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);        }    }

总结

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

后记:个人总结整理的《深入理解Spark:核心思想与源代码分析》一书如今已经正式出版上市,眼下京东、当当、天猫等站点均有销售。欢迎感兴趣的同学购买。

京东: 

当当: 

转载地址:http://dmaxx.baihongyu.com/

你可能感兴趣的文章
最长公共子序列(POJ1458)
查看>>
TouchJSON的简单使用
查看>>
输入法编辑器(IME)程序设计(3)
查看>>
C/C++中各种类型int、long、double、char表示范围(最大最小值)
查看>>
vbs 中调用shell.application 简单函数
查看>>
应用程序委托和新的单例(译)
查看>>
通用线程 -- sed 实例
查看>>
深入PHP使用技巧之变量
查看>>
Android中如何提取和生成mp4文件
查看>>
水晶报表基础入门——4.水晶报表排序、分组技术
查看>>
Dumping ssl passwords with sslstrip
查看>>
C# Winform编程之Button
查看>>
2-7 StatusStrip 控件
查看>>
CCNP路由重分发(四)EIGRP-to-ISIS
查看>>
巩固shell基础知识
查看>>
C#判断当前运行环境是否64bit
查看>>
RHEL6基础之十二RHEL用户和组基础
查看>>
让Python删除window下文件
查看>>
WCF简单教程(4) 数据契约
查看>>
【新书推荐】Silverlight 4教程书籍推荐
查看>>