# 前言

这是一篇以调试为主的记录,主要是为了加深自己对 Tomcat 相关的理解(前面内存马看的时候比较吃力),以及锻炼一下自己的调试能力。如果有不当之处还请指出

# 目录分析

image-20231109171112216

从官网 Apache Tomcat® - Welcome! 中下载到 tomcat 解压后就可以看到上图的目录结构,这里我的 tomcat 版本是 apache-tomcat-9.0.71

# bin

image-20231109171500967

bin 目录如上图所示,其中存放的多是 tomcat 的命令,还有一些环境变量(tomcat 在配置 JAVA_HOME 环境变量后才能启动)

  • 以.sh 结尾的代表 Linux 下的命令
    1. startup.bat 代表 windows 系统下启动 Tomcat 的命令;
    2. shutdown.bat 代表 Windows 系统下关闭 Tomcat 的命令。
  • 以.bat 结尾的代表 Windows 下的命令
    1. startup.sh 代表 linux 系统下启动 Tomcat 的命令;
    2. shutdown.sh 代表 linux 下关闭 Tomcat 的命令。

# conf

image-20231109190421033

  1. catalina.policy:权限相关 Permission,Tomcat 是运行在 JVM 上的,所以有些权限是默认的
  2. server.xml:服务器的配置文件,例如端口号
  3. web.xml:tomcat 会先加载本身的 web.xml 文件,然后再去加载每个 webapp 专属的 web.xml
  4. tomcat-user.xml:储存 tomcat 用户的文件,这里保存的是 tomcat 的用户名和密码,以及用户角色信息。

# lib

image-20231109191301084

lib 目录下面存放着 tomcat 服务所需要的所有 jar 包

  1. logs:存放的是 tomcat 运行过程中产生的日志文件
  2. temp:存放的 tomcat 运行时产生的临时文件
  3. webapps:tomcat 的默认部署路径
  4. work:用来存放 tomcat 在运行时编译后的文件

# 调试之旅

# 启动初始化

# 类加载器创建 & 获取

tomcat 的调试起点在 org.apache.catalina.startup.Bootstrap#main,前面也提到 tomcat 根据 catalina.sh 脚本来进行启动

image-20231113125949225

这里可以看到这里实际上是调用了这个 Bootstart

image-20231109163420143

这里直接将断点下在 main 方法这里,方法注释中说到通过提供的脚本启动 Tomcat 时的主方法和入口点。首先通过 synchronized 关键字保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区。然后这里 if 判断守护进程是否尚未设置,然后实例化一个 Bootstrap 后 init,这里跟进

image-20231109205809919

这里就到了本类的 init 方法中,首先会初始化类加载器,这里跟进到 initClassLoader 方法

image-20231110140559191

这里调用了 createClassLoader 方法创建 ClassLoader,这里看一下 commonloader 的创建

image-20231110141317072

跟进到 createClassloader 方法后,这里获取 Catalina 值

image-20231110141527292

跟进看到在 CatalinaProperties 类中有一个静态代码块,这里会直接调用 loadProperties 方法

image-20231110142529127

这个方法中加载的文件名是 catalina.properties

image-20231110142812122

调试出来后这里会加载 tomcat 的 lib 目录下的文件

image-20231110142934467

接着这里创建了 serverLoader 和 shareLoader,实际上这个方法就是加载当前的所有 jar

image-20231110143709474

这里调试出来回到 init 方法中,首先设置容器类加载器为 URLClassLoader,然后加载我们的启动类并调用它的 process()方法

image-20231110144210574

然后设置设置共享扩展类加载器

image-20231110145214049

init 执行完成,这里设置守护进程

# 解析命令

解析命令默认为 start

image-20231110145453967

这里一共调用了三个方法 setAwait、load 和 start,值得我们注意是后两者。这里先跟进到 setAwait 方法

image-20231110150451580

这里实际上反射调用了 Catalina.setAwait 方法,设置主线程一直等待防止主线程挂掉,跟进到第二个方法

image-20231110152225325

这里是一个反射调用,直接跟进到 invoke 方法

image-20231110152443163

可以看到实际上是调用到 Catalina 的 load 方法,然后调用到本地的无参 load

image-20231110152841039

先初始化 Naming,然后通过 digester 这个组件去解析 server.xml,这里跟进到 parseServerXml 这个方法

image-20231110154218676

这里会拿到当前 server.xml,然后这个 if 是进不去的,这里就不过多的关注

