tomcat&jetty筆記1

Tomcat的總體架構

兩個核心功能:

  1. 處理socket連接,負責網絡字節流和Request和Response對象的轉化
  2. 加載和管理Servlet,以及具體處理Request請求

設計了兩個核心組件連接器connector和容器container分別做着兩件事,container內部處理,connector對外交流。

連接器的設計

Tomcat支持的IO模型:

  1. NIO非阻塞IO,採用java NIO類庫實現
  2. NIO2 異步IO,採用jdk7的NIO2類庫實現
  3. APR 採用Apache可移植運行庫實現,C/C++編寫的本地庫

Tomcat支持的應用層協議:

  1. HTTP/1 大部分web應用採用的訪問協議
  2. AJP 用於和web服務器集成
  3. HTTP/2 2.0大幅度提升了web性能

 

一個容器可能對接多個連接器,但是單獨的連接器或者容器都不能對外提供服務,需要把它們組裝起來才能工作,組裝後這個整體叫作Service組件。Service本身沒有做什麼重要的事情,只是在連接器和容器外面多包了一層,把它們組裝在一起。Tomcat內可能有多個Service,這樣的設計也是出於靈活性的考慮。通過在Tomcat中配置多個Service,可以實現通過不同的端口號來訪問同一臺機器上部署的不同應用。

Server指的就是一個Tomcat實例。一個Server中有一個或者多個Service,一個Service中有多個連接器和一個容器。連接器與容器之間通過標準的ServletRequest和ServletResponse通信。

連接器

連接器對Servlet容器屏蔽了協議及I/O模型等的區別,無論是HTTP還是AJP,在容器中獲取到的都是一個標準的ServletRequest對象。

連接器的功能需求進一步細化:監聽網絡端口;接受網絡連接請求;讀取請求網絡字節流;根據具體應用層協議解析字節流,生成統一的tomcat request對象;將tomcat request對象轉成標準的ServletRequest;調用Servlet容器,得到ServletResponse;將ServletResponse轉成tomcat response對象;將tomcat response轉成網絡字節流;將響應字節流寫回給瀏覽器。

3個高內聚的功能:網絡通信;應用層協議解析;Tomcat Request/Response與ServletRequest/ServletResponse的轉化。

設計了三個組件實現3個功能,endpoint,processor,adaptor。組件之間通過抽象接口交互,封裝變化。

由於I/O模型和應用層協議可以自由組合,比如NIO + HTTP或者NIO2 + AJP。Tomcat的設計者將網絡通信和應用層協議解析放在一起考慮,設計了一個叫ProtocolHandler的接口來封裝這兩種變化點。各種協議和通信模型的組合有相應的具體實現類。比如:Http11NioProtocol和AjpNioProtocol。

設計了一系列抽象基類來封裝這些穩定的部分,抽象基類AbstractProtocol實現了ProtocolHandler接口。每一種應用層協議有自己的抽象基類,比如AbstractAjpProtocol和AbstractHttp11Protocol,具體協議的實現類擴展了協議層抽象基類。

EndPoint是通信端點,即通信監聽的接口,是具體的Socket接收和發送處理器,是對傳輸層的抽象,因此EndPoint是用來實現TCP/IP協議的。EndPoint是一個接口,它的抽象實現類AbstractEndpoint裏面定義了兩個內部類:Acceptor和SocketProcessor。其中Acceptor用於監聽Socket連接請求。SocketProcessor用於處理接收到的Socket請求,它實現Runnable接口,在Run方法裏調用協議處理組件Processor進行處理。爲了提高處理能力,SocketProcessor被提交到線程池來執行。而這個線程池叫作執行器(Executor)。

Processor用來實現HTTP協議,Processor接收來自EndPoint的Socket,讀取字節流解析成Tomcat Request和Response對象,並通過Adapter將其提交到容器處理,Processor是對應用層協議的抽象。Processor是一個接口,定義了請求的處理等方法。它的抽象實現類AbstractProcessor對一些協議共有的屬性進行封裝,沒有對方法進行實現。具體的實現有AJPProcessor、HTTP11Processor等,這些具體實現類實現了特定協議的解析方法和請求處理方式。

