架構篇:Tomcat 高層組件構建一個商業帝國

友情提示:閱讀本文前需要對 Tomcat 有一個全局架構認識,可先翻閱我的歷史文章:Tomcat 架構解析到設計思想借鑑

Tomcat 架構解析到設計思想借鑑中我們學到 Tomcat 的總體架構,學會從宏觀上怎麼去設計一個複雜系統,怎麼設計頂層模塊,以及模塊之間的關係;

Tomcat 實現的 2 個核心功能:

所以 Tomcat 設計了兩個核心組件連接器(Connector)和容器(Container),連接器負責對外交流,容器負責內部處理。

Tomcat 整體架構

本篇作爲 Tomcat 系列的第三篇,帶大家體會 Tomcat 帝國是如何構建的?高層組件如何管理組件的?連接器容器是如何被啓動和管理的?

Tomcat 啓動流程:startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()

Tomcat 啓動流程

Bootstrap、Catalina、Server、Service、 Engine 都承擔了什麼責任?

單獨寫一篇介紹他們是因爲你可以看到這些啓動類或者組件不處理具體請求,它們的任務主要是管理管理下層組件的生命週期並且給下層組件分配任務,也就是把請求路由到負責幹活兒的組件。

他們就像一個公司的高層,管理整個公司的運作,將任務分配給專業的人。

我們在設計軟件系統中,不可避免的會遇到需要一些管理作用的組件,就可以學習和借鑑 Tomcat 是如何抽象和管理這些組件的。

因此我把它們比作 Tomcat 的高層,同時願幹活的不再 996

Bootstrap

當執行 startup.sh 腳本的時候,就會啓動一個 JVM 運行 Tomcat 的啓動類 Bootstrapmain 方法。

先看下他的成員變量窺探核心功能:

public final class Bootstrap {
    ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null;

它的主要任務就是初始化 Tomcat 定義的類加載器,同時創建 Catalina 對象

Bootstrap 就像一個大神,初始化了類加載器,加載萬物。

關於爲何自定義各種類加載器詳情請查看碼哥的 Tomcat 架構設計解析 類加載器部分。

初始化類加載器

WebAppClassLoader

假如我們在 Tomcat 中運行了兩個 Web 應用程序,兩個 Web 應用中有同名的 Servlet,但是功能不同,Tomcat 需要同時加載和管理這兩個同名的 Servlet類,保證它們不會衝突,因此 Web 應用之間的類需要隔離。

Tomcat 的解決方案是自定義一個類加載器 WebAppClassLoader, 並且給每個 Web 應用創建一個類加載器實例

我們知道,Context 容器組件對應一個 Web 應用,因此,每個 Context容器負責創建和維護一個 WebAppClassLoader加載器實例

這背後的原理是,不同的加載器實例加載的類被認爲是不同的類,即使它們的類名相同。

Tomcat 的自定義類加載器 WebAppClassLoader打破了雙親委託機制,它首先自己嘗試去加載某個類,如果找不到則通過 ExtClassLoader 加載 JRE 核心類防止黑客攻擊,無法加載再代理給 AppClassLoader 加載器,其目的是優先加載 Web 應用自己定義的類。

具體實現就是重寫 ClassLoader的兩個方法:findClassloadClass

SharedClassLoader

假如兩個 Web 應用都依賴同一個第三方的 JAR 包,比如 Spring,那 Spring的 JAR 包被加載到內存後,Tomcat要保證這兩個 Web 應用能夠共享,也就是說 Spring的 JAR 包只被加載一次。

SharedClassLoader 就是 Web 應用共享的類庫的加載器,專門加載 Web 應用共享的類。

如果  WebAppClassLoader自己沒有加載到某個類,就會委託父加載器 SharedClassLoader去加載這個類,SharedClassLoader會在指定目錄下加載共享類,之後返回給 WebAppClassLoader,這樣共享的問題就解決了。

CatalinaClassloader

如何隔離 Tomcat 本身的類和 Web 應用的類?

要共享可以通過父子關係,要隔離那就需要兄弟關係了。

兄弟關係就是指兩個類加載器是平行的,它們可能擁有同一個父加載器,基於此 Tomcat 又設計一個類加載器 CatalinaClassloader,專門來加載 Tomcat 自身的類。

這樣設計有個問題,那 Tomcat 和各 Web 應用之間需要共享一些類時該怎麼辦呢?

老辦法,還是再增加一個 CommonClassLoader,作爲 CatalinaClassloader和  SharedClassLoader 的父加載器。

CommonClassLoader能加載的類都可以被  CatalinaClassLoaderSharedClassLoader 使用。

Catalina

Tomcat 是一個公司,Catalina 就好像是一個創始人。因爲它負責組建團隊,創建 Server 以及所有子組件。

Catalina 的主要任務就是創建 Server,解析 server.xml 把裏面配置的各個組件創建出來,並調用每個組件的 initstart方法,將整個 Tomcat 啓動,這樣整個公司就在正常運作了。

我們可以根據 Tomcat 配置文件來直觀感受下:

<Server port="8005" shutdown="SHUTDOWN"> // 頂層組件,可包含多個 Service,代表一個 Tomcat 實例