image-20231110154437948

这里直接看最后面的 try……catch 代码块,这里就是加载 server.xml 这个文件

image-20231110155338662

配置 server 的一系列组件后进行初始化,这里跟进到这个 init () 方法

image-20231110155512698

这里实际上值得我们关注的只有 initInternal 方法,通过名字也可以看出来这个方法才是初始化的地方

image-20231110160057284

这里实际上就两步

  1. 初始化 globalNamingResources
  2. 初始化每一个 service

image-20231110160921696

最后这里放一个初始化这部分的流程图

# Tomcat 的启动过程

这里虽然说我分到了第二个小节,实际上还是接着上面的调试

image-20231110162659478

至这个 load 方法执行完,整个启动的初始化就已经完成了。然后调用这个 start 方法启动 tomcat,这里跟进

image-20231110162925869

这里也是通过反射去调用 Catalina.start () 继续跟进

image-20231110163321976

然后这里经过判断后会调用 getServer ().start (); 方法

image-20231110163904813

然后会走到这个 startInternal 方法上,跟进

image-20231110164304743

  1. 先是 globalNamingResources 开启,开启 JNDI 服务
  2. 启动所有的 service 服务

这里跟进到 service.start () 方法中

image-20231110165452167

这里又会进入到这个 start 方法中

# 开启 Engine

还是跟进到 startInternal 方法中

image-20231110165800759

这里分别循环去启动 engine、executor 和 connector

image-20231110173320071

这里先跟进 engine.start () 方法,又是这个方法再跟进到 startInternal

image-20231110173908025

这里到 super.startInternal 方法

image-20231110174009886

这里调用到 Containerbase#startInternal 这个方法,在这个方法中

  1. 将虚拟主机 Host 封装为 StartChild , 这个是一个 Callback, 交给了线程池执行。
  2. 启动自己的 pipline 管道

这里直接调试出去

image-20231112130519657

调试回到 engine.start 这个地方

# 启动 Connector

这里跟进到下面的 connector.start () 方法

image-20231112134933761

这里和上面的 Engine 的启动实际上很像,这里还是跟进到这个 startInternal 这个方法中

image-20231112135632963

这里调用了这个 Connector#startInternal (),这里主要干两件事

  1. Connector 创建对象的无参构造器默认就指定了使用 http11protocolHandler
  2. 开启协议处理器 protocolHandler

这里跟进到 protocolHandler.start () 方法中

image-20231112135833720

调用到 AbstractProticol.start () 方法,这里主要做一个断点启动也就是 endpoint.start ();,继续跟进到这个方法

image-20231112140041573

还是跟进到 startInternal

image-20231112140831184

/**
 * 启动服务的内部方法
 * @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 就算是启动成功了,这里看启动过程中最后一个需要注意的方法

image-20231112142604847

调试回到这个 getServer ().start (); 方法,然后向下调试到 await () 方法

image-20231112142753765

跟进这个方法

image-20231112142811256

这里就到了,继续跟进这个 await 方法

image-20231112142955677

这个代码很长,这里直译一下注释

/**
 * 等待接收关闭命令的方法
 */
@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) {
                // 忽略
            }
        }
    }
}

简单总结一下有下面这个过程

  1. 如果配置的端口为负值 -2 ,则表示不需要等待端口,直接返回。
  2. 如果配置的端口为 -1 ,则进入轮询等待模式,每隔 10 秒轮询一次,直到 stopAwaittrue
  3. 如果配置的端口为正值,创建一个服务器套接字并等待连接,然后读取输入流中的字符,直到接收到与配置的关闭命令一致的字符串。
  4. 如果接收到关闭命令,记录信息并退出循环,表示关闭服务器。
  5. 如果未接收到关闭命令,记录警告信息并继续等待。

启动到这里也就算是结束了

image-20231113112206422

最后这里放一个这个部分的流程图

# 启动过程中加载 web.xml

本来这部分一个放在 Tomcat 的启动过程中来写的,但是这块东西稍微有点多,写在一块未免过于臃肿,所以这里单独拎出来写一个小节

image-20231114144251819

直接在这个 engine.start (); 这里下一个断点,还是和前面一样的步骤,跟进这里

image-20231114144653006

然后这里实际上启动的代码是 startInternal 这里方法里接着跟进

image-20231114144737712

然后这里调用了 StandardEngine 这个类的父类中的 startInternal 方法

