茫茫網海中的冷日
         
茫茫網海中的冷日
發生過的事,不可能遺忘,只是想不起來而已!
 恭喜您是本站第 1670321 位訪客!  登入  | 註冊
主選單

Google 自訂搜尋

Goole 廣告

隨機相片
IMG_60D_00180.jpg

授權條款

使用者登入
使用者名稱:

密碼:


忘了密碼?

現在就註冊!

爪哇咖啡屋 : [轉貼]Tomcat 系統架構與設計模式,第 1 部分: 工作原理

發表者 討論內容
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[轉貼]Tomcat 系統架構與設計模式,第 1 部分: 工作原理

Tomcat 系統架構與設計模式,第 1 部分: 工作原理

這個分為兩個部分的系列文章將研究 Apache Tomcat 的系統架構以及其運用的很多經典設計模式。本文是第 1 部分,將主要從 Tomcat 如何分發請求、如何處理多用戶同時請求,還有它的多級容器是如何協調工作的角度來分析 Tomcat 的工作原理,這也是一個 Web 服務器首要解決的關鍵問題。

本文以 Tomcat 5 為基礎,也兼顧最新的 Tomcat 6 和 Tomcat 4。Tomcat 的基本設計思路和架構是具有一定連續性的。

Tomcat 總體結構

Tomcat 的結構很複雜,但是 Tomcat 也非常的模塊化,找到了 Tomcat 最核心的模塊,您就抓住了 Tomcat 的「七寸」。下面是 Tomcat 的總體結構圖:

圖 1.Tomcat 的總體結構

從上圖中可以看出 Tomcat 的心臟是兩個組件:Connector 和 Container,關於這兩個組件將在後面詳細介紹。Connector 組件是可以被替換,這樣可以提供給服務器設計者更多的選擇,因為這個組件是如此重要,不僅跟服務器的設計的本身,而且和不同的應用場景也十分相關,所以一個 Container 可以選擇對應多個 Connector。多個 Connector 和一個 Container 就形成了一個 Service,Service 的概念大家都很熟悉了,有了 Service 就可以對外提供服務了,但是 Service 還要一個生存的環境,必須要有人能夠給她生命、掌握其生死大權,那就非 Server 莫屬了。所以整個 Tomcat 的生命週期由 Server 控制。

以 Service 作為「婚姻」


我們將 Tomcat 中 Connector、Container 作為一個整體比作一對情侶的話,Connector 主要負責對外交流,可以比作為 Boy,Container 主要處理 Connector 接受的請求,主要是處理內部事務,可以比作為 Girl。那麼這個 Service 就是連接這對男女的結婚證了。是 Service 將它們連接在一起,共同組成一個家庭。當然要組成一個家庭還要很多其它的元素。

說白了,Service 只是在 Connector 和 Container 外面多包一層,把它們組裝在一起,向外面提供服務,一個 Service 可以設置多個 Connector,但是只能有一個 Container 容器。這個 Service 接口的方法列表如下:

圖 2. Service 接口

從 Service 接口中定義的方法中可以看出,它主要是為了關聯 Connector 和 Container,同時會初始化它下面的其它組件,注意接口中它並沒有規定一定要控制它下面的組件的生命週期。所有組件的生命週期在一個 Lifecycle 的接口中控制,這裡用到了一個重要的設計模式,關於這個接口將在後面介紹。

Tomcat 中 Service 接口的標準實現類是 StandardService 它不僅實現了 Service 借口同時還實現了 Lifecycle 接口,這樣它就可以控制它下面的組件的生命週期了。StandardService 類結構圖如下:

圖 3. StandardService 的類結構圖

從上圖中可以看出除了 Service 接口的方法的實現以及控制組件生命週期的 Lifecycle 接口的實現,還有幾個方法是用於在事件監聽的方法的實現,不僅是這個 Service 組件,Tomcat 中其它組件也同樣有這幾個方法,這也是一個典型的設計模式,將在後面介紹。

下面看一下 StandardService 中主要的幾個方法實現的代碼,下面是 setContainer 和 addConnector 方法的源碼:

清單 1. StandardService. SetContainer

public void setContainer(Container container) {
Container oldContainer = this.container;
if ((oldContainer != null) && (oldContainer instanceof Engine))
((Engine) oldContainer).setService(null);
this.container = container;
if ((this.container != null) && (this.container instanceof Engine))
((Engine) this.container).setService(this);
if (started && (this.container != null) && (this.container instanceof Lifecycle)) {
try {
((Lifecycle) this.container).start();
} catch (LifecycleException e) {
;
}
}
synchronized (connectors) {
for (int i = 0; i < connectors.length; i++)
connectors[i].setContainer(this.container);
}
if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) {
try {
((Lifecycle) oldContainer).stop();
} catch (LifecycleException e) {
;
}
}
support.firePropertyChange("container", oldContainer, this.container);
}

