环境

运行环境:Windows 10 | Apache Tomcat 9.0.34 | 博客记录时间:2020-4-23 17:16:12

通常我们 打开 / 关闭 Tomcat 服务时都会执行bin目录下的 startup.bat / shutdown.bat 文件。

但是细心的小伙伴就会发现(╮( ̄▽ ̄)╭),startup.batshutdown.bat 文件中都有这么一句话。

1
2
3
4
rem ---------------------------------------------------------------------------
rem Start / Stop script for the CATALINA Server
rem ---------------------------------------------------------------------------
call "%EXECUTABLE%" start / stop %CMD_LINE_ARGS%

所以不论是 startup 还是 shutdown 都是执行了 catalina.bat,只是执行的指令不同。

catalina.bat

首选我们分析下 catalina.bat 脚本的内容:

1
2
3
4
5
6
7
8
rem Guess CATALINA_HOME if not defined
set "CURRENT_DIR=%cd%"
if not "%CATALINA_HOME%" == "" goto gotHome
set "CATALINA_HOME=%CURRENT_DIR%"
if exist "%CATALINA_HOME%\bin\catalina.bat" goto okHome
cd ..
set "CATALINA_HOME=%cd%"
cd "%CURRENT_DIR%"

检验 CATALINA_HOME 环境变量,如果不正确则重新设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
set CLASSPATH=

rem Get standard environment variables
if not exist "%CATALINA_BASE%\bin\setenv.bat" goto checkSetenvHome
call "%CATALINA_BASE%\bin\setenv.bat"
goto setenvDone
:checkSetenvHome
if exist "%CATALINA_HOME%\bin\setenv.bat" call "%CATALINA_HOME%\bin\setenv.bat"
:setenvDone

rem Get standard Java environment variables
if exist "%CATALINA_HOME%\bin\setclasspath.bat" goto okSetclasspath
echo Cannot find "%CATALINA_HOME%\bin\setclasspath.bat"
echo This file is needed to run this program
goto end
:okSetclasspath
call "%CATALINA_HOME%\bin\setclasspath.bat" %1
if errorlevel 1 goto end

设置环境变量:

  • 在 CATALINA_BASE 和 CATALINA_BASE 寻找setenv.bat文件并执行,找不到则不执行。
  • 寻找setclasspath.bat(设置 Java 相关的环境变量)文件并执行,找不到则结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
rem Add tomcat-juli.jar to classpath
rem tomcat-juli.jar can be over-ridden per instance
if not exist "%CATALINA_BASE%\bin\tomcat-juli.jar" goto juliClasspathHome
set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%\bin\tomcat-juli.jar"
goto juliClasspathDone
:juliClasspathHome
set "CLASSPATH=%CLASSPATH%;%CATALINA_HOME%\bin\tomcat-juli.jar"
:juliClasspathDone

if not "%JSSE_OPTS%" == "" goto gotJsseOpts
set "JSSE_OPTS=-Djdk.tls.ephemeralDHKeySize=2048"
:gotJsseOpts
set "JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS%"

rem Register custom URL handlers
rem Do this here so custom URL handles (specifically 'war:...') can be used in the security policy
set "JAVA_OPTS=%JAVA_OPTS% -Djava.protocol.handler.pkgs=org.apache.catalina.webresources"

if not "%LOGGING_CONFIG%" == "" goto noJuliConfig
set LOGGING_CONFIG=-Dnop
if not exist "%CATALINA_BASE%\conf\logging.properties" goto noJuliConfig
set LOGGING_CONFIG=-Djava.util.logging.config.file="%CATALINA_BASE%\conf\logging.properties"
:noJuliConfig

if not "%LOGGING_MANAGER%" == "" goto noJuliManager
set LOGGING_MANAGER=-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
:noJuliManager

tomcat-juli.jar:

  • tomcat-juli.jar 添加到 classPath 环境变量中。
  • 将日志的配置文件路径添加到 LOGGING_CONFIG 环境变量中。

