Tomcat 版本

  • 独立部署的 Tomcat 版本 - 9.0.30
  • Spring boot 版本 - 2.2.4.RELEASE
  • 内嵌 Tomcat-embed-core - 9.0.30

Tomcat 概念论述

Tomcat 的架构 (也叫做 Catalina),是一个精密的层级结构系统。

  • Server - Tomcat 实例,一个 Tomcat 进程即为一个 Server;
  • Service - Tomcat 服务,Service 是 Tomcat 提供 Servlet 容器服务的单元,Service 在 Server 内部,一个 Server 可以有多个 Service;
  • Connector - 用户请求的主体,是 Servlet 容器与外部用户的交汇部分,Connector 在 Service 内,一个 Service 可以有多个 Connector 用以处理多种请求;
  • Engine - Engine 代表 Service 内部的 Servlet 容器,一个 Service 只有一个 Engine;
  • Host - Host 代表一个站点,一个 Engine 可以有多个 Host;
  • Context - Context 代表一个 app 应用,一个 Engine 可以有多个子工程组成,分配不同的路由;
  • Wrapper - Wrapper 即是 Servlet 容器,也代表着某一个 Servlet;
  • Container - Servlet 容器,上述的 Engine / Host / Context / Wrapper 严格意义上都是 Container;
  • Lifecycle - 生命周期,当有相关的事件发生的时候可以被相关的 LifecycleListener 监听到;
  • Global Naming Resources - 全局的配置资源;
  • Realm - 权限配置;
  • Executor - 资源池。

Tomcat 独立部署版配置文件

/conf/server.xml

<?xml version="1.0" encoding="UTF-8"?>

<!--
  Apache Tomcat v9.0.30 配置文件
 -->

<!-- Server 代表了一个 Tomcat 实例。
     className 是 Tomcat 实体类,必须使用 org.apache.catalina.Server 的子类,默认 org.apache.catalina.core.StandardServer;
     shutdown 代表关闭的指令;
     address 代表关闭指令可以的来源,默认只有本地的关闭指令 Tomcat 才会采用;
     port 是 Tomcat 关闭自身的指令接收端口,可以通过设置为 -1 来禁止 -->