EndPoint接收到Socket連接後,生成一個SocketProcessor任務提交到線程池去處理,SocketProcessor的Run方法會調用Processor組件去解析應用層協議,Processor通過解析生成Request對象後,會調用Adoptor的Service方法。

Adaptor組件

 

Tomcat定義了自己的Request類來“存放”這些請求信息。ProtocolHandler接口負責解析請求並生成Tomcat Request類。但是這個Request對象不是標準的ServletRequest,也就意味着,不能用Tomcat Request作爲參數來調用容器。Tomcat設計者的解決方案是引入CoyoteAdapter,這是適配器模式的經典運用,連接器調用CoyoteAdapter的Sevice方法,傳入的是Tomcat Request對象,CoyoteAdapter負責將Tomcat Request轉成ServletRequest,再調用容器的Service方法。

容器的層次結構

Tomcat設計了4種容器,分別是Engine、Host、Context和Wrapper。父子關係。Tomcat通過一種分層的架構,使得Servlet容器具有很好的靈活性。

Context表示一個Web應用程序;Wrapper表示一個Servlet,一個Web應用程序中可能會有多個Servlet;

Host代表的是一個虛擬主機,或者說一個站點,可以給Tomcat配置多個虛擬主機地址,而一個虛擬主機下可以部署多個Web應用程序;Engine表示引擎,用來管理多個虛擬站點,一個Service最多隻能有一個Engine。

Tomcat就是用組合模式來管理這些容器的。具體實現方法是,所有容器組件都實現了Container接口,因此組合模式可以使得用戶對單容器對象和組合容器對象的使用具有一致性。這裏單容器對象指的是最底層的Wrapper,組合容器對象指的是上面的Context、Host或者Engine。

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);
}

在該接口中有getParent、SetParent、addChild和removeChild等方法。Container接口擴展了LifeCycle接口,LifeCycle接口用來統一管理各組件的生命週期。

請求定位Servlet的過程

Tomcat是怎麼確定請求是由哪個Wrapper容器裏的Servlet來處理的呢?用Mapper組件來完成這個任務的。

Mapper組件的功能就是將用戶請求的URL定位到一個Servlet,它的工作原理是:Mapper組件裏保存了Web應用的配置信息,其實就是容器組件與訪問路徑的映射關係,比如Host容器裏配置的域名、Context容器裏的Web應用路徑,以及Wrapper容器裏Servlet映射的路徑,你可以想象這些配置信息就是一個多層次的Map。

 

Tomcat如何將這個URL定位到一個Servlet呢?

首先,根據協議和端口號選定Service和Engine。

Tomcat默認的HTTP連接器監聽8080端口、默認的AJP連接器監聽8009端口。上面例子中的URL訪問的是8080端口,因此這個請求會被HTTP連接器接收,而一個連接器是屬於一個Service組件的,這樣Service組件就確定了。我們還知道一個Service組件裏除了有多個連接器,還有一個容器組件,具體來說就是一個Engine容器,因此Service確定了也就意味着Engine也確定了。

然後,根據域名選定Host。

Service和Engine確定後,Mapper組件通過URL中的域名去查找相應的Host容器,比如例子中的URL訪問的域名是user.shopping.com,因此Mapper會找到Host2這個容器。

之後,根據URL路徑找到Context組件。

Host確定以後,Mapper根據URL的路徑來匹配相應的Web應用的路徑,比如例子中訪問的是/order,因此找到了Context4這個Context容器。

最後,根據URL路徑找到Wrapper(Servlet)。

Context確定後,Mapper再根據web.xml中配置的Servlet映射路徑來找到具體的Wrapper和Servlet。

責任鏈模式

這個查找路徑上的父子容器都會對請求做一些處理。連接器中的Adapter會調用容器的Service方法來執行Servlet,最先拿到請求的是Engine容器,Engine容器對請求做一些處理後,會把請求傳給自己子容器Host繼續處理,依次類推,最後這個請求會傳給Wrapper容器,Wrapper會調用最終的Servlet來處理。那麼這個調用過程具體是怎麼實現的呢?答案是使用Pipeline-Valve管道。