這段代碼很簡單,其實就是先判斷當前的這個 Service 有沒有已經關聯了 Container,如果已經關聯了,那麼去掉這個關聯關係—— oldContainer.setService(null)。如果這個 oldContainer 已經被啟動了,結束它的生命週期。然後再替換新的關聯、再初始化並開始這個新的 Container 的生命週期。最後將這個過程通知感興趣的事件監聽程序。這裡值得注意的地方就是,修改 Container 時要將新的 Container 關聯到每個 Connector,還好 Container 和 Connector 沒有雙向關聯,不然這個關聯關係將會很難維護。

清單 2. StandardService. addConnector

public void addConnector(Connector connector) {
synchronized (connectors) {
connector.setContainer(this.container);
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
if (initialized) {
try {
connector.initialize();
} catch (LifecycleException e) {
e.printStackTrace(System.err);
}
}
if (started && (connector instanceof Lifecycle)) {
try {
((Lifecycle) connector).start();
} catch (LifecycleException e) {
;
}
}
support.firePropertyChange("connector", null, connector);
}
}

上面是 addConnector 方法,這個方法也很簡單,首先是設置關聯關係,然後是初始化工作,開始新的生命週期。這裡值得一提的是,注意 Connector 用的是數組而不是 List 集合,這個從性能角度考慮可以理解,有趣的是這裡用了數組但是並沒有向我們平常那樣,一開始就分配一個固定大小的數組,它這裡的實現機制是:重新創建一個當前大小的數組對象,然後將原來的數組對像 copy 到新的數組中,這種方式實現了類似的動態數組的功能,這種實現方式,值得我們以後拿來借鑒。

最新的 Tomcat6 中 StandardService 也基本沒有變化,但是從 Tomcat5 開始 Service、Server 和容器類都繼承了 MBeanRegistration 接口,Mbeans 的管理更加合理。

以 Server 為「居」

前面說一對情侶因為 Service 而成為一對夫妻,有了能夠組成一個家庭的基本條件,但是它們還要有個實體的家,這是它們在社會上生存之本,有了家它們就可以安心的為人民服務了,一起為社會創造財富。

Server 要完成的任務很簡單,就是要能夠提供一個接口讓其它程序能夠訪問到這個 Service 集合、同時要維護它所包含的所有 Service 的生命週期,包括如何初始化、如何結束服務、如何找到別人要訪問的 Service。還有其它的一些次要的任務,如您住在這個地方要向當地政府去登記啊、可能還有要配合當地公安機關日常的安全檢查什麼的。

Server 的類結構圖如下:

圖 4. Server 的類結構圖

它的標準實現類 StandardServer 實現了上面這些方法,同時也實現了 Lifecycle、MbeanRegistration 兩個接口的所有方法,下面主要看一下 StandardServer 重要的一個方法 addService 的實現:

清單 3. StandardServer.addService

public void addService(Service service) {
service.setServer(this);
synchronized (services) {
Service results[] = new Service[services.length + 1];
System.arraycopy(services, 0, results, 0, services.length);
results[services.length] = service;
services = results;
if (initialized) {
try {
service.initialize();
} catch (LifecycleException e) {
e.printStackTrace(System.err);
}
}
if (started && (service instanceof Lifecycle)) {
try {
((Lifecycle) service).start();
} catch (LifecycleException e) {
;
}
}
support.firePropertyChange("service", null, service);
}
}

從上面第一句就知道了 Service 和 Server 是相互關聯的,Server 也是和 Service 管理 Connector 一樣管理它,也是將 Service 放在一個數組中,後面部分的代碼也是管理這個新加進來的 Service 的生命週期。Tomcat6 中也是沒有什麼變化的。

組件的生命線「Lifecycle」

前面一直在說 Service 和 Server 管理它下面組件的生命週期,那它們是如何管理的呢?

Tomcat 中組件的生命週期是通過 Lifecycle 接口來控制的,組件只要繼承這個接口並實現其中的方法就可以統一被擁有它的組件控制了,這樣一層一層的直到一個最高級的組件就可以控制 Tomcat 中所有組件的生命週期,這個最高的組件就是 Server,而控制 Server 的是 Startup,也就是您啟動和關閉 Tomcat。

下面是 Lifecycle 接口的類結構圖:

圖 5. Lifecycle 類結構圖

除了控制生命週期的 Start 和 Stop 方法外還有一個監聽機制,在生命週期開始和結束的時候做一些額外的操作。這個機制在其它的框架中也被使用,如在 Spring 中。關於這個設計模式會在後面介紹。

