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);
        }
    }
}
相關文章
相關標籤/搜索