Tomcat运行脚本及启动过程分析--catalina.sh

一、概述

作为一个成熟的中间件产品,tomcat有很多值得我们学习的地方。下面我将从tomcat的运行脚本catalina.sh入手,分析tomcat的启动过程,以及tomcat脚本是如何实现停止tomcat服务的。

注意:此处分析的是linux环境下的脚本文件

二、tomcat目录结构

tomcat目录结构如下:

bin目录的内容如下:

  • 可执行文件,包括startup.sh、shutdown.sh、catalina.sh
  • tomcat启动所依赖的jar包,包括 bootstrap.jar、tomcat-juli.jar

三、脚本分析

我会介绍每个脚本的主要逻辑,并对涉及到的linux命令做简要介绍。脚本中做了大部分的注释,请结合注释理解。毕竟是代码,干说是很难描述的,注释效果更好。

startup.sh

#!/bin/sh

# 1)检测是否为os400操作系统
os400=false
case "`uname`" in
OS400*) os400=true;;
esac

# 2)PRG表示脚本路径,如果当前脚本文件为软链接,则会解析出PRG真正文件所在路径
# resolve links - $0 may be a softlink
PRG="$0"

while [ -h "$PRG" ] ; do # -h 判断是否为软链接
  ls=`ls -ld "$PRG"` # 如果为软链接,输出中含有 link -> source 的字串
  link=`expr "$ls" : '.*-> \(.*\)$'` # 模式匹配出源文件的路径,对这里感觉模糊请搜索“expr模式匹配”
  if expr "$link" : '/.*' > /dev/null; then # 匹配 /.*,这里expr会输出匹配个数,如果不为0,说明$link包含目录
    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

# 3)执行catalina.sh start
exec "$PRGDIR"/"$EXECUTABLE" start "$@"

脚本主要逻辑如下:

1. 检测操作系统,以兼容特定操作系统的特性

  • uname 显示操作系统信息

2. 获取脚本路径

这段代码几乎在所有启动脚本、停止脚本的开头都会出现。为了防止获取到的路径是软链接的路径,需要对路径做解析。

  • expr命令解释

1)expr  string : regex

       这种形式下,会用:后面的正则表达式匹配前面的字符串,输出匹配的个数

2)expr string : xxx\(zzz\ )

      这种形式下,\(zzz\)中,zzz匹配的字符串会被输出

3.执行catalina.sh start

总结:startup.sh 调用 catalina.sh start

shutdown.sh

逻辑和startup.sh一样,最终调用 catalina.sh stop

catalina.sh

从上面的分析我们知道,其他的脚本最终会以不同的参数调用catalina脚本,那么,catalina脚本的主要逻辑是什么呢?

由于catalina.sh脚本较长,截取一些片段来分析:

1.检测操作系统

2.获取脚本路径$PRG

3.设置两个重要的环境变量,CATALINA_HOME、CATALINA_BASE

一般情况下,这两个变量的值相同,都是tomcat根目录

# Get standard environment variables
PRGDIR=`dirname "$PRG"`

