Tomcat运行脚本及启动过程分析

一、概述

作为一个成熟的中间件产品,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命令,从而在关闭进程前执行有效的清理工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值