image-20231114144854267

这次我们将目光看向这个 if 语句中,这段代码的作用是启动一个后台线程,定期执行 ContainerBackgroundProcessorMonitor 中的任务,用于处理一些周期性的工作。这里跟进到 scheduleWithFixedDelay 方法中

image-20231114145257875

这里会调用到这个 executor.scheduleWithFixedDelay,这里继续跟进

image-20231114145458218

这里调用到 ScheduledThreadPoolExecutor#scheduleWithFixedDelay 方法,这个方法用于以固定的延迟时间执行指定的任务。我们跟进到这个 delayedExecute 方法中

image-20231114145752921

调用本类的 delayedExecute 方法负责将任务添加到延迟队列中,并根据线程池的状态进行一些必要的处理,确保任务能够按照预期执行,继续跟进到 ensurePrestart () 方法

image-20231114150022212

调用到 ThreadPoolExecutor#ensurePrestart 确保线程池中至少有一个工作线程,以便在有任务提交时能够及时执行。跟进 addWorker 方法

image-20231114150121764

调用本类的 addwork 方法,这个方法也很明显就是想线程池中添加工作线程,这里我们跟进到 new worker 这个方法中

image-20231114150442189

这里调用 worker 类的构造方法,创建一个新的工作线程对象,这里跟进到 newThread

image-20231114150653957

在这个方法中创建新的线程对象,并对线程的一些属性进行设置,包括是否为守护线程、优先级以及上下文类加载器等这里,其中就是各种线程的启动,这里也不在过多的去关注。最后会调用 FutureTask#run 方法的 result = c.call (); 这里

image-20231114162110769

直接跟进这个方法

image-20231114162446347

这里会直接去调用 run,继续跟进

image-20231114162539811

这里是调用到了 HostConfig#run 方法

image-20231114162623222

这里调用 HostConfig#deployDirectory 方法,这个方法主要目的是在 Tomcat 中动态部署应用程序,并在运行时检测文件和配置的更改。跟进到这个方法中 host.addChild ()

image-20231114163113984

这个方法的主要作用是确保在 StandardHost 中添加的子容器是有效的 Context 对象,并且为新添加的 Context 注册了一个内存泄漏跟踪监听器。这有助于在开发和调试过程中更容易地发现和解决潜在的内存泄漏问题。这里跟进到这个 super.addChild (child); 方法中

image-20231114163757341

这就是一个 add,然后这里跟进到 addChildInternal 方法中

image-20231114163900627

方法主要负责在容器中添加子容器,并在添加完成后触发相应的事件

值得注意的是实际的启动子容器操作在同步块之外进行,以避免在同步块内执行耗时的操作,可能引发其他问题

触发就是这里的 child.start ();,这里继续跟进到这个方法

image-20231114164032987

start 方法的调用还是回到 LifecycleBase#start 方法,还是进到 startInternal (); 方法中

image-20231114164222755

启动组件,这里先跟进到这个 postWorkDirectory () 方法

image-20231114165528466

这里调用本类的 postWorkDirectory 方法,这里跟进到 getServletContext 方法中

image-20231114165629417

这个方法的主要目的是获取 Servlet 上下文对象,如果上下文对象不存在,则创建一个新的 ApplicationContext 对象。

image-20231114165909547

调试回到这个 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 处理器,用于遵循这一规范。

image-20231114170215281

一直到下面这里,跟进到这个 ((Lifecycle) loader).start (); 方法中

image-20231114170333148

还是到这个 LifecycleBase,这里跟进到 startInternal

image-20231114170431576

这个方法的主要任务是初始化和启动 WebappLoader 类加载器,同时记录适当的日志信息和注册 JMX 组件,这里更多就没有什么可以看的了,然后跳出这段代码

image-20231114170837065

这里调进这个方法

image-20231114170912405

这个方法的是为了允许子类在其生命周期内触发特定类型的事件,并通知已注册的监听器。然后跟进到这个 listener.lifecycleEvent (event); 方法

image-20231114171049540

这段代码实现了 LifecycleListener 接口的 lifecycleEvent 方法,该方法用于处理与 Lifecycle 事件相关的逻辑,跟进到这个 configureStart (); 方法

image-20231114171223442

处理上下文配置,跟进到 webconfig

image-20231114171918732

继续跟进到 configureContext 方法

image-20231114171959478

然后这里就是各种加载配置文件了

未命名文件

贴张图