  <Service >  // 頂層組件,包含一個 Engine ,多個連接器
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />  // 連接器

 // 容器組件:一個 Engine 處理 Service 所有請求,包含多個 Host
    <Engine >
   // 容器組件:處理指定Host下的客戶端請求, 可包含多個 Context
      <Host 
            unpackWARs="true" autoDeploy="true">
   // 容器組件:處理特定 Context Web應用的所有客戶端請求
   <Context></Context>
      </Host>
    </Engine>
  </Service>
</Server>

作爲創始人,Catalina 還需要處理公司的各種異常情況,比如有人搶公章(執行了 Ctrl + C 關閉 Tomcat)。

Tomcat 要如何清理資源呢?

通過向 JVM 註冊一個「關閉鉤子」,具體關鍵邏輯詳見

org.apache.catalina.startup.Catalina#start 源碼:

  1. Server 不存在則解析 server.xml 創建;

  2. 創建失敗則報錯;

  3. 啓動 Server;

  4. 創建並註冊「關閉鉤子」;

  5. await 方法監聽停止請求。

   /**
     * Start a new server instance.
     */
    public void start() {

        // 如果 Catalina 持有的 Server 爲空則解析 server.xml 創建
        if (getServer() == null) {
            load();
        }

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        // Start the new server
        try {
            getServer().start();
        } catch (LifecycleException e) {
            // 省略部分代碼
        }

        // 創建鉤子並註冊
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);
        }

        // 監聽停止請求,內部調用 Server 的 stop
        if (await) {
            await();
            stop();
        }
    }

當我們需要在 JVM 關閉做一些清理工作,比如將緩存數據刷到磁盤或者清理一些文件,就可以向 JVM 註冊一個「關閉鉤子」。

它其實就是一個線程,當 JVM 停止前嘗試執行這個線程的 run 方法。

org.apache.catalina.startup.Catalina.CatalinaShutdownHook

    protected class CatalinaShutdownHook extends Thread {

        @Override
        public void run() {
            try {
                if (getServer() != null) {
                    Catalina.this.stop();
                }
            } catch (Throwable ex) {
              // 省略部分代碼....
            }
        }
    }

其實就是執行了 Catalina 的 stop 方法,通過它將整個 Tomcat 停止。

Server

Server 組件的職責就是管理 Service 組件,負責調用持有的 Servicestart 方法。

他就像是公司的 CEO,負責管理多個事業部,每個事業部就是一個 Service

它管理兩個部門:

實現類是 org.apache.catalina.core.StandardServer,Server 繼承 org.apache.catalina.util.LifecycleMBeanBase,所以他的生命週期也被統一管理,Server 的子組件是 Service,所以還需要管理 Service 的生命週期。

也就是說在啓動和關閉 Server 的時候會分別先調用 Service 的 啓動和停止方法。

這就是設計思想呀,抽象出生命週期 Lifecycle 接口,體現出接口隔離原則,將生命週期的相關功能內聚。

我們接着看 Server 如何管理 Service 的,核心源碼如下 org.apache.catalina.core.StandardServer#addService:

public void addService(Service service) {

        service.setServer(this);

        synchronized (servicesLock) {
            // 創建 長度 +1 的數組
            Service results[] = new Service[services.length + 1];
            // 將舊的數據複製到新數組
            System.arraycopy(services, 0, results, 0, services.length);
            results[services.length] = service;
            services = results;
            // 啓動 Service 組件
            if (getState().isAvailable()) {
                try {
                    service.start();
                } catch (LifecycleException e) {
                    // Ignore
                }
            }

            // 發送事件
            support.firePropertyChange("service", null, service);
        }

    }

在添加 Service 過程中動態拓展數組長度,爲了節省內存。

除此之外,Server 組件還有一個重要的任務是啓動一個 Socket 來監聽停止端口,這就是爲什麼你能通過 shutdown 命令來關閉 Tomcat。

不知道你留意到沒有,上面 Caralina 的啓動方法的最後一行代碼就是調用了 Server 的 await 方法。

在 await 方法裏會創建一個 Socket 監聽 8005 端口,並在一個死循環裏接收 Socket 上的連接請求,如果有新的連接到來就建立連接,然後從 Socket 中讀取數據;如果讀到的數據是停止命令 “SHUTDOWN”,就退出循環,進入 stop 流程。

Service

他的職責就是管理 Connector 連接器頂層容器 Engine,會分別調用他們的 start 方法。至此,整個 Tomcat 就算啓動完成了。

Service 就是事業部的話事人,管理兩個職能部門對外推广部(連接器),對內研發部(容器)。