<Server port="8005" shutdown="SHUTDOWN" address="localhost" className="org.apache.catalina.core.StandardServer" >

  <!-- 监听器配置,监听类必须是 org.apache.catalina.LifecycleListener 的子类,
      这里配置的类会根据自己监听的生命周期事件,被相关的 Lifecycle 的子类存入 list 中,当发生了相关事件就执行回调方法 -->

  <!-- 用于在服务启动的时候打印日志的监听器 -->
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- 默认开启 apr 监听器 -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- 内存不够的时候调用此监听器,执行一次 full gc -->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <!-- JNDI 全局变量管理监听器 -->
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <!-- 由于线程引用了 threadLocal 中的变量导致内存不够的时候调用此监听器,杀掉线程 -->
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- JNDI 全局资源配置,
      此处为一个 UserDatabase 对象,用来存储 tomcat-users.xml 文件中的信息 
  -->
  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <!-- Service 代表了一个 Tomcat 服务,一个 Server 可以有多个 Service,
      同一个 Server 下的 Service 的 name 必须不同, 
      className 必须使用 org.apache.catalina.Service 的子类,默认 org.apache.catalina.core.StandardService -->
  <Service name="Catalina" className="org.apache.catalina.core.StandardService">

    <!-- 连接池配置,可以不做配置,会有一个默认的连接池。
         namePrefix 线程名称;
         maxThreads 最大线程数,默认 200;
         maxQueueSize 任务队列最大值,默认 Integer.MAX;
         minSpareThreads 最小的活跃线程,默认 25;
         maxIdleTime 线程被销毁之前的存活时间,默认 60000 ms(1 min);
         deamon 池内的线程是否是守护线程,默认 true -->
     <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="200" minSpareThreads="25" maxQueueSize="100000000"
        deamon="true" maxIdleTime="60000"/>


    <!-- Connector 是相应用户请求的主体,一种 Connector 用于代表一种用户请求,
         通俗来说 Connector 是 Container 的前置工作对象,可以通俗理解为 Request 和 Response。
       port 监控的端口;
       protocol 使用的网络 io 连接器;
       connectionTimeout 连接超时时间,单位毫秒;
         executor 配置连接池;
       sslEnabled 是否允许 https;
         maxThreads 最大连接数;
         redirectPort 用户用 http 请求某个资源,而该资源本身又被设置了必须要 https 方式访问,会自动重定向到这个端口 -->
    <!-- protocol: 
         org.apache.coyote.http11.Http11NioProtocol  http1.1 nio 连接器
         org.apache.coyote.http11.Http11Nio2Protocol  http1.1 aio 连接器
         org.apache.coyote.http11.Http11AprProtocol  http1.1 apr 连接器,需要操作系统其它支持
         HTTP/1.1  http1.1 协议的自动选择选项,有 apr 就使用 apr,没有就使用 nio
         AJP/1.3  ajp1.3 协议的自动选择选项,有 apr 就使用 apr,没有就使用 nio -->
     <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
               redirectPort="8443" SSLEnabled="false" executor="tomcatThreadPool"
               maxThreads="150">

          <!-- http/2 配置,让这个 Connector 可以兼顾解析 http/2 -->
          <!-- <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol"/> -->
          <!-- 配置 https 证书 -->
          <!-- 
          <SSLHostConfig>
            <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                         certificateFile="conf/localhost-rsa-cert.pem"
                         certificateChainFile="conf/localhost-rsa-chain.pem"
                         type="RSA" />
          </SSLHostConfig> 
          -->
     </Connector>
     




    <!-- Engine 是 Servlet Container 的最高层级,一个 Service 中只能有一个 Engine。
         name 名称;
         defaultHost 默认的 host 地址 -->
    <Engine name="Catalina" defaultHost="localhost">

      <!-- Realm 提供用户名密码的映射,很少使用到,不做展开 -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
      </Realm>

      <!-- Host 虚拟主机,用以管理一个站点,一个站点可以有多个子项目构成。
           name 名称,同一个 Engine 下的 Host 不要重复即可,
           className 必须是 org.apache.catalina.Engine 的子类,默认 org.apache.catalina.core.StandardEngine,
           appBase 站点下属所有项目配置的目录;
           autoDeploy 是否自动扫描 appBase 目录下的所有的项目,如果配置为 true 的话,就可以无需配置 <Context></Context> 了;
           unpackWars 自动解压 war 包 -->
      <Host name="localhost"  appBase="webapps" className="org.apache.catalina.core.StandardEngine"
            unpackWARs="true" autoDeploy="true">

        <!-- 日志格式配置 -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

        <!-- Context 代表一个在 host appBase 路径下的 web 应用,
            一般情况下可以不配置,因为 host 会去扫描 appBase 下的所有文件夹找 web.xml。 
             此处将 /webapps/example 路径下的项目配置成根路由下的项目,
             将 /webapps/host-manager 路径下的项目配置成 /manager 路由下的项目,以此类推。 -->
        <Context docBase="examples" path="/" />
        <Context docBase="host-manager" path="/manager" />
        <Context docBase="ROOT" path="/root" />
        

      </Host>
    </Engine>
  </Service>
</Server>

Tomcat 部分组件源码简读

该部分的解析使用 tomcat-embed-core 进行源码观察。

一 Lifecycle

org.apache.catalina.Lifecycle 是一个接口,org.apache.catalina.util.LifecycleBase 实现了 Lifecycle 接口,而 Server / Service / Engine / Host / Context / Wrapper 等组件都继承了 LifecycleBase,也就是说这些组件在发生生命周期事件的时候都可以被监听器监控。

1 监控事件

event 是 Lifecycle 定义的能够被监控的生命周期事件。

// 组件初始化即将开始
public static final String BEFORE_INIT_EVENT = "before_init";
// 组件初始化完成之后
public static final String AFTER_INIT_EVENT = "after_init";
// 组件启动中
public static final String START_EVENT = "start";
// 组件即将启动
public static final String BEFORE_START_EVENT = "before_start";
// 组件启动完成
public static final String AFTER_START_EVENT = "after_start";
// 组件停止中
public static final String STOP_EVENT = "stop";
// 组件即将停止
public static final String BEFORE_STOP_EVENT = "before_stop";
// 组件停止完成
public static final String AFTER_STOP_EVENT = "after_stop";
// 组件即将被摧毁
public static final String AFTER_DESTROY_EVENT = "after_destroy";
// 组件摧毁完成
public static final String BEFORE_DESTROY_EVENT = "before_destroy";
// 组件的周期性事件
public static final String PERIODIC_EVENT = "periodic";
// 组件开始根据配置文件配置自身,在 before_start 之后,start 之前
public static final String CONFIGURE_START_EVENT = "configure_start";
// 组件配置结束,在 stop 之后,after_stop 之前
public static final String CONFIGURE_STOP_EVENT = "configure_stop";

2 组件状态

组件状态被定义在 org.apache.catalina.LifecycleState 中,是一个枚举类。