Lifecycle 接口的方法的實現都在其它組件中,就像前面中說的,組件的生命週期由包含它的父組件控制,所以它的 Start 方法自然就是調用它下面的組件的 Start 方法,Stop 方法也是一樣。如在 Server 中 Start 方法就會調用 Service 組件的 Start 方法,Server 的 Start 方法代碼如下:

清單 4. StandardServer.Start

public void start() throws LifecycleException {
if (started) {
log.debug(sm.getString("standardServer.start.started"));
return;
}
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
synchronized (services) {
for (int i = 0; i < services.length; i++) {
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).start();
}
}
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}

監聽的代碼會包圍 Service 組件的啟動過程,就是簡單的循環啟動所有 Service 組件的 Start 方法,但是所有 Service 必須要實現 Lifecycle 接口,這樣做會更加靈活。

Server 的 Stop 方法代碼如下:

清單 5. StandardServer.Stop

public void stop() throws LifecycleException {
if (!started)
return;
lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
started = false;
for (int i = 0; i < services.length; i++) {
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).stop();
}
lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);
}

它所要做的事情也和 Start 方法差不多。


回頁首

Connector 組件

Connector 組件是 Tomcat 中兩個核心組件之一,它的主要任務是負責接收瀏覽器的發過來的 tcp 連接請求,創建一個 Request 和 Response 對像分別用於和請求端交換數據,然後會產生一個線程來處理這個請求並把產生的 Request 和 Response 對像傳給處理這個請求的線程,處理這個請求的線程就是 Container 組件要做的事了。

由於這個過程比較複雜,大體的流程可以用下面的順序圖來解釋:

圖 6. Connector 處理一次請求順序圖

查看清晰大圖

Tomcat5 中默認的 Connector 是 Coyote,這個 Connector 是可以選擇替換的。Connector 最重要的功能就是接收連接請求然後分配線程讓 Container 來處理這個請求,所以這必然是多線程的,多線程的處理是 Connector 設計的核心。Tomcat5 將這個過程更加細化,它將 Connector 劃分成 Connector、Processor、Protocol, 另外 Coyote 也定義自己的 Request 和 Response 對象。

下面主要看一下 Tomcat 中如何處理多線程的連接請求,先看一下 Connector 的主要類圖:

圖 7. Connector 的主要類圖


查看清晰大圖

看一下 HttpConnector 的 Start 方法:

清單 6. HttpConnector.Start

public void start() throws LifecycleException {
if (started)
throw new LifecycleException
(sm.getString("httpConnector.alreadyStarted"));
threadName = "HttpConnector[" + port + "]";
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
threadStart();
while (curProcessors < minProcessors) {
if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
break;
HttpProcessor processor = newProcessor();
recycle(processor);
}
}

threadStart() 執行就會進入等待請求的狀態,直到一個新的請求到來才會激活它繼續執行,這個激活是在 HttpProcessor 的 assign 方法中,這個方法是代碼如下

清單 7. HttpProcessor.assign

synchronized void assign(Socket socket) {
while (available) {
try {
wait();
} catch (InterruptedException e) {
}
}
this.socket = socket;
available = true;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" An incoming request is being assigned");
}

創建 HttpProcessor 對象是會把 available 設為 false,所以當請求到來時不會進入 while 循環,將請求的 socket 賦給當期處理的 socket,並將 available 設為 true,當 available 設為 true 是 HttpProcessor 的 run 方法將被激活,接下去將會處理這次請求。

Run 方法代碼如下:

清單 8. HttpProcessor.Run

public void run() {
while (!stopped) {
Socket socket = await();
if (socket == null)
continue;
try {
process(socket);
} catch (Throwable t) {
log("process.invoke", t);
}
connector.recycle(this);
}
synchronized (threadSync) {
threadSync.notifyAll();
}
}

解析 socket 的過程在 process 方法中,process 方法的代碼片段如下:

