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

  1. #!/bin/sh
  2. # 1)检测是否为os400操作系统
  3. os400=false
  4. case "`uname`" in
  5. OS400*) os400=true;;
  6. esac
  7. # 2)PRG表示脚本路径,如果当前脚本文件为软链接,则会解析出PRG真正文件所在路径
  8. # resolve links - $0 may be a softlink
  9. PRG="$0"
  10. while [ -h "$PRG" ] ; do # -h 判断是否为软链接
  11. ls=`ls -ld "$PRG"` # 如果为软链接,输出中含有 link -> source 的字串
  12. link=`expr "$ls" : '.*-> \(.*\)$'` # 模式匹配出源文件的路径,对这里感觉模糊请搜索“expr模式匹配”
  13. if expr "$link" : '/.*' > /dev/null; then # 匹配 /.*,这里expr会输出匹配个数,如果不为0,说明$link包含目录
  14. PRG="$link"
  15. else
  16. PRG=`dirname "$PRG"`/"$link" # 当不包含目录,说明软链接和源文件在同一目录
  17. fi
  18. done
  19. #获取脚本目录路径
  20. PRGDIR=`dirname "$PRG"`
  21. EXECUTABLE=catalina.sh
  22. # Check that target executable exists
  23. if $os400; then
  24. # -x will Only work on the os400 if the files are:
  25. # 1. owned by the user
  26. # 2. owned by the PRIMARY group of the user
  27. # this will not work if the user belongs in secondary groups
  28. eval
  29. else
  30. if [ ! -x "$PRGDIR"/"$EXECUTABLE" ]; then
  31. echo "Cannot find $PRGDIR/$EXECUTABLE"
  32. echo "The file is absent or does not have execute permission"
  33. echo "This file is needed to run this program"
  34. exit 1
  35. fi
  36. fi
  37. # 3)执行catalina.sh start
  38. 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根目录

  1. # Get standard environment variables
  2. PRGDIR=`dirname "$PRG"`
  3. # Only set CATALINA_HOME if not already set
  4. [ -z "$CATALINA_HOME" ] && CATALINA_HOME=`cd "$PRGDIR/.." >/dev/null; pwd` # $CATALINA_HOME即tomcat根目录
  5. # Copy CATALINA_BASE from CATALINA_HOME if not already set
  6. [ -z "$CATALINA_BASE" ] && CATALINA_BASE="$CATALINA_HOME" # $CATALINA_BASE等同$CATALINA_HOME

4.设置CLASSPATH变量

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

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

  1. # Add on extra jar files to CLASSPATH
  2. if [ ! -z "$CLASSPATH" ] ; then
  3. CLASSPATH="$CLASSPATH":
  4. fi
  5. CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar #将bootstrap.jar作为CLASSPATH
  6. # Add tomcat-juli.jar to classpath
  7. # tomcat-juli.jar can be over-ridden per instance
  8. if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
  9. CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar #添加tomcat-juli.jar到classpath
  10. else
  11. CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
  12. fi

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

  • 参数为start的情况下

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

  1. eval $_NOHUP "\"$_RUNJAVA\"" "\"$LOGGING_CONFIG\"" $LOGGING_MANAGER $JAVA_OPTS $CATALINA_OPTS \
  2. -D$ENDORSED_PROP="\"$JAVA_ENDORSED_DIRS\"" \
  3. -classpath "\"$CLASSPATH\"" \
  4. -Dcatalina.base="\"$CATALINA_BASE\"" \
  5. -Dcatalina.home="\"$CATALINA_HOME\"" \
  6. -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
  7. org.apache.catalina.startup.Bootstrap "$@" start \
  8. >> "$CATALINA_OUT" 2>&1 "&"
  • 参数为stop的情况下

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