Pipeline-Valve是責任鏈模式,責任鏈模式是指在一個請求處理的過程中有很多處理者依次對請求進行處理,每個處理者負責做自己相應的處理,處理完之後將再調用下一個處理者繼續處理。Valve表示一個處理點,比如權限認證和記錄日誌。如果你還不太理解的話,可以來看看Valve和Pipeline接口中的關鍵方法。

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

由於Valve是一個處理點,因此invoke方法就是來處理請求的。注意到Valve中有getNext和setNext方法,因此我們大概可以猜到有一個鏈表將Valve鏈起來了。請你繼續看Pipeline接口:

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

Pipeline中有addValve方法。Pipeline中維護了Valve鏈表,Valve可以插入到Pipeline中,對請求做某

些處理。我們還發現Pipeline中沒有invoke方法,因爲整個調用鏈的觸發是Valve來完成的,Valve完成自己的處理後,調用getNext.invoke()來觸發下一個Valve調用。

每一個容器都有一個Pipeline對象,只要觸發這個Pipeline的第一個Valve,這個容器裏Pipeline中的Valve就都會被調用到。但是,不同容器的Pipeline是怎麼鏈式觸發的呢,比如Engine中Pipeline需要調用下層容器Host中的Pipeline。

這是因爲Pipeline中還有個getBasic方法。這個BasicValve處於Valve鏈表的末端,它是Pipeline中必不可少的一個Valve,負責調用下層容器的Pipeline裏的第一個Valve。

整個調用過程由連接器中的Adapter觸發的,它會調用Engine的第一個Valve:

// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Wrapper容器的最後一個Valve會創建一個Filter鏈,並調用doFilter()方法,最終會調到Servlet的service方法。

ServletContext、Context、ApplicationContext的關係

Servlet規範中ServletContext表示web應用的上下文環境,而web應用對應tomcat的概念是Context,

所以從設計上,ServletContext自然會成爲tomcat的Context具體實現的一個成員變量。

tomcat內部實現也是這樣完成的,ServletContext對應tomcat實現是org.apache.catalina.core.Applica

tionContext,Context容器對應tomcat實現是org.apache.catalina.core.StandardContext。ApplicationCo

ntext是StandardContext的一個成員變量。

Spring的ApplicationContext之前已經介紹過,tomcat啓動過程中ContextLoaderListener會監聽到容

器初始化事件,它的contextInitialized方法中,Spring會初始化全局的Spring根容器ApplicationContext,

初始化完畢後,Spring將其存儲到ServletContext中。

總而言之,Servlet規範中ServletContext是tomcat的Context實現的一個成員變量,而Spring的Applicatio

nContext是Servlet規範中ServletContext的一個屬性。

 

如果想讓一個系統能夠對外提供服務,我們需要創建、組裝並啓動這些組件;在服務停止的時候,我們還需要釋放資源,銷燬這些組件,因此這是一個動態的過程。也就是說,Tomcat需要動態地管理這些組件的生命週期。

如果你需要設計一個比較大的系統或者框架時,你同樣也需要考慮這幾個問題:如何統一管理組件的創建、初始化、啓動、停止和銷燬?如何做到代碼邏輯清晰?如何方便地添加或者刪除組件?如何做到組件啓動和停止不遺漏、不重複?

組件具有兩層關係:第一層關係是組件有大有小,大組件管理小組件,比如Server管理Service,Service又管理連接器和容器;第二層關係是組件有外有內,外層組件控制內層組件,比如連接器是外層組件,負責對外交流,外層組件調用內層組件完成業務功能。也就是說,請求的處理過程是由外層組件來驅動的。

這兩層關係決定了系統在創建組件時應該遵循一定的順序:第一個原則是先創建子組件,再創建父組件,子組件需要被“注入”到父組件中;第二個原則是先創建內層組件,再創建外層組件,內層組建需要被“注入”到外層組件。