# Only set CATALINA_HOME if not already set
[ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd` # $CATALINA_HOME即tomcat根目录

# Copy CATALINA_BASE from CATALINA_HOME if not already set
[ -z "$CATALINA_BASE" ] && CATALINA_BASE="$CATALINA_HOME" # $CATALINA_BASE等同$CATALINA_HOME

4.设置CLASSPATH变量

#在当前shell环境执行setenv.sh,设置CLASSPATH环境变量,setenv.sh默认不存在,如果用户需要添加额外的CLASSPATH,在这个文件添加
if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then
  . "$CATALINA_BASE/bin/setenv.sh"
elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then
  . "$CATALINA_HOME/bin/setenv.sh"
fi

5.在CLASSPATH后追加Bootstrap.jar、Tomcat-juli.jar

# Add on extra jar files to CLASSPATH
if [ ! -z "$CLASSPATH" ] ; then
  CLASSPATH="$CLASSPATH":
fi
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar #将bootstrap.jar作为CLASSPATH

# Add tomcat-juli.jar to classpath
# tomcat-juli.jar can be over-ridden per instance
if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
  CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar #添加tomcat-juli.jar到classpath
else
  CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
fi

6.解析脚本参数,执行Bootstrap类的main方法,并传入相应的参数

  • 参数为start的情况下

java命令执行Bootstrap类的main方法,将start作为参数传入

    eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
      -D$ENDORSED_PROP="\"$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 "&"
  • 参数为stop的情况下

java命令执行Bootstrap类的main方法,将stop作为参数传入

至此,整个过程结束。

脚本中多次出现对$CATALINA_PID这个文件的操作,这里简单介绍一下这个文件的作用:

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

  if [ ! -z "$CATALINA_PID" ]; then
    if [ -f "$CATALINA_PID" ]; then
      if [ -s "$CATALINA_PID" ]; then
        echo "Existing PID file found during start."
        if [ -r "$CATALINA_PID" ]; then
          PID=`cat "$CATALINA_PID"`
          ps -p $PID >/dev/null 2>&1
          if [ $? -eq 0 ] ; then
            echo "Tomcat appears to still be running with PID $PID. Start aborted."
            echo "If the following process is not a Tomcat process, remove the PID file and try again:"
            ps -f -p $PID
            exit 1
          else
            echo "Removing/clearing stale PID file."
            rm -f "$CATALINA_PID" >/dev/null 2>&1
            if [ $? != 0 ]; then
              if [ -w "$CATALINA_PID" ]; then
                cat /dev/null > "$CATALINA_PID"
              else
                echo "Unable to remove or clear stale PID file. Start aborted."
                exit 1
              fi
            fi
          fi
        else
          echo "Unable to read PID file. Start aborted."
          exit 1
        fi
      else
        rm -f "$CATALINA_PID" >/dev/null 2>&1
        if [ $? != 0 ]; then
          if [ ! -w "$CATALINA_PID" ]; then
            echo "Unable to remove or write to empty PID file. Start aborted."
            exit 1
          fi
        fi
      fi
    fi
  fi

1.$CATALINA变量默认tomcat没有启用,用户如果要用,就要在脚本中自己定义该变量,他表示的记录tomcat进程id的文件路径

2.start时,会先检测$CATALINA文件是否存在,如果存在并且内容不为空,说明tomcat进程已经启动,则启动失败

3.start成功,如果定义了$CATALINA变量,则将进程id写入该文件

4.stop时,先执行 Bootstrap.main stop,如果不成功并且$CATALINA存在,则尝试使用kill命令杀死进程

5.stop成功,如果定义了$CATALINA变量,则删除$CATALINA文件

四、tomcat源码之Bootstrap与Catalina分析

由catalina.sh我们知道,启动和停止tomcat都是以Bootstrap类作为主类运行。

这里,我们主要关注一个问题:

执行Bootstrap stop,为何可以停止另一个进程中的tomcat呢?要知道每次执行脚本中的java命令,我们都启动了一个新的进程。我们下面源码的分析只关注这个问题。

Bootstrap main方法

以下是main方法代码片段:

        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 if (command.equals("configtest")) {
                daemon.load(args);
                if (null==daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

主要注意“start”和“stop"的部分,在Bootstrap中对daemon变量的操作,最终都变为对Catalina的操作,方法名映射是一致的。那么我们主要关注的是Catalina.start()Catalina.stopServer()方法了。

Catalina.start

这里主要是调用了Server.start,其中我们要注意的片段是:

        if (await) {
            await();
            stop();
        }

我们可以大致猜到代码的意思:等待,等待结束则执行stop()。

到底在等待什么呢?看StandardServer.await,谜团解开了:

        // Set up a server socket to wait on
        try {
            awaitSocket = new ServerSocket(port, 1,
                    InetAddress.getByName(address));
        } catch (IOException e) {
            log.error("StandardServer.await: create[" + address
                               + ":" + port
                               + "]: ", e);
            return;
        }

此处建立了ServerSocket,监听的端口是 conf/server.xml中配置的shutdown端口,默认是8005,当该端口接收到"SHUTDOWN"请求,await()结束,进而执行stop()方法。

server.xml中shutdown端口:

<Server port="8005" shutdown="SHUTDOWN">

通过网络通信的方式,tomcat实现了从一个Java进程关闭另一个Java进程。

Catalina.stopServer

从对Catalina.start的分析,我们知道这个方法会向另一个Java进程发送shutdown命令,验证我们的假设:

Catalina.stopServer代码片段:

// Stop the existing server
        s = getServer();
        if (s.getPort()>0) {
            Socket socket = null;
            OutputStream stream = null;
            try {
                socket = new Socket(s.getAddress(), s.getPort());
                stream = socket.getOutputStream();
                String shutdown = s.getShutdown();
                for (int i = 0; i < shutdown.length(); i++) {
                    stream.write(shutdown.charAt(i));
                }
                stream.flush();
            } catch (ConnectException ce) {
                log.error(sm.getString("catalina.stopServer.connectException",
                                       s.getAddress(),
                                       String.valueOf(s.getPort())));
                log.error("Catalina.stop: ", ce);
                System.exit(1);
            } catch (IOException e) {
                log.error("Catalina.stop: ", e);
                System.exit(1);
            } finally {
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException e) {
                        // Ignore
                    }
                }
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        // Ignore
                    }
                }
            }

五、总结

我们分析了tomcat是如何通过catalina.sh脚本启动和停止tomcat进程的,而catalina.sh的逻辑基本上适用任何的Java程序。

Java程序的脚本,最终的逻辑基本上是调用java命令执行Java程序,总结几个注意的点:

1.脚本路径的获取

2.CLASSPATH的获取

3.PID文件记录启动的Java进程id

4.如何停止Java进程

  • 通过网络通信,如Socket向正在运行的进程发送shutdown命令,Java进程根据收到的消息决定是否关闭进程。
  • 通过kill命令,强制杀死进程

注意:通过Runtime.getRuntime().addShutdownHook设置钩子线程,可相应kill命令,从而在关闭进程前执行有效的清理工作。

猜你喜欢

转载自blog.csdn.net/qq_21508059/article/details/82713797
今日推荐