至此,整个过程结束。

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

  1. elif [ "$1" = "start" ] ; then
  2. if [ ! -z "$CATALINA_PID" ]; then
  3. if [ -f "$CATALINA_PID" ]; then
  4. if [ -s "$CATALINA_PID" ]; then
  5. echo "Existing PID file found during start."
  6. if [ -r "$CATALINA_PID" ]; then
  7. PID=`cat "$CATALINA_PID"`
  8. ps -p $PID >/dev/null 2>&1
  9. if [ $? -eq 0 ] ; then
  10. echo "Tomcat appears to still be running with PID $PID. Start aborted."
  11. echo "If the following process is not a Tomcat process, remove the PID file and try again:"
  12. ps -f -p $PID
  13. exit 1
  14. else
  15. echo "Removing/clearing stale PID file."
  16. rm -f "$CATALINA_PID" >/dev/null 2>&1
  17. if [ $? != 0 ]; then
  18. if [ -w "$CATALINA_PID" ]; then
  19. cat /dev/null > "$CATALINA_PID"
  20. else
  21. echo "Unable to remove or clear stale PID file. Start aborted."
  22. exit 1
  23. fi
  24. fi
  25. fi
  26. else
  27. echo "Unable to read PID file. Start aborted."
  28. exit 1
  29. fi
  30. else
  31. rm -f "$CATALINA_PID" >/dev/null 2>&1
  32. if [ $? != 0 ]; then
  33. if [ ! -w "$CATALINA_PID" ]; then
  34. echo "Unable to remove or write to empty PID file. Start aborted."
  35. exit 1
  36. fi
  37. fi
  38. fi
  39. fi
  40. 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方法代码片段:

  1. try {
  2. String command = "start";
  3. if (args.length > 0) {
  4. command = args[args.length - 1];
  5. }
  6. if (command.equals("startd")) {
  7. args[args.length - 1] = "start";
  8. daemon.load(args);
  9. daemon.start();
  10. } else if (command.equals("stopd")) {
  11. args[args.length - 1] = "stop";
  12. daemon.stop();
  13. } else if (command.equals("start")) {
  14. daemon.setAwait(true);
  15. daemon.load(args);
  16. daemon.start();
  17. } else if (command.equals("stop")) {
  18. daemon.stopServer(args);
  19. } else if (command.equals("configtest")) {
  20. daemon.load(args);
  21. if (null==daemon.getServer()) {
  22. System.exit(1);
  23. }
  24. System.exit(0);
  25. } else {
  26. log.warn("Bootstrap: command \"" + command + "\" does not exist.");
  27. }
  28. } catch (Throwable t) {
  29. // Unwrap the Exception for clearer error reporting
  30. if (t instanceof InvocationTargetException &&
  31. t.getCause() != null) {
  32. t = t.getCause();
  33. }
  34. handleThrowable(t);
  35. t.printStackTrace();
  36. System.exit(1);
  37. }

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

Catalina.start

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

  1. if (await) {
  2. await();
  3. stop();
  4. }

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

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

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

此处建立了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代码片段:

  1. // Stop the existing server
  2. s = getServer();
  3. if (s.getPort()>0) {
  4. Socket socket = null;
  5. OutputStream stream = null;
  6. try {
  7. socket = new Socket(s.getAddress(), s.getPort());
  8. stream = socket.getOutputStream();
  9. String shutdown = s.getShutdown();
  10. for (int i = 0; i < shutdown.length(); i++) {
  11. stream.write(shutdown.charAt(i));
  12. }
  13. stream.flush();
  14. } catch (ConnectException ce) {
  15. log.error(sm.getString("catalina.stopServer.connectException",
  16. s.getAddress(),
  17. String.valueOf(s.getPort())));
  18. log.error("Catalina.stop: ", ce);
  19. System.exit(1);
  20. } catch (IOException e) {
  21. log.error("Catalina.stop: ", e);
  22. System.exit(1);
  23. } finally {
  24. if (stream != null) {
  25. try {
  26. stream.close();
  27. } catch (IOException e) {
  28. // Ignore
  29. }
  30. }
  31. if (socket != null) {
  32. try {
  33. socket.close();
  34. } catch (IOException e) {
  35. // Ignore
  36. }
  37. }
  38. }

五、总结

我们分析了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命令,从而在关闭进程前执行有效的清理工作。

出处:https://blog.csdn.net/qq_21508059/article/details/82713797
posted @ 2021-01-12 20:38  十点书屋  阅读(1228)  评论(0)    收藏  举报