最直觀的做法就是將圖上所有的組件按照先小後大、先內後外的順序創建出來,然後組裝在一起。但這樣不僅會造成代碼邏輯混亂和組件遺漏,而且也不利於後期的功能擴展。希望找到一種通用的、統一的方法來管理組件的生命週期,就像汽車“一鍵啓動”那樣的效果。

一鍵式啓停:LifeCycle接口

設計就是要找到系統的變化點和不變點。這裏的不變點就是每個組件都要經歷創建、初始化、啓動這幾個過程,這些狀態以及狀態的轉化是不變的。而變化點是每個具體組件的初始化方法,也就是啓動方法是不一樣的。把不變點抽象出來成爲一個接口,這個接口跟生命週期有關,叫作LifeCycle。LifeCycle接口裏應該定義這麼幾個方法:init()、start()、stop()和destroy(),每個具體的組件去實現這些方法。在父組件的init()方法裏需要創建子組件並調用子組件的init()方法。同樣,在父組件的start()方法裏也需要調用子組件的start()方法,因此調用者可以無差別的調用各組件的init()方法和start()方法,這就是組合模式的使用,並且只要調用最頂層組件,也就是Server組件的init()和start()方法,整個Tomcat就被啓動起來了。

可擴展性:LifeCycle事件

各個組件init()和start()方法的具體實現是複雜多變的,比如在Host容器的啓動方法裏需要掃描webapps目錄下的Web應用,創建相應的Context容器,如果將來需要增加新的邏輯,直接修改start()方法?這樣會違反開閉原則,那如何解決這個問題呢?開閉原則說的是爲了擴展系統的功能,你不能直接修改系統中已有的類,但是你可以定義新的類。

組件的init()和start()調用是由它的父組件的狀態變化觸發的,上層組件的初始化會觸發子組件的初始化,上層組件的啓動會觸發子組件的啓動,因此我們把組件的生命週期定義成一個個狀態,把狀態的

轉變看作是一個事件。而事件是有監聽器的,在監聽器裏可以實現一些邏輯,並且監聽器也可以方便的添加和刪除,這就是典型的觀察者模式。

具體來說就是在LifeCycle接口裏加入兩個方法:添加監聽器和刪除監聽器。除此之外,我們還需要定義一個Enum來表示組件有哪些狀態,以及處在什麼狀態會觸發什麼樣的事件。因此LifeCycle接口和LifeCycleState就定義成了下面這樣。

 

組件的生命週期有NEWINITIALIZINGINITIALIZEDSTARTING_PREPSTARTINGSTARTED等,而一旦組件到達相應的狀態就觸發相應的事件,比如NEW狀態表示組件剛剛被實例化;而當init()方法被調用時,狀態就變成INITIALIZING狀態,這個時候,就會觸發BEFORE_INIT_EVENT事件,如果有監聽器在監聽這個事件,它的方法就會被調用。

重用性:LifeCycleBase抽象基類

基類中往往會定義一些抽象方法,所謂的抽象方法就是說基類不會去實現這些方法,而是調用這些方法來實現骨架邏輯。抽象方法是留給各個子類去實現的,並且子類必須實現,否則無法實例化。

Tomcat定義一個基類LifeCycleBase來實現LifeCycle接口,把一些公共的邏輯放到基類

中去,比如生命狀態的轉變與維護、生命事件的觸發以及監聽器的添加和刪除等,而子類就負責實現自己的初始化、啓動和停止等方法。

LifecycleBase的init()方法實現:

@Override
public final synchronized void init() throws LifecycleException {
    // 1. 檢查狀態
    if (!state.equals(LifecycleState.NEW)) {
        invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
    }

    try {
        // 2. 觸發INITIALIZING事件的監聽器
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        // 3. 調用具體子類的初始化方法
        initInternal();
        // 4. 觸發INITIALIZED事件的監聽器
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        handleSubClassException(t, "lifecycleBase.initFail", toString());
    }
}

邏輯爲:

第一步,檢查狀態的合法性,比如當前狀態必須是NEW然後才能進行初始化。

第二步,觸發INITIALIZING事件的監聽器 在這個setStateInternal方法裏,會調用監聽器的業務方法。

第三步,調用具體子類實現的抽象方法initInternal()方法。爲了實現一鍵式啓動,具體組件在實現initInternal()方法時,又會調用它的子組件的init()方法。 

第四步,子組件初始化後,觸發INITIALIZED事件的監聽器,相應監聽器的業務方法就會被調用。

 

LifeCycleBase負責觸發事件,並調用監聽器的方法,那是什麼時候、誰把監聽器註冊進來的呢?

  1. Tomcat自定義了一些監聽器,這些監聽器是父組件在創建子組件的過程中註冊到子組件的。比如MemoryLeakTrackingListener監聽器,用來檢測Context容器中的內存泄漏,這個監聽器是Host容器在創建Context容器時註冊到Context中的。
  2. 還可以在server.xml中定義自己的監聽器,Tomcat在啓動時會解析server.xml,創建監聽器並註冊到容器組件。

 生命週期管理總體類圖

圖中出錯之處:Container接口應該也繼承Lifecycle接口

StandardEngineStandardHostStandardContextStandardWrapper是相應容器組件的具體實現類,因
爲它們都是容器,所以繼承了ContainerBase抽象基類,而ContainerBase實現了Container接口,也繼承了LifeCycleBase類,它們的生命週期管理接口和功能接口是分開的,這也符合設計中接口分離的原則

ContainerBase也是一個骨架抽象類,通用邏輯包括:容器的創建/初始化/銷燬;容器添加/刪除子容器;如果還要監聽容器狀態變化的話還需要有添加/移除事件的方法。

Tomcat的高層都負責什麼?

通過startup.sh啓動tomcat,之後發生什麼?

  1. Tomcat本質是個java程序,startup.sh腳本會啓動一個jvm運行tomcat的啓動類Bootstrap。
  2. Bootstrap的主要任務是初始化tomcat的類加載器,並且創建Catalina。
  3. Catalina是個啓動類,通過解析server.xml、創建相應的組件,並調用Server的start方法。
  4. Server組件的職責是管理Service組件,會負責調用Service的start方法
  5. Service組件的職責就是管理連接器和頂層容器Engine,調用連接器和Engine的start方法

這樣就完成啓動了,Bootstrap初始化類加載器,也就是創造萬物的工具。如果tomcat比作公司,Catalina就是創始人,負責組件團隊,創建Server和它的子組件。Server是CEO,負責多個事業羣,每個事業羣就是一個Service。Service是事業羣總經理,管理兩個職能部門,一個是對外的市場部,也就是連接器組件;另一個是對內的研發部,也就是容器組件。Engine是研發部經理,是最頂層的容器組件。

這些啓動類或組件不處理具體請求,任務主要是“管理”,管理下層組件的生命週期,並給下層組件分配任務,也就是把請求路由到負責幹活的組件,因此稱爲高層。

Catalina

Catalina的主要任務就是創建Server,需要解析server.xml,把在server.xml裏配置的各種組件一一創建出來,接着調用Server組件的init方法和start方法,這樣整個Tomcat就啓動起來了。作爲管理者Catalina還需要處理各種異常情況,比如當我們通過“Ctrl +C”關閉Tomcat時,Tomcat將如何優雅的停止並且清理資源呢?因此CatalinaJVM中註冊一個關閉鉤子

public void start() {
    // 1. 如果持有的server實例爲空,就解析server.xml創建出來
    if (getServer() == null) {
        load();
    }
    // 2. 如果創建失敗,報錯退出
    if (getServer() == null) {
        log.fatal("Cannot start server. Server instance is not configured.");
        return;
    }
    // 3. 啓動server
    try {
        getServer().start();
    } catch (LifecycleException e) {
        return;
    }
    // 創建並註冊關閉鉤子
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
    // 用await方法監聽停止請求
    if (await) {
        await();
        stop();
    }
}

