# 前言
这是一篇以调试为主的记录,主要是为了加深自己对 Tomcat 相关的理解(前面内存马看的时候比较吃力),以及锻炼一下自己的调试能力。如果有不当之处还请指出
# 目录分析
从官网 Apache Tomcat® - Welcome! 中下载到 tomcat 解压后就可以看到上图的目录结构,这里我的 tomcat 版本是 apache-tomcat-9.0.71
# bin
bin 目录如上图所示,其中存放的多是 tomcat 的命令,还有一些环境变量(tomcat 在配置 JAVA_HOME 环境变量后才能启动)
- 以.sh 结尾的代表 Linux 下的命令
- startup.bat 代表 windows 系统下启动 Tomcat 的命令;
- shutdown.bat 代表 Windows 系统下关闭 Tomcat 的命令。
- 以.bat 结尾的代表 Windows 下的命令
- startup.sh 代表 linux 系统下启动 Tomcat 的命令;
- shutdown.sh 代表 linux 下关闭 Tomcat 的命令。
# conf
- catalina.policy:权限相关 Permission,Tomcat 是运行在 JVM 上的,所以有些权限是默认的
- server.xml:服务器的配置文件,例如端口号
- web.xml:tomcat 会先加载本身的 web.xml 文件,然后再去加载每个 webapp 专属的 web.xml
- tomcat-user.xml:储存 tomcat 用户的文件,这里保存的是 tomcat 的用户名和密码,以及用户角色信息。
# lib
lib 目录下面存放着 tomcat 服务所需要的所有 jar 包
- logs:存放的是 tomcat 运行过程中产生的日志文件
- temp:存放的 tomcat 运行时产生的临时文件
- webapps:tomcat 的默认部署路径
- work:用来存放 tomcat 在运行时编译后的文件
# 调试之旅
# 启动初始化
# 类加载器创建 & 获取
tomcat 的调试起点在 org.apache.catalina.startup.Bootstrap#main,前面也提到 tomcat 根据 catalina.sh 脚本来进行启动
这里可以看到这里实际上是调用了这个 Bootstart
这里直接将断点下在 main 方法这里,方法注释中说到通过提供的脚本启动 Tomcat 时的主方法和入口点。首先通过 synchronized 关键字保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区。然后这里 if 判断守护进程是否尚未设置,然后实例化一个 Bootstrap 后 init,这里跟进
这里就到了本类的 init 方法中,首先会初始化类加载器,这里跟进到 initClassLoader 方法
这里调用了 createClassLoader 方法创建 ClassLoader,这里看一下 commonloader 的创建
跟进到 createClassloader 方法后,这里获取 Catalina 值
跟进看到在 CatalinaProperties 类中有一个静态代码块,这里会直接调用 loadProperties 方法
这个方法中加载的文件名是 catalina.properties
调试出来后这里会加载 tomcat 的 lib 目录下的文件
接着这里创建了 serverLoader 和 shareLoader,实际上这个方法就是加载当前的所有 jar
这里调试出来回到 init 方法中,首先设置容器类加载器为 URLClassLoader,然后加载我们的启动类并调用它的 process()方法
然后设置设置共享扩展类加载器
init 执行完成,这里设置守护进程
# 解析命令
解析命令默认为 start
这里一共调用了三个方法 setAwait、load 和 start,值得我们注意是后两者。这里先跟进到 setAwait 方法
这里实际上反射调用了 Catalina.setAwait 方法,设置主线程一直等待防止主线程挂掉,跟进到第二个方法
这里是一个反射调用,直接跟进到 invoke 方法
可以看到实际上是调用到 Catalina 的 load 方法,然后调用到本地的无参 load
先初始化 Naming,然后通过 digester 这个组件去解析 server.xml,这里跟进到 parseServerXml 这个方法
这里会拿到当前 server.xml,然后这个 if 是进不去的,这里就不过多的关注
这里直接看最后面的 try……catch 代码块,这里就是加载 server.xml 这个文件
配置 server 的一系列组件后进行初始化,这里跟进到这个 init () 方法
这里实际上值得我们关注的只有 initInternal 方法,通过名字也可以看出来这个方法才是初始化的地方
这里实际上就两步
- 初始化 globalNamingResources
- 初始化每一个 service
最后这里放一个初始化这部分的流程图
# Tomcat 的启动过程
这里虽然说我分到了第二个小节,实际上还是接着上面的调试
至这个 load 方法执行完,整个启动的初始化就已经完成了。然后调用这个 start 方法启动 tomcat,这里跟进
这里也是通过反射去调用 Catalina.start () 继续跟进
然后这里经过判断后会调用 getServer ().start (); 方法
然后会走到这个 startInternal 方法上,跟进
- 先是
globalNamingResources
开启,开启 JNDI 服务 - 启动所有的
service
服务
这里跟进到 service.start () 方法中
这里又会进入到这个 start 方法中
# 开启 Engine
还是跟进到 startInternal 方法中
这里分别循环去启动 engine、executor 和 connector
这里先跟进 engine.start () 方法,又是这个方法再跟进到 startInternal
这里到 super.startInternal 方法
这里调用到 Containerbase#startInternal 这个方法,在这个方法中
- 将虚拟主机 Host 封装为
StartChild
, 这个是一个 Callback, 交给了线程池执行。 - 启动自己的
pipline
管道
这里直接调试出去
调试回到 engine.start 这个地方
# 启动 Connector
这里跟进到下面的 connector.start () 方法
这里和上面的 Engine 的启动实际上很像,这里还是跟进到这个 startInternal 这个方法中
这里调用了这个 Connector#startInternal (),这里主要干两件事
Connector
创建对象的无参构造器默认就指定了使用http11protocolHandler
- 开启协议处理器
protocolHandler
这里跟进到 protocolHandler.start () 方法中
调用到 AbstractProticol.start () 方法,这里主要做一个断点启动也就是 endpoint.start ();,继续跟进到这个方法
还是跟进到 startInternal
/** | |
* 启动服务的内部方法 | |
* @throws Exception 如果启动过程中发生异常 | |
*/ | |
public void startInternal() throws Exception { | |
// 检查服务是否已经在运行,避免重复启动 | |
if (!running) { | |
// 标记服务为运行中 | |
running = true; | |
// 将暂停标志设置为 false,表示服务没有暂停 | |
paused = false; | |
// 初始化处理器缓存 | |
if (socketProperties.getProcessorCache() != 0) { | |
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, | |
socketProperties.getProcessorCache()); | |
} | |
// 初始化事件缓存 | |
if (socketProperties.getEventCache() != 0) { | |
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, | |
socketProperties.getEventCache()); | |
} | |
// 初始化 NIO 通道缓存 | |
if (socketProperties.getBufferPool() != 0) { | |
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, | |
socketProperties.getBufferPool()); | |
} | |
// 如果没有配置执行器(线程池),则创建一个 | |
if (getExecutor() == null) { | |
createExecutor(); | |
} | |
// 初始化连接计数器 | |
initializeConnectionLatch(); | |
// 启动 Poller 线程 | |
poller = new Poller(); | |
Thread pollerThread = new Thread(poller, getName() + "-Poller"); | |
// 设置线程优先级 | |
pollerThread.setPriority(threadPriority); | |
// 将线程标记为守护线程,即当所有非守护线程退出时,守护线程会自动退出 | |
pollerThread.setDaemon(true); | |
// 启动 Poller 线程 | |
pollerThread.start(); | |
// 启动接收器线程 | |
startAcceptorThread(); | |
} | |
} |
# Cataliona.await () 方法等待数据
Connector 就算是启动成功了,这里看启动过程中最后一个需要注意的方法
调试回到这个 getServer ().start (); 方法,然后向下调试到 await () 方法
跟进这个方法
这里就到了,继续跟进这个 await 方法
这个代码很长,这里直译一下注释
/** | |
* 等待接收关闭命令的方法 | |
*/ | |
@Override | |
public void await() { | |
// 如果端口为负值 - 不等待端口,可能是嵌入式应用或者不使用端口 | |
if (getPortWithOffset() == -2) { | |
// 对于嵌入式应用,或者其他不需要监听端口的情况,直接返回 | |
return; | |
} | |
// 如果端口为 -1,则进入轮询等待模式 | |
if (getPortWithOffset() == -1) { | |
try { | |
awaitThread = Thread.currentThread(); | |
// 循环等待直到 stopAwait 为 true | |
while(!stopAwait) { | |
try { | |
Thread.sleep(10000); // 每隔 10 秒轮询一次 | |
} catch(InterruptedException ex) { | |
// 睡眠被中断,继续检查 stopAwait 标志 | |
} | |
} | |
} finally { | |
awaitThread = null; | |
} | |
return; | |
} | |
// 在指定端口创建一个服务器套接字 | |
try { | |
awaitSocket = new ServerSocket(getPortWithOffset(), 1, InetAddress.getByName(address)); | |
} catch (IOException e) { | |
// 打印错误日志,端口创建失败 | |
log.error(sm.getString("standardServer.awaitSocket.fail", address, | |
String.valueOf(getPortWithOffset()), String.valueOf(getPort()), | |
String.valueOf(getPortOffset())), e); | |
return; | |
} | |
try { | |
awaitThread = Thread.currentThread(); | |
// 循环等待连接和有效命令 | |
while (!stopAwait) { | |
ServerSocket serverSocket = awaitSocket; | |
if (serverSocket == null) { | |
break; | |
} | |
// 等待下一个连接 | |
Socket socket = null; | |
StringBuilder command = new StringBuilder(); | |
try { | |
InputStream stream; | |
long acceptStartTime = System.currentTimeMillis(); | |
try { | |
socket = serverSocket.accept(); | |
socket.setSoTimeout(10 * 1000); // 设置超时时间为 10 秒 | |
stream = socket.getInputStream(); | |
} catch (SocketTimeoutException ste) { | |
// 超时异常,打印警告日志并继续循环 | |
log.warn(sm.getString("standardServer.accept.timeout", | |
Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste); | |
continue; | |
} catch (AccessControlException ace) { | |
// 安全权限异常,打印警告日志并继续循环 | |
log.warn(sm.getString("standardServer.accept.security"), ace); | |
continue; | |
} catch (IOException e) { | |
if (stopAwait) { | |
// 等待被中止,使用 socket.close () 中止 | |
break; | |
} | |
// 打印错误日志,连接异常 | |
log.error(sm.getString("standardServer.accept.error"), e); | |
break; | |
} | |
// 从套接字读取字符 | |
int expected = 1024; // 防止 DoS 攻击,设置一个最大值 | |
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(sm.getString("standardServer.accept.readError"), e); | |
ch = -1; | |
} | |
// 控制字符或 EOF(-1)终止循环 | |
if (ch < 32 || ch == 127) { | |
break; | |
} | |
command.append((char) ch); | |
expected--; | |
} | |
} finally { | |
// 在完成后关闭套接字 | |
try { | |
if (socket != null) { | |
socket.close(); | |
} | |
} catch (IOException e) { | |
// 忽略 | |
} | |
} | |
// 比对接收到的命令 | |
boolean match = command.toString().equals(shutdown); | |
if (match) { | |
// 匹配到关闭命令,打印信息并结束循环 | |
log.info(sm.getString("standardServer.shutdownViaPort")); | |
break; | |
} else { | |
// 未匹配到命令,打印警告日志 | |
log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString())); | |
} | |
} | |
} finally { | |
ServerSocket serverSocket = awaitSocket; | |
awaitThread = null; | |
awaitSocket = null; | |
// 关闭服务器套接字并返回 | |
if (serverSocket != null) { | |
try { | |
serverSocket.close(); | |
} catch (IOException e) { | |
// 忽略 | |
} | |
} | |
} | |
} |
简单总结一下有下面这个过程
- 如果配置的端口为负值
-2
,则表示不需要等待端口,直接返回。 - 如果配置的端口为
-1
,则进入轮询等待模式,每隔 10 秒轮询一次,直到stopAwait
为true
。 - 如果配置的端口为正值,创建一个服务器套接字并等待连接,然后读取输入流中的字符,直到接收到与配置的关闭命令一致的字符串。
- 如果接收到关闭命令,记录信息并退出循环,表示关闭服务器。
- 如果未接收到关闭命令,记录警告信息并继续等待。
启动到这里也就算是结束了
最后这里放一个这个部分的流程图
# 启动过程中加载 web.xml
本来这部分一个放在 Tomcat 的启动过程中来写的,但是这块东西稍微有点多,写在一块未免过于臃肿,所以这里单独拎出来写一个小节
直接在这个 engine.start (); 这里下一个断点,还是和前面一样的步骤,跟进这里
然后这里实际上启动的代码是 startInternal 这里方法里接着跟进
然后这里调用了 StandardEngine 这个类的父类中的 startInternal 方法
这次我们将目光看向这个 if 语句中,这段代码的作用是启动一个后台线程,定期执行 ContainerBackgroundProcessorMonitor
中的任务,用于处理一些周期性的工作。这里跟进到 scheduleWithFixedDelay 方法中
这里会调用到这个 executor.scheduleWithFixedDelay,这里继续跟进
这里调用到 ScheduledThreadPoolExecutor#scheduleWithFixedDelay 方法,这个方法用于以固定的延迟时间执行指定的任务。我们跟进到这个 delayedExecute 方法中
调用本类的 delayedExecute 方法负责将任务添加到延迟队列中,并根据线程池的状态进行一些必要的处理,确保任务能够按照预期执行,继续跟进到 ensurePrestart () 方法
调用到 ThreadPoolExecutor#ensurePrestart 确保线程池中至少有一个工作线程,以便在有任务提交时能够及时执行。跟进 addWorker 方法
调用本类的 addwork 方法,这个方法也很明显就是想线程池中添加工作线程,这里我们跟进到 new worker 这个方法中
这里调用 worker 类的构造方法,创建一个新的工作线程对象,这里跟进到 newThread
在这个方法中创建新的线程对象,并对线程的一些属性进行设置,包括是否为守护线程、优先级以及上下文类加载器等这里,其中就是各种线程的启动,这里也不在过多的去关注。最后会调用 FutureTask#run 方法的 result = c.call (); 这里
直接跟进这个方法
这里会直接去调用 run,继续跟进
这里是调用到了 HostConfig#run 方法
这里调用 HostConfig#deployDirectory 方法,这个方法主要目的是在 Tomcat 中动态部署应用程序,并在运行时检测文件和配置的更改。跟进到这个方法中 host.addChild ()
这个方法的主要作用是确保在 StandardHost
中添加的子容器是有效的 Context
对象,并且为新添加的 Context
注册了一个内存泄漏跟踪监听器。这有助于在开发和调试过程中更容易地发现和解决潜在的内存泄漏问题。这里跟进到这个 super.addChild (child); 方法中
这就是一个 add,然后这里跟进到 addChildInternal 方法中
方法主要负责在容器中添加子容器,并在添加完成后触发相应的事件
值得注意的是实际的启动子容器操作在同步块之外进行,以避免在同步块内执行耗时的操作,可能引发其他问题
触发就是这里的 child.start ();,这里继续跟进到这个方法
start 方法的调用还是回到 LifecycleBase#start 方法,还是进到 startInternal (); 方法中
启动组件,这里先跟进到这个 postWorkDirectory () 方法
这里调用本类的 postWorkDirectory 方法,这里跟进到 getServletContext 方法中
这个方法的主要目的是获取 Servlet 上下文对象,如果上下文对象不存在,则创建一个新的 ApplicationContext
对象。
调试回到这个 startInternal 方法中
if (getLoader() == null) { | |
WebappLoader webappLoader = new WebappLoader(); | |
webappLoader.setDelegate(getDelegate()); | |
setLoader(webappLoader); | |
} |
创建一个新的 WebappLoader
对象,并设置它的 delegate
属性为当前上下文的 delegate
属性,最后将新创建的 WebappLoader
设置为当前上下文的类加载器。
// An explicit cookie processor hasn't been specified; use the default | |
if (cookieProcessor == null) { | |
cookieProcessor = new Rfc6265CookieProcessor(); | |
} |
这段代码检查是否已经为 cookieProcessor
属性指定了明确的 Cookie 处理器。如果没有指定,它会创建一个默认的 Rfc6265CookieProcessor
对象并将其赋值给 cookieProcessor
属性。
在 Tomcat 中, CookieProcessor
负责处理 HTTP Cookie。RFC 6265 是有关 HTTP Cookies 的规范,而 Rfc6265CookieProcessor
是 Tomcat 中默认的 Cookie 处理器,用于遵循这一规范。
一直到下面这里,跟进到这个 ((Lifecycle) loader).start (); 方法中
还是到这个 LifecycleBase,这里跟进到 startInternal
这个方法的主要任务是初始化和启动 WebappLoader
类加载器,同时记录适当的日志信息和注册 JMX 组件,这里更多就没有什么可以看的了,然后跳出这段代码
这里调进这个方法
这个方法的是为了允许子类在其生命周期内触发特定类型的事件,并通知已注册的监听器。然后跟进到这个 listener.lifecycleEvent (event); 方法
这段代码实现了 LifecycleListener
接口的 lifecycleEvent
方法,该方法用于处理与 Lifecycle
事件相关的逻辑,跟进到这个 configureStart (); 方法
处理上下文配置,跟进到 webconfig
继续跟进到 configureContext 方法
然后这里就是各种加载配置文件了
贴张图