Service 組件的實現類是org.apache.catalina.core.StandardService,直接看關鍵的成員變量。

public class StandardService extends LifecycleMBeanBase implements Service {
    // 名字
    private String name = null;
    
    // 所屬的 Server 實例
    private Server server = null;
 
    // 連接器數組
    protected Connector connectors[] = new Connector[0];
    private final Object connectorsLock = new Object();
 
    // 對應的 Engine 容器
    private Engine engine = null;
    
    // 映射器及其監聽器
    protected final Mapper mapper = new Mapper();
    protected final MapperListener mapperListener = new MapperListener(this);

繼承 LifecycleMBeanBase 而  LifecycleMBeanBase 又繼承 LifecycleBase,這裏實際上是模板方法模式的運用,org.apache.catalina.util.LifecycleBase#initorg.apache.catalina.util.LifecycleBase#startorg.apache.catalina.util.LifecycleBase#stop 分別是對應的模板方法,內部定義了整個算法流程,子類去實現自己內部具體變化部分,將變與不變抽象出來實現開閉原則設計思路。

那爲什麼還有一個 MapperListener?這是因爲 Tomcat 支持熱部署,當 Web 應用的部署發生變化時,Mapper 中的映射信息也要跟着變化,MapperListener 就是一個監聽器,它監聽容器的變化,並把信息更新到 Mapper 中,這是典型的觀察者模式。

作爲 “管理” 角色的組件,最重要的是維護其他組件的生命週期。

此外在啓動各種組件時,要注意它們的依賴關係,也就是說,要注意啓動的順序。我們來看看 Service 啓動方法:

protected void startInternal() throws LifecycleException {
 
    //1. 觸發啓動監聽器
    setState(LifecycleState.STARTING);
 
    //2. 先啓動 Engine,Engine 會啓動它子容器
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }
    
    //3. 再啓動 Mapper 監聽器
    mapperListener.start();
 
    //4. 最後啓動連接器,連接器會啓動它子組件,比如 Endpoint
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }
}

這裏啓動順序也很講究,Service 先啓動了 Engine 組件,再啓動 Mapper 監聽器,最後纔是啓動連接器。

這很好理解,因爲內層組件啓動好了才能對外提供服務,產品沒做出來,市場部也不能瞎忽悠,研發好了才能啓動外層的連接器組件。

而 Mapper 也依賴容器組件,容器組件啓動好了才能監聽它們的變化,因此 Mapper 和 MapperListener 在容器組件之後啓動。

組件停止的順序跟啓動順序正好相反的,也是基於它們的依賴關係。

Engine

他就是一個研發部的頭頭,是最頂層的容器組件。繼承 Container,所有的容器組件都繼承 Container,這裏實際上運用了組合模式統一管理。

他的實現類是 org.apache.catalina.core.StandardEngine,繼承 ContainerBase

public class StandardEngine extends ContainerBase implements Engine {
}

他的子容器是 Host,所以持有 Host 容器數組,這個屬性每個容器都會存在,所以放在抽象類中

protected final HashMap<String, Container> children = new HashMap<>();

ContainerBase 用 HashMap 保存了它的子容器,並且 ContainerBase 還實現了子容器的 “增刪改查”,甚至連子組件的啓動和停止都提供了默認實現,比如 ContainerBase 會用專門的線程池來啓動子容器。

org.apache.catalina.core.ContainerBase#startInternal

// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
  results.add(startStopExecutor.submit(new StartChild(child)));
}

Engine 在啓動 Host 子容器時就直接重用了這個方法。

容器組件最重要的功能是處理請求,而 Engine 容器對請求的 “處理”,其實就是把請求轉發給某一個 Host 子容器來處理,具體是通過 Valve 來實現的。

每一個容器組件都有一個 Pipeline,而 Pipeline 中有一個基礎閥(Basic Valve),透過構造方法創建 Pipeline。

public StandardEngine() {

    super();
    pipeline.setBasic(new StandardEngineValve());
    // 省略部分代碼

}

Engine 容器的基礎閥定義如下:

final class StandardEngineValve extends ValveBase {
 
    public final void invoke(Request request, Response response)
      throws IOException, ServletException {
  
      // 拿到請求中的 Host 容器
      Host host = request.getHost();
      if (host == null) {
          return;
      }
  
      // 調用 Host 容器中的 Pipeline 中的第一個 Valve
      host.getPipeline().getFirst().invoke(request, response);
  }
  
}

這個基礎閥實現非常簡單,就是把請求轉發到 Host 容器。

從代碼中可以看到,處理請求的 Host 容器對象是從請求中拿到的,請求對象中怎麼會有 Host 容器呢?

這是因爲請求到達 Engine 容器中之前,Mapper 組件已經對請求進行了路由處理,Mapper 組件通過請求的 URL 定位了相應的容器,並且把容器對象保存到了請求對象中。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/224RymA-gHgqyTuAWLxUpQ