Apache Tomcat由一个自己的实现了 java.util.logging 多个关键元素的实现。这个实现被称为 JULI 。实现的核心组件是定制化的 LogManager ,可以获取运行在Tomcat中的不同web应用(以及不同的class loader)。他支持为应用配置单独的日志配置。当有web应用从内在中是被卸载时,会接到Tomcat的通知,以便他所引用的类可以被清除,避免内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
rem Execute The Requested Command
echo Using CATALINA_BASE: "%CATALINA_BASE%"
echo Using CATALINA_HOME: "%CATALINA_HOME%"
echo Using CATALINA_TMPDIR: "%CATALINA_TMPDIR%"
if ""%1"" == ""debug"" goto use_jdk
echo Using JRE_HOME: "%JRE_HOME%"
goto java_dir_displayed
:use_jdk
echo Using JAVA_HOME: "%JAVA_HOME%"
:java_dir_displayed
echo Using CLASSPATH: "%CLASSPATH%"

set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start
set SECURITY_POLICY_FILE=
set DEBUG_OPTS=
set JPDA=

if not ""%1"" == ""jpda"" goto noJpda
set JPDA=jpda
if not "%JPDA_TRANSPORT%" == "" goto gotJpdaTransport
set JPDA_TRANSPORT=dt_socket
:gotJpdaTransport
if not "%JPDA_ADDRESS%" == "" goto gotJpdaAddress
set JPDA_ADDRESS=localhost:8000
:gotJpdaAddress
if not "%JPDA_SUSPEND%" == "" goto gotJpdaSuspend
set JPDA_SUSPEND=n
:gotJpdaSuspend
if not "%JPDA_OPTS%" == "" goto gotJpdaOpts
set JPDA_OPTS=-agentlib:jdwp=transport=%JPDA_TRANSPORT%,address=%JPDA_ADDRESS%,server=y,suspend=%JPDA_SUSPEND%
:gotJpdaOpts
shift
:noJpda
  • 设置启动类 - org.apache.catalina.startup.Bootstrap
  • 设置远程调试相关参数 - JPDA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if ""%1"" == ""debug"" goto doDebug
if ""%1"" == ""run"" goto doRun
if ""%1"" == ""start"" goto doStart
if ""%1"" == ""stop"" goto doStop
if ""%1"" == ""configtest"" goto doConfigTest
if ""%1"" == ""version"" goto doVersion

echo Usage: catalina ( commands ... )
echo commands:
echo debug Start Catalina in a debugger
echo debug -security Debug Catalina with a security manager
echo jpda start Start Catalina under JPDA debugger
echo run Start Catalina in the current window
echo run -security Start in the current window with security manager
echo start Start Catalina in a separate window
echo start -security Start in a separate window with security manager
echo stop Stop Catalina
echo configtest Run a basic syntax check on server.xml
echo version What version of tomcat are you running?
goto end

这里值得注意的且常用的(start / stop),正如开头提到的。

org.apache.catalina

解压 bin 目录中的 catalina.jar 后我们能找到 org.apache.catalina.startup.Bootstrap 也就是 catalina.bat 里设置的启动类(MAINCLASS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.apache.catalina.startup;
public final class Bootstrap {
public static void main(String[] args) {
// --- 略 ---
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
} else if (command.equals("stop")) {
daemon.stopServer(args);
}
}
// --- 略 ---
}
}

当指令等于start时运行 Catalina.class 中的 start()方法:

1
2
3
4
5
6
7
8
9
10
11
package org.apache.catalina.startup;

public class Catalina {
public void start() {
// --- 略 ---
if (this.await) {
this.await();
this.stop();
}
}
}

start()方法中调用了await()stop()两个方法:

  • await() 方法监听停止服务请求的方法

  • stop() 方法是停止服务的方法

await() 方法是阻塞方法,只有客户端请求关闭Tomcat服务时,他才会执行stop()方法,否则一直等待关闭请求。

StandardServer.class 中的 await() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package org.apache.catalina.core;

public final class StandardServer extends LifecycleMBeanBase implements Server {
private volatile ServerSocket awaitSocket = null;
private String shutdown = "SHUTDOWN";

public void await() {
/* 略 */
try {
this.awaitSocket = new ServerSocket(this.getPortWithOffset(), 1,InetAddress.getByName(this.address));
} catch (IOException var67) {
log.error(sm.getString("standardServer.awaitSocket.fail", new Object[]{this.address,String.valueOf(this.getPortWithOffset()), String.valueOf(this.getPort()),String.valueOf(this.getPortOffset())}), var67);
return;
}
ServerSocket serverSocket;
try {
/* 略 */
while(true) {
label611: {
label610: {
try {
label634: {
try {
// 执行accept等待请求
socket = serverSocket.accept();
socket.setSoTimeout(10000);
stream = socket.getInputStream();
} catch (SocketTimeoutException var69) {
log.warn(sm.getString("standardServer.accept.timeout", new Object[]{System.currentTimeMillis() - acceptStartTime}), var69);
continue;
}
/* 略 */
}
}
}
}
/* 略 */
boolean match = command.toString().equals(this.shutdown);
// 如果请求内容与 shutdown 字段相同则跳出循环,Socket 服务停止
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
var32 = false;
break;
}
log.warn(sm.getString("standardServer.invalidShutdownCommand", new Object[]{command.toString()}));
}
}
/* 略 */
}
}

