Tomcat 学习归整

Tomcat 版本

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

Tomcat 概念论述

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

  • 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.xmlweb

<?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 进行源码观察。spring

一 Lifecycle

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

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 中,是一个枚举类。tomcat

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(...):session

// 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);
        }
    }
}
相关文章
相关标签/搜索