1. 程式人生 > >死磕Tomcat系列(5)——容器

死磕Tomcat系列(5)——容器

容器的整體設計

Container是容器的父介面,所有子容器都需要實現此介面,我們首先看一下Container介面的設計。

public interface Container extends Lifecycle {
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public void removeChild(Container child);
    public Container findChild(String name);
}

Tomcat是如何管理這些容器的呢?我們可以通過介面的設計可以瞭解到是通過設定父子關係,形成一個樹形的結構(一父多子)、鏈式結構(一父一子)來管理的。一想到樹形的結構我們應該就立馬能夠聯想到設計模式中的組合模式,而鏈式結構我們應該能夠想到設計模式中的責任鏈設計模式。無論這兩種的哪一種我們都知道這種關係是上下層級的關係。用圖來表示就是如下。

既然是父子的結構,那麼聯結器是如何將轉換好的ServletRequest給到容器的呢?我們可以看CoyoteAdapter中的service方法。因為在聯結器中最後一環是將解析過的Request給到Adapter運用介面卡設計模式解析為ServletRequest

物件。在service方法中我們看到有這麼一句。

connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);

而其中的getContainer方法,返回的是Engine物件

public Engine getContainer();

這裡看到了PipelinePipeline應該大家有所熟悉,是管道的概念,那麼管道里面裝的是什麼呢?我們看其定義的方法

public interface Pipeline extends Contained {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

可以看到Pipeline管道里面裝的是Valve,那麼Valve是如何組織起來的呢?我們也可以看它的程式碼定義

public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

可以知道每個Valve都是一個處理點,它的invoke就是相對應的處理邏輯。可以看到有setNext的方法,因此我們大概能夠猜到是通過連結串列將Valve組織起來的。然後將此Valve裝入Pipeline中。因此每個容器都有一個Pipeline,裡面裝入系統定義或者自定義的一些攔截節點來做一些相應的處理。因此只要獲得了容器中Pipeline管道中的第一個Valve物件,那麼後面一系列鏈條都會執行到。

但是不同容器之間Pipeline之間是如何進行觸發的呢?即例如EnginePipeline處理完了最後一個Valve,那麼如何呼叫HostPipeLine管道中的Valve呢?我們可以看到每個Pipeline中還有一個方法。setBasic這個方法設定的就是Valve鏈條的末端節點是什麼,它負責呼叫底層容器的Pipeline第一個Valve節點。用圖表示就是這樣的。

Engine容器

Engine容器比較簡單,只是定義了一些基本的關聯關係。它的實現類是StandardEngine

    @Override
    public void addChild(Container child) {
        if (!(child instanceof Host))
            throw new IllegalArgumentException
                (sm.getString("standardEngine.notHost"));
        super.addChild(child);
    }
    @Override
    public void setParent(Container container) {
        throw new IllegalArgumentException
            (sm.getString("standardEngine.notParent"));

    }

需要注意Engine容器是沒有父容器的。如果新增是會報錯。新增子容器也只是能新增Host容器。

Host 容器

Host容器是Engine的子容器,一個Host在Engine中代表一個虛擬主機,這個虛擬主機的作用就是執行多個應用,它負責安裝和展開這個應用,並且標識這個應用以便能夠區分它們。它的子容器通常是Context容器。我們可以看配置檔案中也能夠看出Host檔案的作用。

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

那麼Host容器在啟動時具體幹了什麼呢?我們看它的startInternal方法看不出來什麼,只是啟動了相應的Valve,是因為在Tomcat的設計中引入了生命週期的概念,即每個模組都有自己相應的生命週期,模組的生命週期定義有NEW、INITIALIZING、INITIALIZED、SSTARTING_PREP、STARTING、STARTED,每個模組狀態的變化都會引發一系列的動作,那麼這些動作的執行是直接寫在startInternal中嗎?這樣會違反開閉原則,那麼如何解決這個問題呢?開閉原則說的是為了擴充套件性系統的功能,你不能修改系統中現有的類,但是你可以定義新的類。

於是每個模組狀態的變化相當於一個事件的發生,而事件是有相應的監聽器的。在監聽器中實現具體的邏輯,監聽器也可以方便的增加和刪除。這就是典型的觀察者模式。

那麼Host容器在啟動的時候需要掃描webapps目錄下面的所有Web應用,建立相應的Context容器。那麼Host的監聽器就是HostConfig,它實現了LifecycleListener介面

public interface LifecycleListener {

    public void lifecycleEvent(LifecycleEvent event);
}

介面中只定義了一個方法,即監聽到相應事件的處理邏輯。可以看到在setState方法中呼叫了監聽器的觸發。

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

所以容器中各元件的具體處理邏輯是在監聽器中實現的。

Context 容器

一個Context對應一個Web應用

Context代表的是Servlet的Context,它具備了Servlet的執行的基本環境。Context最重要的功能就是管理它裡面的Servlet例項,Servlet例項在Context中是以Wrapper出現的。Context準備執行環境是在ContextConfiglifecycleEvent方法準備的。

@Override
public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }
}

Wrapper容器

Wrapper容器代表一個Servlet,包括Servlet的裝載、初始化、執行以及資源的回收。Wrapper是最底層的容器,它沒有子容器。

Wrapper的實現類是StandardWrapper,主要任務是載入Servlet類,並進行例項化。但是StandardWrapper類並不會呼叫Servletservice方法。而是StandardWrapperValue類通過呼叫StandardWrpperallocate方法獲得相應的servlet,然後通過攔截器的過濾之後才會呼叫相應的Servlet的service方法

總結

Tomcat的容器中有許多值得我們學習的設計思想,例如將不變的抽取出來,然後變化的子類來實現的模板設計模式、維護一堆父子關係的組合設計模式、事件的發生伴隨監聽者的相應動作執行的觀察者設計模式等等。

在學習框架的時候,有時沒必要深究裡面一行一行的程式碼,而要學習它的思想。知道它是如何執行,隨後如果查詢問題,或者是對框架進行相應擴充套件。這時候再深入學習裡面的程式碼將會事半功倍。

參考文章

往期文章

如何斷點除錯Tomcat原始碼

死磕Tomcat系列(1)——整體架構

死磕Tomcat系列(2)——EndPoint原始碼解析

死磕Tomcat系列(3)——Tomcat如何做到一鍵式啟停的

死磕Tomcat系列(4)——Tomcat中的類載入器

一次奇怪的StackOverflowError問題查詢之旅

徒手擼一個簡單的RPC框架

徒手擼一個簡單的RPC框架(2)