可以看到这里开启了一个 ServerSocket ,调用 accept() 方法监听请求。

serverSocket.close()之后 start()方法中的this.stop()执行,Server 被关闭。

当指令等于stop时运行 Catalina.class 中的 stopServer()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void stopServer(String[] arguments) {
if (arguments != null) {
this.arguments(arguments);
}
Server s = this.getServer();
if (s == null) {
/* 略 */
} else {
try {
s.stop();
s.destroy();
} catch (LifecycleException var63) {
log.error(sm.getString("catalina.stopError"), var63);
}
}
}

显而易见,Server 不为 null 时,停止并且销毁。

Server 中的 shutdown 属性

1
<Server port="8085" shutdown="SHUTDOWN">

这里shutdown的默认属性就是SHUTDOWN,与 StandardServer 类中 shutdown 属性的值一样。

当然 StandardServer 类中 shutdown 属性提供了 setShutdown(String shutdown) 方法。

做个测试,我们执行 startup.bat 先启动 Tomcat ,跳出窗口并打印日志:

1
2
3
4
5
6
7
8
9
┌────────────────────────────────────────────────────────┐
│Tomcat
├────────────────────────────────────────────────────────┤
│Server.服务器版本:Apache Tomcat/9.0.34
│服务器构建:Apr 3 2020 2020 12:02:52 UTC
│服务器版本号(:9.0.34.0
# 略
│Server startup in [673] milliseconds
└────────────────────────────────────────────────────────┘

我们再启动另一个窗口,进入telnet

1
2
3
4
5
6
7
8
┌────────────────────────────────────────────────────────┐
│Command Prompt
├────────────────────────────────────────────────────────┤
│Microsoft Windows [版本 10.0.17763.1039]
│(c) 2018 Microsoft Corporation。保留所有权利。
│C:\Users\Administrator>telnet localhost 8005
│正在连接localhost...
└────────────────────────────────────────────────────────┘

之后我们再像8005端口发送点东西

1
2
3
4
5
┌────────────────────────────────────────────────────────┐
│Telnet localhost
├────────────────────────────────────────────────────────┤
│unshell
└────────────────────────────────────────────────────────┘

发送unshell字符串,Tomcat窗口打印出新的内容

1
2
3
4
5
6
7
8
9
10
┌────────────────────────────────────────────────────────┐
│Tomcat
├────────────────────────────────────────────────────────┤
│Server.服务器版本:Apache Tomcat/9.0.34
│服务器构建:Apr 3 2020 2020 12:02:52 UTC
│服务器版本号(:9.0.34.0
│# 略
│Server startup in [673] milliseconds
│Invalid shutdown command [unshell] received
└────────────────────────────────────────────────────────┘

如果内容没有更新可以尝试按enter

1
2
# 完整内容如下(可以看到警告的方法,就如我们之前所说的那样)
23-Apr-2020 16:57:20.166 警告 [main] org.apache.catalina.core.StandardServer.await Invalid shutdown command [unshell] received

所以当我们输入指令为SHUTDOWN时,Tomcat 就关闭了。

当然这个指令默认监听localhost的关闭请求,如果要支持远程关闭的话,可以添加参数 address,例子如下:

1
<Server port="8085" shutdown="SHUTDOWN" address="36.152.44.95">

具体版本是否支持该参数,运行 Tomcat ,文档在 http://localhost:8080/docs/config/server.html中有说明。

这里以版本9为例

All implementations of Server support the following attributes:

Attribute Description
port The TCP/IP port number on which this server waits for a shutdown command. Set to -1 to disable the shutdown port.
shutdown The command string that must be received via a TCP/IP connection to the specified port number, in order to shut down Tomcat.
address The TCP/IP address on which this server waits for a shutdown command. If no address is specified, localhost is used.

以上就是所有内容,本文只是在理解 Tomcat 时,碰巧看到的一些边缘内容,作为记录保存下来。

引文