清單 9. HttpProcessor.process

 private void process(Socket socket) {
boolean ok = true;
boolean finishResponse = true;
SocketInputStream input = null;
OutputStream output = null;
try {
input = new SocketInputStream(socket.getInputStream(),connector.getBufferSize());
} catch (Exception e) {
log("process.create", e);
ok = false;
}
keepAlive = true;
while (!stopped && ok && keepAlive) {
finishResponse = true;
try {
request.setStream(input);
request.setResponse(response);
output = socket.getOutputStream();
response.setStream(output);
response.setRequest(request);
((HttpServletResponse) response.getResponse())
.setHeader("Server", SERVER_INFO);
} catch (Exception e) {
log("process.create", e);
ok = false;
}
try {
if (ok) {
parseConnection(socket);
parseRequest(input, output);
if (!request.getRequest().getProtocol().startsWith("HTTP/0"))
parseHeaders(input);
if (http11) {
ackRequest(output);
if (connector.isChunkingAllowed())
response.setAllowChunking(true);
}
}
。。。。。。
try {
((HttpServletResponse) response).setHeader
("Date", FastHttpDateFormat.getCurrentDate());
if (ok) {
connector.getContainer().invoke(request, response);
}
。。。。。。
}
try {
shutdownInput(input);
socket.close();
} catch (IOException e) {
;
} catch (Throwable e) {
log("process.invoke", e);
}
socket = null;
}

當 Connector 將 socket 連接封裝成 request 和 response 對像後接下來的事情就交給 Container 來處理了。


回頁首

Servlet 容器「Container」

Container 是容器的父接口,所有子容器都必須實現這個接口,Container 容器的設計用的是典型的責任鏈的設計模式,它有四個子容器組件構成,分別是:Engine、Host、Context、Wrapper,這四個組件不是平行的,而是父子關係,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。通常一個 Servlet class 對應一個 Wrapper,如果有多個 Servlet 就可以定義多個 Wrapper,如果有多個 Wrapper 就要定義一個更高的 Container 了,如 Context,Context 通常就是對應下面這個配置:

清單 10. Server.xml

<Context
path="/library"
docBase="D:\projects\library\deploy\target\library.war"
reloadable="true"
/>

容器的總體設計

Context 還可以定義在父容器 Host 中,Host 不是必須的,但是要運行 war 程序,就必須要 Host,因為 war 中必有 web.xml 文件,這個文件的解析就需要 Host 了,如果要有多個 Host 就要定義一個 top 容器 Engine 了。而 Engine 沒有父容器了,一個 Engine 代表一個完整的 Servlet 引擎。

那麼這些容器是如何協同工作的呢?先看一下它們之間的關係圖:

圖 8. 四個容器的關係圖

查看清晰大圖

當 Connector 接受到一個連接請求時,將請求交給 Container,Container 是如何處理這個請求的?這四個組件是怎麼分工的,怎麼把請求傳給特定的子容器的呢?又是如何將最終的請求交給 Servlet 處理。下面是這個過程的時序圖:

圖 9. Engine 和 Host 處理請求的時序圖

查看清晰大圖


這裡看到了 Valve 是不是很熟悉,沒錯 Valve 的設計在其他框架中也有用的,同樣 Pipeline 的原理也基本是相似的,它是一個管道,Engine 和 Host 都會執行這個 Pipeline,您可以在這個管道上增加任意的 Valve,Tomcat 會挨個執行這些 Valve,而且四個組件都會有自己的一套 Valve 集合。您怎麼才能定義自己的 Valve 呢?在 server.xml 文件中可以添加,如給 Engine 和 Host 增加一個 Valve 如下:

清單 11. Server.xml

    <Valve className="org.apache.catalina.valves.RequestDumperValve"/>
………
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true"
xmlNamespaceAware="false" xmlValidation="false">
<Valve className="org.apache.catalina.valves.FastCommonAccessLogValve"
directory="logs" prefix="localhost_access_log." suffix=".txt"
pattern="common" resolveHosts="false"/>
…………
</Host>
</Engine>

StandardEngineValve 和 StandardHostValve 是 Engine 和 Host 的默認的 Valve,它們是最後一個 Valve 負責將請求傳給它們的子容器,以繼續往下執行。

前面是 Engine 和 Host 容器的請求過程,下面看 Context 和 Wrapper 容器時如何處理請求的。下面是處理請求的時序圖:

圖 10. Context 和 wrapper 的處理請求時序圖

查看清晰大圖

從 Tomcat5 開始,子容器的路由放在了 request 中,request 中保存了當前請求正在處理的 Host、Context 和 wrapper。

Engine 容器

Engine 容器比較簡單,它只定義了一些基本的關聯關係,接口類圖如下:

圖 11. Engine 接口的類結構

它的標準實現類是 StandardEngine,這個類注意一點就是 Engine 沒有父容器了,如果調用 setParent 方法時將會報錯。添加子容器也只能是 Host 類型的,代碼如下:

清單 12. StandardEngine. addChild

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

它的初始化方法也就是初始化和它相關聯的組件,以及一些事件的監聽。

Host 容器

Host 是 Engine 的字容器,一個 Host 在 Engine 中代表一個虛擬主機,這個虛擬主機的作用就是運行多個應用,它負責安裝和展開這些應用,並且標識這個應用以便能夠區分它們。它的子容器通常是 Context,它除了關聯子容器外,還有就是保存一個主機應該有的信息。

下面是和 Host 相關的類關聯圖:

圖 12. Host 相關的類圖

查看清晰大圖

從上圖中可以看出除了所有容器都繼承的 ContainerBase 外,StandardHost 還實現了 Deployer 接口,上圖清楚的列出了這個接口的主要方法,這些方法都是安裝、展開、啟動和結束每個 web application。

Deployer 接口的實現是 StandardHostDeployer,這個類實現了的最要的幾個方法,Host 可以調用這些方法完成應用的部署等。

Context 容器

Context 代表 Servlet 的 Context,它具備了 Servlet 運行的基本環境,理論上只要有 Context 就能運行 Servlet 了。簡單的 Tomcat 可以沒有 Engine 和 Host。

Context 最重要的功能就是管理它裡面的 Servlet 實例,Servlet 實例在 Context 中是以 Wrapper 出現的,還有一點就是 Context 如何才能找到正確的 Servlet 來執行它呢? Tomcat5 以前是通過一個 Mapper 類來管理的,Tomcat5 以後這個功能被移到了 request 中,在前面的時序圖中就可以發現獲取子容器都是通過 request 來分配的。


Context 準備 Servlet 的運行環境是在 Start 方法開始的,這個方法的代碼片段如下:

清單 13. StandardContext.start

public synchronized void start() throws LifecycleException {
………
if( !initialized ) {
try {
init();
} catch( Exception ex ) {
throw new LifecycleException("Error initializaing ", ex);
}
}
………
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
setAvailable(false);
setConfigured(false);
boolean ok = true;
File configBase = getConfigBase();
if (configBase != null) {
if (getConfigFile() == null) {
File file = new File(configBase, getDefaultConfigFile());
setConfigFile(file.getPath());
try {
File appBaseFile = new File(getAppBase());
if (!appBaseFile.isAbsolute()) {
appBaseFile = new File(engineBase(), getAppBase());
}
String appBase = appBaseFile.getCanonicalPath();
String basePath =
(new File(getBasePath())).getCanonicalPath();
if (!basePath.startsWith(appBase)) {
Server server = ServerFactory.getServer();
((StandardServer) server).storeContext(this);
}
} catch (Exception e) {
log.warn("Error storing config file", e);
}
} else {
try {
String canConfigFile = (new File(getConfigFile())).getCanonicalPath();
if (!canConfigFile.startsWith (configBase.getCanonicalPath())) {
File file = new File(configBase, getDefaultConfigFile());
if (copy(new File(canConfigFile), file)) {
setConfigFile(file.getPath());
}
}
} catch (Exception e) {
log.warn("Error setting config file", e);
}
}
}
………
Container children[] = findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] instanceof Lifecycle)
((Lifecycle) children[i]).start();
}
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
………
}