public enum LifecycleState {
    // 新建
    NEW(false, null),
    // 正在初始化
    INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT),
    // 初始化完成
    INITIALIZED(false, Lifecycle.AFTER_INIT_EVENT),
    // 准备开始组件
    STARTING_PREP(false, Lifecycle.BEFORE_START_EVENT),
    // 正在执行
    STARTING(true, Lifecycle.START_EVENT),
    // 执行结束了
    STARTED(true, Lifecycle.AFTER_START_EVENT),
    // 准备停止组件
    STOPPING_PREP(true, Lifecycle.BEFORE_STOP_EVENT),
    // 正在停止中
    STOPPING(false, Lifecycle.STOP_EVENT),
    // 停止了
    STOPPED(false, Lifecycle.AFTER_STOP_EVENT),
    // 正在被销毁
    DESTROYING(false, Lifecycle.BEFORE_DESTROY_EVENT),
    // 被销毁之后
    DESTROYED(false, Lifecycle.AFTER_DESTROY_EVENT),
    // 失败
    FAILED(false, null);
    
    private final boolean available; // 是否可控制,如果为 false 
    private final String lifecycleEvent; // 此状态绑定的监控事件

// ...
}

代码内容很简洁。

3 LifecycleBase

LifecycleBase 是 Lifecycle 的基本实现类,以 init() 方法举例:

// LifecycleBase.class
@Override
public final synchronized void init() throws LifecycleException {
    // 调用 init 方法需要确保这个组件的状态是 new,如果不是的话会抛出错误
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }

    try {
        // before_init 监听通知
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        // 真正的初始化逻辑
        initInternal();
        // after_init 监听通知
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        handleSubClassException(t, "lifecycleBase.initFail", toString());
    }
}

与监听器进行交互的核心方法是 addLifecycleListener(...) 与 setStateInternal(...):

// LifecycleBase.class
@Override
public void addLifecycleListener(LifecycleListener listener) {
    // lifecycleListeners 是一个存放监听器的列表
    lifecycleListeners.add(listener);
}

// LifecycleBase.class
// 此方法用于通知所有的监听器相关逻辑
private synchronized void setStateInternal(
    LifecycleState state, Object data, boolean check) throws LifecycleException {

    // 记录日志
    if (log.isDebugEnabled()) {
        log.debug(sm.getString("lifecycleBase.setState", this, state));
    }

    // 是否启用状态判断
    if (check) {
    
        // 传入的状态值的非空判断,非法操作,一般不会出现
        if (state == null) {
            invalidTransition("null");
            return;
        }

        // 如果状态是如下几种的话,会抛出错误
        if (!(state == LifecycleState.FAILED ||
            (this.state == LifecycleState.STARTING_PREP 
                && state == LifecycleState.STARTING) 
                || (this.state == LifecycleState.STOPPING_PREP 
                && state == LifecycleState.STOPPING) 
                || (this.state == LifecycleState.FAILED 
                && state == LifecycleState.STOPPING))
        ) {
            invalidTransition(state.name());
        }
    }

    // 更新状态
    this.state = state;
    String lifecycleEvent = state.getLifecycleEvent();
    if (lifecycleEvent != null) {
        // 此处会遍历监听器
        fireLifecycleEvent(lifecycleEvent, data);
    }
}

