我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

前言

  熟悉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;;
esac

# 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'
  fi
done

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

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'
    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的类加载器,并设置线程上下文类加载器(具体实现细节,读者可以参考《TOMCAT源码分析——类加载体系》一文); 用反射实例化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)的执行步骤如下:

验证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.
     */
    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)的执行步骤如下:

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

代码清单9

    public void await() {
        // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
        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的命令如下:

sh shutdown.sh

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

代码清单10

os400=false
case '`uname`' in
OS400*) os400=true;;
esac

# 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'
  fi
done

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

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.
     */
    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容器获取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)的实现,其执行步骤如下:

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

代码清单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
                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进程作为socket客户端向服务端发送shutdown命令,两个进程通过共享server.xml里Server标签的端口以及地址信息打通了socket的通信。

我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为1000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版百度优化、名注册、主机空间、手机网站建设公众号开发小程序制作、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线: 13820372851,我们会详细为你一一解答你心中的疑难。项目经理在线

我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

在线客服
联系方式

热线电话

13820372851

上班时间

周一到周五

公司电话

022-26262675

二维码
线
在线留言