它主要是設置各種資源屬性和管理組件,還有非常重要的就是啟動子容器和 Pipeline。

我們知道 Context 的配置文件中有個 reloadable 屬性,如下面配置:

清單 14. Server.xml

<Context
path="/library"
docBase="D:\projects\library\deploy\target\library.war"
reloadable="true"
/>

當這個 reloadable 設為 true 時,war 被修改後 Tomcat 會自動的重新加載這個應用。如何做到這點的呢 ? 這個功能是在 StandardContext 的 backgroundProcess 方法中實現的,這個方法的代碼如下:

清單 15. StandardContext. backgroundProcess

public void backgroundProcess() {
if (!started) return;
count = (count + 1) % managerChecksFrequency;
if ((getManager() != null) && (count == 0)) {
try {
getManager().backgroundProcess();
} catch ( Exception x ) {
log.warn("Unable to perform background process on manager",x);
}
}
if (getLoader() != null) {
if (reloadable && (getLoader().modified())) {
try {
Thread.currentThread().setContextClassLoader
(StandardContext.class.getClassLoader());
reload();
} finally {
if (getLoader() != null) {
Thread.currentThread().setContextClassLoader
(getLoader().getClassLoader());
}
}
}
if (getLoader() instanceof WebappLoader) {
((WebappLoader) getLoader()).closeJARs(false);
}
}
}

它會調用 reload 方法,而 reload 方法會先調用 stop 方法然後再調用 Start 方法,完成 Context 的一次重新加載。可以看出執行 reload 方法的條件是 reloadable 為 true 和應用被修改,那麼這個 backgroundProcess 方法是怎麼被調用的呢?

這個方法是在 ContainerBase 類中定義的內部類 ContainerBackgroundProcessor 被週期調用的,這個類是運行在一個後台線程中,它會週期的執行 run 方法,它的 run 方法會週期調用所有容器的 backgroundProcess 方法,因為所有容器都會繼承 ContainerBase 類,所以所有容器都能夠在 backgroundProcess 方法中定義週期執行的事件。