// LifecycleBase.class
protected void fireLifecycleEvent(String type, Object data) {
    // 遍历监听器列表,执行所有监听器的 lifecycleEvent(...) 方法
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

4 LifecycleListener

所有实现了 org.apache.catalina.LifecycleListener 接口的类都可以是监听器:

public interface LifecycleListener {
    public void lifecycleEvent(LifecycleEvent event);
}

以最简单的 org.apache.catalina.startup.VersionLoggerListener 为例子:

// VersionLoggerListener.class
@Override
public void lifecycleEvent(LifecycleEvent event) {
    // Tomcat 中的 Listener 一般都使用 if 判断进行事件的筛选
    // Tomcat 没有在接口层面作出更加强制的监听逻辑判断
    if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {
        // 打印日志
        log();
    }
}

Spring boot 中与 Servlet 相关的配置

Spring boot 中与此相关的配置比较多,只列举部分笔者在项目中常用的部分。

server:
  # 端口
  port: 8080
  # http 协议头的大小
  max-http-header-size: 8KB

  # servlet 设置
  servlet:
    session:
      # session 失效时常,默认 30min
      timeout: 30m
      # 是否持久化 session,持久化之后 session 不会因为服务的重启而丢失
      persistent: false
      # 如果设置为需要持久化,那么可以指定存储的目录
      # store-dir: classpath:session

  # 配置 Tomcat 相关的参数
  tomcat:
    # 最大线程数
    max-threads: 2
    # 最大连接数,默认 200
    max-connections: 10
    # encode
    uri-encoding: UTF-8
    # 连接超时时间
    connection-timeout: 2m
    # 最大连接排队数
    accept-count:
    # 提交表单的最大大小
    max-http-form-post-size: 2MB

  # 是否支持 http2
  http2.enabled: false
  
  

spring:
  # Spring 网络配置
  http:
    encoding:
      # 编码格式
      charset: UTF-8
      enabled: true
      force: true

  # servlet 配置
  servlet:
    # 文件上传配置
    multipart:
      # 是否支持文件上传
      enabled: true
      # 上传的文件的最大值
      max-file-size: 100MB
      # request 的最大值
      max-request-size: 100MB

Spring boot 内嵌 Tomcat 的启动

Spring boot 对内嵌服务器的运用,会使用 ServletWebServerFactory :

// 接口
public interface ServletWebServerFactory {
    WebServer getWebServer(ServletContextInitializer... initializers);
}

它的具体实现类有:

JettyServletWebServerFactory - eclipse jetty 内嵌服务对象的工厂类
TomcatServletWebServerFactory - apache tomcat 内嵌服务对象的工厂类
UndertowServletWebServerFactory - jboss undertow 内嵌服务对象的工厂类

本例中由于默认内嵌 Tomcat 服务包,所以 Spring boot 会使用 TomcatServletWebServerFactory:

// step 1
// TomcatServletWebServerFactory.class
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    Tomcat tomcat = new Tomcat();
    File baseDir 
    = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
    
    // Tomcat 配置 baseDir,如果不配置的话会创建一个临时目录,用来存放一些 log 文件等临时文件
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    // 创建 Connector,并存入 protocol
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    // 在 Tomcat 的 service 中存入 connector,由于是内嵌的 Tomcat,所以 service 只允许有一个
    tomcat.getService().addConnector(connector);
    // 根据配置对 connector 进行配置
    customizeConnector(connector);
    // 本质上这行代码和上述 tomcat.getService().addConnector(connector) 的功能是一样的,不太理解为什么又重新 set 了一遍
    tomcat.setConnector(connector);
    // 关闭 host 的自动扫描
    tomcat.getHost().setAutoDeploy(false);
    // 配置 engine
    configureEngine(tomcat.getEngine());
    // 添加一些默认的 connector,一般是空的
    for (Connector additionalConnector 
        : this.additionalTomcatConnectors) {
        tomcat.getService().addConnector(additionalConnector);
    }
    
    // ServletContextInitializer 用于封装 servlet 的配置信息,并将 servlet 注册到 context 上
    // 此处会创建一个 context 对象,并注册到 host 中
    // 由于 spring mvc 内部实际上只有一个 dispatcher servlet,所以此处的数组一般只有一个
    prepareContext(tomcat.getHost(), initializers);
    
    // 用一个 TomcatWebServer 对象包装原生的 Tomcat 对象
    return getTomcatWebServer(tomcat);
}

// step 2
// TomcatServletWebServerFactory.class
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
    return new TomcatWebServer(tomcat, getPort() >= 0);
}

// step 3
// TomcatWebServer.class
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
    Assert.notNull(tomcat, "Tomcat Server must not be null");
    this.tomcat = tomcat;
    this.autoStart = autoStart;
    initialize();
}

// step 4
// TomcatWebServer.class
private void initialize() throws WebServerException {
    // 记录日志
    logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
    synchronized (this.monitor) {
        try {
            addInstanceIdToEngineName();

            // 给 context 增加生命周期监听器
            Context context = findContext();
            context.addLifecycleListener((event) -> {
                if (context.equals(event.getSource()) 
                        && Lifecycle.START_EVENT.equals(event.getType())) {
                    removeServiceConnectors();
                }
            });

            // 此处启动 tomcat
            this.tomcat.start();
            // 此处检测一下是否启动成功,如果失败了就直接抛出错误
            rethrowDeferredStartupExceptions();

            try {
                ContextBindings.bindClassLoader(
                    context, context.getNamingToken(), getClass().getClassLoader());
            } catch (NamingException ex) { }
            
            // 启动一条主线程
            // 由于 Tomcat 的所有现场都默认是守护线程,所以需要一条非守护线程来确保项目不退出
            startDaemonAwaitThread();
        } catch (Exception ex) {
            stopSilently();
            destroySilently();
            throw new WebServerException("Unable to start embedded Tomcat", ex);
        }
    }
}

三流
57 声望16 粉丝

三流程序员一枚,立志做保姆级教程。