什麼是關閉鉤子?如果我們需要在JVM關閉時做一些清理工作,比如將緩存數據刷到磁盤上,或者清理一些臨時文件,可以向JVM註冊一個關閉鉤子關閉鉤子其實就是一個線程,JVM在停止之前會嘗試執行這個線程的run方法。下面我們來看看Tomcat關閉鉤子”CatalinaShutdownHook做了些什麼。

protected class CatalinaShutdownHook extends Thread {
    @Override
    public void run() {
        try {
            if (getServer() != null) {
                Catalina.this.stop();
            }
        } catch (Throwable ex) {
          ...
        }
    }
}

Tomcat的關閉鉤子實際上就執行了Server的stop方法,Server的stop方法會釋放和清理所有的資源。

Server

Server組件的具體實現類是StandardServerServer繼承了LifeCycleBase,它的生命週期被統一管理,並且它的子組件是Service,因此它還需要管理Service的生命週期,也就是說在啓動時調用Service組件的啓動方法,在停止時調用它們的停止方法。Server在內部維護了若干Service組件,它是以數組來保存的,那Server是如何添加一個Service到數組中的呢?

@Override
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命令來關閉TomcatCatalina的啓動方法的最後一行代碼就是調用了Serverawait方法。
await方法裏會創建一個Socket監聽8005端口,並在一個死循環裏接收Socket上的連接請求,如果有新的連接到來就建立連接,然後從Socket中讀取數據;如果讀到的數據是停止命令“SHUTDOWN”,就退出循環,進入stop流程。

@Override
public void await() {
    ...
    // Set up a server socket to wait on, port=8005
    try {
        awaitSocket = new ServerSocket(port, 1,
                InetAddress.getByName(address));
    } catch (IOException e) {
        ...
    }
    try {
        awaitThread = Thread.currentThread();
        // Loop waiting for a connection and a valid command
        while (!stopAwait) {
            ServerSocket serverSocket = awaitSocket;
            if (serverSocket == null) {
                break;
            }
            // Wait for the next connection
            Socket socket = null;
            StringBuilder command = new StringBuilder();
            try {
                InputStream stream;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } catch (Exception e) {
                    ...
                }
                // Read a set of characters from the socket 
                int expected = 1024; // Cut off to avoid DoS attack
                while (expected < shutdown.length()) {
                    // ...
                    expected += (random.nextInt() % 1024);
                }
                while (expected > 0) {
                    // ...
                }
            } finally {
                // Close the socket now that we are done with it
            }
            // Match against our command string
            boolean match = command.toString().equals(shutdown);
            if (match) {
                log.info(sm.getString("standardServer.shutdownViaPort"));
                break;
            } else {
                ...
            }
        }
    } finally {
        // Close the server socket and return
    }
}

Service組件

具體實現類爲StandardService

public class StandardService extends LifecycleBase 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);

StandardService繼承了LifecycleBase抽象類,此外,StandardService還有熟悉的組件,Server、Connector、Engine、Mapper。

還有個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也依賴容器組件,容器組件啓動好了才能監聽它們的變化,因此MapperMapperListener在容器組件之後啓動。組件停止的順序跟啓動順序正好相反的,也是基於它們的依賴關係。

Engine組件

Engine本質是一個容器,因此它繼承了ContainerBase基類,並且實現了Engine接口。

public class StandardEngine extends ContainerBase implements Engine {
}

Engine的子容器是Host,所以它持有了一個Host容器的數組,這些功能都被抽象到了ContainerBase中,ContainerBase中有這樣一個數據結構:

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

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

for (int i = 0; i < children.length; i++) {
    results.add(startStopExecutor.submit(new StartChild(children[i])));
}

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

Engine自己做了什麼呢?容器組件最重要的功能是處理請求,而Engine容器對請求的處理,其實就是把請求轉發給某一個Host子容器來處理,具體是通過Valve來實現的。

每一個容器組件都有一個Pipeline,而Pipeline中有一個基礎閥(Basic Valve),而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定位了相應的容器,並且把容器對象保存到了請求對象中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章