Wrapper 容器

Wrapper 代表一個 Servlet,它負責管理一個 Servlet,包括的 Servlet 的裝載、初始化、執行以及資源回收。Wrapper 是最底層的容器,它沒有子容器了,所以調用它的 addChild 將會報錯。

Wrapper 的實現類是 StandardWrapper,StandardWrapper 還實現了擁有一個 Servlet 初始化信息的 ServletConfig,由此看出 StandardWrapper 將直接和 Servlet 的各種信息打交道。

下面看一下非常重要的一個方法 loadServlet,代碼片段如下:

清單 16. StandardWrapper.loadServlet

public synchronized Servlet loadServlet() throws ServletException {
………
Servlet servlet;
try {
………
ClassLoader classLoader = loader.getClassLoader();
………
Class classClass = null;
………
servlet = (Servlet) classClass.newInstance();
if ((servlet instanceof ContainerServlet) &&
(isContainerProvidedServlet(actualClass) ||
((Context)getParent()).getPrivileged() )) {
((ContainerServlet) servlet).setWrapper(this);
}
classLoadTime=(int) (System.currentTimeMillis() -t1);
try {
instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet);
if( System.getSecurityManager() != null) {
Class[] classType = new Class[]{ServletConfig.class};
Object[] args = new Object[]{((ServletConfig)facade)};
SecurityUtil.doAsPrivilege("init",servlet,classType,args);
} else {
servlet.init(facade);
}
if ((loadOnStartup >= 0) && (jspFile != null)) {
………
if( System.getSecurityManager() != null) {
Class[] classType = new Class[]{ServletRequest.class,
ServletResponse.class};
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",servlet,classType,args);
} else {
servlet.service(req, res);
}
}
instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet);
………
return servlet;
}

它基本上描述了對 Servlet 的操作,當裝載了 Servlet 後就會調用 Servlet 的 init 方法,同時會傳一個 StandardWrapperFacade 對像給 Servlet,這個對象包裝了 StandardWrapper,ServletConfig 與它們的關係圖如下:

圖 13. ServletConfig 與 StandardWrapperFacade、StandardWrapper 的關係

Servlet 可以獲得的信息都在 StandardWrapperFacade 封裝,這些信息又是在 StandardWrapper 對像中拿到的。所以 Servlet 可以通過 ServletConfig 拿到有限的容器的信息。

當 Servlet 被初始化完成後,就等著 StandardWrapperValve 去調用它的 service 方法了,調用 service 方法之前要調用 Servlet 所有的 filter。


回頁首

Tomcat 中其它組件

Tomcat 還有其它重要的組件,如安全組件 security、logger 日誌組件、session、mbeans、naming 等其它組件。這些組件共同為 Connector 和 Container 提供必要的服務。

參考資料

學習

  • 查看本系列的第 2 部分:「 Tomcat 設計模式分析」。

  • 面向初級 Web 開發人員的 Tomcat」(developerWorks,2005 年 10 月):Apache Tomcat 應用服務器不再是高級 Web 系統開發人員的專用領域。在本教程中,Sing Li 將向初級 Web 開發人員展示如何利用他們當前的 Java 開發技能,使用 Tomcat 編寫服務器端 JSP、servlet 和 Web 服務。
  • 技術書店:瀏覽關於這些和其他技術主題的圖書。
  • developerWorks Java 技術專區:數百篇關於 Java 編程各個方面的文章。

討論


原文出處:Tomcat 系统架构与设计模式,第 1 部分: 工作原理
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[轉貼]Tomcat 系統架構與設計模式,第 2 部分: 設計模式分析

Tomcat 系統架構與設計模式,第 2 部分: 設計模式分析

這個分為兩個部分的系列文章研究了 Apache Tomcat 服務器的系統架構以及其運用的很多經典設計模式。 第 1 部分 分析了 Tomcat 的工作原理,第 2 部分將分析 Tomcat 中運用的許多經典設計模式,如模版模式、工廠模式和單例模式等。通過學習它們的實踐運用能給我們以後的軟件設計起到一定的借鑒作用。

門面設計模式

門面設計模式在 Tomcat 中有多處使用,在 Request 和 Response 對像封裝中、Standard Wrapper 到 ServletConfig 封裝中、ApplicationContext 到 ServletContext 封裝中等都用到了這種設計模式。

門面設計模式的原理

這麼多場合都用到了這種設計模式,那這種設計模式究竟能有什麼作用呢?顧名思義,就是將一個東西封裝成一個門面好與人家更容易進行交流,就像一個國家的外交部一樣。

這種設計模式主要用在一個大的系統中有多個子系統組成時,這多個子系統肯定要涉及到相互通信,但是每個子系統又不能將自己的內部數據過多的暴露給其它系統,不然就沒有必要劃分子系統了。每個子系統都會設計一個門面,把別的系統感興趣的數據封裝起來,通過這個門面來進行訪問。這就是門面設計模式存在的意義。

門面設計模式示意圖如下:

圖 1. 門面示意圖

Client 只能訪問到 Facade 中提供的數據是門面設計模式的關鍵,至於 Client 如何訪問 Facade 和 Subsystem 如何提供 Facade 門面設計模式並沒有規定死。

Tomcat 的門面設計模式示例


Tomcat 中門面設計模式使用的很多,因為 Tomcat 中有很多不同組件,每個組件要相互交互數據,用門面模式隔離數據是個很好的方法。

下面是 Request 上使用的門面設計模式:

圖 2. Request 的門面設計模式類圖

從圖中可以看出 HttpRequestFacade 類封裝了 HttpRequest 接口能夠提供數據,通過 HttpRequestFacade 訪問到的數據都被代理到 HttpRequest 中,通常被封裝的對象都被設為 Private 或者 Protected 訪問修飾,以防止在 Facade 中被直接訪問。


回頁首

觀察者設計模式

這種設計模式也是常用的設計方法通常也叫發佈 - 訂閱模式,也就是事件監聽機制,通常在某個事件發生的前後會觸發一些操作。

觀察者模式的原理

觀察者模式原理也很簡單,就是你在做事的時候旁邊總有一個人在盯著你,當你做的事情是它感興趣的時候,它就會跟著做另外一些事情。但是盯著你的人必須要到你那去登記,不然你無法通知它。觀察者模式通常包含下面這幾個角色:

  • Subject 就是抽像主題:它負責管理所有觀察者的引用,同時定義主要的事件操作。
  • ConcreteSubject 具體主題:它實現了抽像主題的所有定義的接口,當自己發生變化時,會通知所有觀察者。
  • Observer 觀察者:監聽主題發生變化相應的操作接口。

Tomcat 的觀察者模式示例

Tomcat 中觀察者模式也有多處使用,前面講的控制組件生命週期的 Lifecycle 就是這種模式的體現,還有對 Servlet 實例的創建、Session 的管理、Container 等都是同樣的原理。下面主要看一下 Lifecycle 的具體實現。


Lifecycle 的觀察者模式結構圖:

圖 3. Lifecycle 的觀察者模式結構圖

上面的結構圖中,LifecycleListener 代表的是抽像觀察者,它定義一個 lifecycleEvent 方法,這個方法就是當主題變化時要執行的方法。 ServerLifecycleListener 代表的是具體的觀察者,它實現了 LifecycleListener 接口的方法,就是這個具體的觀察者具體的實現方式。Lifecycle 接口代表的是抽像主題,它定義了管理觀察者的方法和它要所做的其它方法。而 StandardServer 代表的是具體主題,它實現了抽像主題的所有方法。這裡 Tomcat 對觀察者做了擴展,增加了另外兩個類:LifecycleSupport、LifecycleEvent,它們作為輔助類擴展了觀察者的功能。LifecycleEvent 使得可以定義事件類別,不同的事件可區別處理,更加靈活。LifecycleSupport 類代理了主題對多觀察者的管理,將這個管理抽出來統一實現,以後如果修改只要修改 LifecycleSupport 類就可以了,不需要去修改所有具體主題,因為所有具體主題的對觀察者的操作都被代理給 LifecycleSupport 類了。這可以認為是觀察者模式的改進版。

LifecycleSupport 調用觀察者的方法代碼如下:

清單 1. LifecycleSupport 中的 fireLifecycleEvent 方法

public void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interested[] = null;
synchronized (listeners) {
interested = (LifecycleListener[]) listeners.clone();
}
for (int i = 0; i < interested.length; i++)
interested[i].lifecycleEvent(event);
}

主題是怎麼通知觀察者呢?看下面代碼:

清單 2. 容器中的 start 方法

public void start() throws LifecycleException {
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
synchronized (services) {
for (int i = 0; i < services.length; i++) {
if (services[i] instanceof Lifecycle)
((Lifecycle) services[i]).start();
}
}
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}


回頁首

命令設計模式

前面把 Tomcat 中兩個核心組件 Connector 和 Container,比作一對夫妻。男的將接受過來的請求以命令的方式交給女主人。對應到 Connector 和 Container,Connector 也是通過命令模式調用 Container 的。

命令模式的原理

命令模式主要作用就是封裝命令,把發出命令的責任和執行命令的責任分開。也是一種功能的分工。不同的模塊可以對同一個命令做出不同解釋。

下面是命令模式通常包含下面幾個角色:

  • Client:創建一個命令,並決定接受者
  • Command 命令:命令接口定義一個抽像方法
  • ConcreteCommand:具體命令,負責調用接受者的相應操作
  • Invoker 請求者:負責調用命令對像執行請求
  • Receiver 接受者:負責具體實施和執行一次請求

Tomcat 中的命令模式的示例

Tomcat 中命令模式在 Connector 和 Container 組件之間有體現,Tomcat 作為一個應用服務器,無疑會接受到很多請求,如何分配和執行這些請求是必須的功能。

下面看一下 Tomcat 是如何實現命令模式的,下面是 Tomcat 命令模式的結構圖:

圖 4. Tomcat 命令模式的結構圖

Connector 作為抽像請求者,HttpConnector 作為具體請求者。HttpProcessor 作為命令。Container 作為命令的抽像接受者,ContainerBase 作為具體的接受者。客戶端就是應用服務器 Server 組件了。Server 首先創建命令請求者 HttpConnector 對象,然後創建命令 HttpProcessor 命令對象。再把命令對像交給命令接受者 ContainerBase 容器來處理命令。命令的最終是被 Tomcat 的 Container 執行的。命令可以以隊列的方式進來,Container 也可以以不同的方式來處理請求,如 HTTP1.0 協議和 HTTP1.1 的處理方式就會不同。


回頁首

責任鏈模式

Tomcat 中一個最容易發現的設計模式就是責任鏈模式,這個設計模式也是 Tomcat 中 Container 設計的基礎,整個容器的就是通過一個鏈連接在一起,這個鏈一直將請求正確的傳遞給最終處理請求的那個 Servlet。

責任鏈模式的原理

責任鏈模式,就是很多對像有每個對象對其下家的引用而連接起來形成一條鏈,請求在這條鏈上傳遞,直到鏈上的某個對象處理此請求,或者每個對象都可以處理請求,並傳給下一家,直到最終鏈上每個對象都處理完。這樣可以不影響客戶端而能夠在鏈上增加任意的處理節點。

通常責任鏈模式包含下面幾個角色:

  • Handler(抽像處理者):定義一個處理請求的接口
  • ConcreteHandler(具體處理者):處理請求的具體類,或者傳給下家

Tomcat 中責任鏈模式示例

在 tomcat 中這種設計模式幾乎被完整的使用,tomcat 的容器設置就是責任鏈模式,從 Engine 到 Host 再到 Context 一直到 Wrapper 都是通過一個鏈傳遞請求。

Tomcat 中責任鏈模式的類結構圖如下:

圖 5. Tomcat 責任鏈模式的結構圖

上圖基本描述了四個子容器使用責任鏈模式的類結構圖,對應的責任鏈模式的角色,Container 扮演抽像處理者角色,具體處理者由 StandardEngine 等子容器扮演。與標準的責任鏈不同的是,這裡引入了 Pipeline 和 Valve 接口。他們有什麼作用呢?

實際上 Pipeline 和 Valve 是擴展了這個鏈的功能,使得在鏈往下傳遞過程中,能夠接受外界的干預。Pipeline 就是連接每個子容器的管子,裡面傳遞的 Request 和 Response 對像好比管子裡流的水,而 Valve 就是這個管子上開的一個個小口子,讓你有機會能夠接觸到裡面的水,做一些額外的事情。

為了防止水被引出來而不能流到下一個容器中,每一段管子最後總有一個節點保證它一定能流到下一個子容器,所以每個容器都有一個 StandardXXXValve。只要涉及到這種有鏈式是處理流程這是一個非常值得借鑒的模式。

參考資料

學習

  • 查看本系列的第 1 部分:「 Tomcat 系統原理分析」。
  • 面向初級 Web 開發人員的 Tomcat」(developerWorks,2005 年 10 月):Apache Tomcat 應用服務器不再是高級 Web 系統開發人員的專用領域。在本教程中,Sing Li 將向初級 Web 開發人員展示如何利用他們當前的 Java 開發技能,使用 Tomcat 編寫服務器端 JSP、servlet 和 Web 服務。

  • 技術書店:瀏覽關於這些和其他技術主題的圖書。
  • developerWorks Java 技術專區:數百篇關於 Java 編程各個方面的文章。

討論


原文出處: Tomcat 系统架构与设计模式,第 2 部分: 设计模式分析
前一個主題 | 下一個主題 | 頁首 | | |



Powered by XOOPS 2.0 © 2001-2008 The XOOPS Project|