編輯注: 在這個系列中, 本文和上一篇, "How Web Servers Work," 是Tomcat 內部工作原理的指南書籍 How Tomcat Works 的節選. 如果你還沒有讀過上一篇, 那麼趕緊先去讀讀吧; 那篇文章告訴了你一些有用的背景信息. 在本文裏, 你會學習到怎樣去創建2個 servlet 容器. 本書附帶的程序可以下載. 如果你有興趣的話, 在限定的時間內, 作者的網站上允許下載本書的其它部分.
本文講解了一個簡單的 servlet 容器是怎樣工作的. 將會給您展示2個 servlet 容器應用程序; 第一個儘可能簡單, 第二個則在第一個基礎上做了美化. 我不想把第一個容器做的完美的唯一原因是讓它儘可能保持簡單. 更多複雜的 servlet 容器, 包括 Tomcat 4 和 5, 則在 How Tomcat Works 的其它章節討論.
servlet container 既能處理簡單的 servlet, 也能處理靜態資源. 你可以使用 PrimitiveServlet
(位於
webroot/ 目錄下)測試這個容器. 更復雜的servlet已經超出了這個容器的能力, 但你可以從 How Tomcat Works 這本書中學習到怎樣建立更完善的 servlet container.
這些程序的類都是 ex02.pyrmont
包中的一部分. 要想了解程序是如何工作的, 你必須要熟悉 javax.servlet.Servlet
接口. 爲了重新溫習一下這方面的知識, 本文第一部分將討論這個接口. 然後, 你會學習到究竟一個 servlet container 需要做些什麼工作才能處理 servlet.
The javax.servlet.Servlet
Interface
Servlet 編程需要用到2個包中的類和接口: javax.servlet
和 javax.servlet.http
. 在這些類和接口中, javax.servlet.Servlet
接口是最重要的接口. 所有servlet 都必須實現這個接口或者繼承一個實現了這個接口的類.
Servlet
接口有5個方法, 定義如下:
init
, service
, 和 destroy
方法是 servlet 的生命週期方法. init
方法只在servlet類被初始化後被容器調用一次,指出這個 servlet 已經處於可提供服務的狀態. 在servlet能夠接收任何請求之前,init
方法的調用必須完全成功. Servlet 程序員可以覆蓋這個方法,實現一些需要只運行一次的初始化代碼, 像裝載 database driver, 初始化值, 等. 其它情況下, 這個方法通常是空白的.
service
方法會被容器調用讓這個servlet響應一個請求. servlet container 傳遞一個 javax.servlet.ServletRequest
對象和一個 javax.servlet.ServletResponse
對象. ServletRequest
對象包含客戶端 HTTP 請求信息,ServletResponse
封裝了 servlet 的響應. 這2個對象讓你能夠寫一些自己的代碼決定這個servlet怎樣爲這個客戶端請求提供服務.
servlet container 在移除一個servlet實例之前會調用 destroy
方法. 這通常發生在當 servlet container 要關閉或者當 servlet container 需要更多 free memory 的時候. 這個方法只會在這個servlet的service
方法中的所有線程都已經退出來或者超時時間已過之後
. 在 servlet container 調用了 destroy
之後
, 它就不會再調用這個servlet上的 service
方法. destroy
方法給了servlet一個機會去清理該servlet正在把持的任何資源 (例如, 內存, 文件句柄, 和線程) 和確認任何持久性狀態信息都已經與內存中這個servlet的當前狀態進行了同步.
Listing 2.1 包含
了
PrimitiveServlet
的代碼
, 這是一個非常簡單的 servlet,你可以用來測試本文這個 servlet container 程序. PrimitiveServlet
類實現了 javax.servlet.Servlet
(所有servlet 都必須實現) 並且爲所有5個servlet 方法都提供了實現. 它做的事情非常簡單: 每次 init
, service
, 或者 destroy
方法中的任意一個被調用, servlet 就向控制檯打印方法名. service
方法中的代碼也會從ServletResponse
對象
獲得一個 java.io.PrintWriter
對象向瀏覽器發送字符串.
Listing 2.1. PrimitiveServlet.java
Application 1
現在, 讓我們從servlet container 的視角來看 servlet 編程. 簡而言之, 一個功能完備的 servlet container 需爲每個servlet的HTTP 請求完成如下工作:
-
當 servlet 當 servlet 第一次被調用的時候, 加載這個 servlet 類並調用它的
init
方法 (只調用一次). -
對於每個請求, 構造一個
javax.servlet.ServletRequest
實例和javax.servlet.ServletResponse
實例
. -
調用 servlet 的
service
方法, 傳給它ServletRequest
和ServletResponse
對象. -
當 servlet 類被關掉的時候, 調用 servlet 的
destroy
方法並卸載 servlet 類.
在一個真正的 servlet container 裏需要做的比這些複雜的多. 不過, 這個簡單的 servlet container 功能並不完備. 因此, 它只能運行非常簡單的 servlet,而且也不調用 servlet 的 init
和 destroy
方法. 取而代之, 它做了如下工作:
-
等待 HTTP 請求.
-
構造一個
ServletRequest
對象和一個ServletResponse
對象. -
如果請求的是靜態資源, 調用
StaticResourceProcessor
實例的
process
方法, 並傳給它ServletRequest
和ServletResponse
對象. -
如果請求的是一個 servlet, 加載這個 servlet 類,調用它的
service
方法, 並傳遞ServletRequest
和ServletResponse
對象給它. 注意,在這個 servlet container 中, 請求的servlet 類會每次都加載.
在第一個程序裏, servlet container 由6個類組成:
-
HttpServer1
-
Request
-
Response
-
StaticResourceProcessor
-
ServletProcessor1
-
Constants
就像上一篇文章中的程序那樣, 這個程序的入口點 (the static main
method) 在 HttpServer
類中. 這個方法創建了 HttpServer
的一個實例並調用它的 await
方法. 就像名字所隱含的, 這個方法會等待 HTTP 請求, 創建一個 Request
對象和一個 Response
對象, 然後分發給一個 StaticResourceProcessor
實例或者一個 ServletProcessor
實例, 這依賴於請求的是靜態資源還是一個servlet.
Constants
類包含 static final WEB_ROOT
這個被其它類引用的變量. WEB_ROOT
指示了 PrimitiveServlet
的位置和這個容器所能提供的靜態資源.
HttpServer1
實例一直等待 HTTP 請求知道它接收到關閉命令. 發出關閉命令與上一篇文章裏所做的一樣.
這個程序裏的每個類都會在下面的章節裏討論到.
The HttpServer1
Class
這個程序的 HttpServer1
類與上一篇文章中的那個簡單的 web server 程序的 HttpServer
類很相似
. 不過, 在這個程序裏, HttpServer1
能夠同時支持靜態資源和 servlet. 如果要請求靜態資源, 使用下面形式的URL:
這正是上一篇文章中,你如何請求web server程序的一個靜態資源. 要想請求一個 servlet, 你需使用下面的 URL:
因此, 如果你想使用瀏覽器請求一個叫PrimitiveServlet
的
servlet
, 在瀏覽器地址欄中輸入如下 URL:
Listing 2.2 中給出的類的 await
方法, 等待 HTTP 請求,直到發出關閉命令爲止. 這跟上一篇文章討論到的 await
方法很類似.
Listing 2.2. The HttpServer1
class' await
method
Listing 2.2 中的await
方法與前一篇文章的
await
Listing 2.2中, request 可以分發至 方法的區別是,
StaticResourceProcessor
或者 ServletProcessor
. 如果URI 包含字符串"/servlet/
",request 會被 forward 到後者. 否則, request 會被傳給 StaticResourceProcessor
實例.
The Request
Class
Servlet 的 service
方法從 servlet container 接收一個 javax.servlet.ServletRequest
實例和一個 javax.servlet.ServletResponse
實例. container 因而必須要構造一個 ServletRequest
對象和一個 ServletResponse
對象並傳給這個servlet的 service
方法.
ex02.pyrmont.Request
類代表要傳遞給 service
方法的 request 對象. 同樣的, 它必須實現 javax.servlet.ServletRequest
接口. 這個類必須提供接口中所聲明的所有方法的實現. 但是, 我們想使它簡單一些,所以只實現了部分方法. 要想成功編譯這個 Request
類, 你需要給其它一些方法空的實現. 如果你看了 Request
類, 你會看到所有方法聲明中返回對象實例的方法都返回的是一個 null
, 如下:
另外, Request
類也有 parse
和 getUri
方法, 這些上篇文章討論過.
The Response
Class
Response
類實現了 javax.servlet.ServletResponse
. 同樣地, 該類必須提供接口中的所有方法的實現. 類似於 Request
類, 我把所有方法都實現爲空除了 getWriter
方法.
傳給 PrintWriter
類構造器的第二個參數是一個 Boolean,指示是否要啓用 autoflush
. 傳遞 true 作爲第二個參數將使每次對 println
方法的調用都flush輸出. 但是, print
調用不會 flush output. 因此, 如果在servlet 的 service
方法中的最後一行代碼調用 print
方法, 那麼輸出不會發送至瀏覽器. 這個瑕疵將在後面的程序中得到修復.
Response
類也有一個我們在上一篇文章裏討論到的 sendStaticResource
方法.
The StaticResourceProcessor
Class
StaticResourceProcessor
類用來處理請求靜態資源的請求. 它唯一的方法是 process
, 如 Listing 2.3 所示.
Listing 2.3. The StaticResourceProcessor
class' process
method
process
方法接收2個參數: 一個 Request
實例和一個 Response
實例. 它只簡單的調用了一下 Response
類的 sendStaticResource
方法
.
The ServletProcessor1
Class
ServletProcessor1
類處理 HTTP 請求. 它令人驚訝的簡單, 只包含 process
方法. 這個方法接收2個參數: 一個 javax.servlet.ServletRequest
實例和一個 javax.servlet.ServletResponse
實例
. process
方法也構造了一個 java.net.URLClassLoader
對象並用它來加載 servlet 類文件. 從 class loader 得到了 Class
對象後, process
方法創建了這個 servlet 的一個實例並調用 service
方法.
Listing 2.4 給出了 process
方法.
Listing 2.4. The ServletProcessor1
class' process
method
process
方法接收2個參數: 一個 ServletRequest
實例和一個 ServletResponse
實例
. 它從 ServletRequest
的
getRequestUri
方法獲得
URI:
記住,URI 是如下格式的:
servletName 是 servlet 類的名字.
要想加載 servlet 類, 我們必須要從 URI 中知道 servlet 的名字, 我們是通過 process
方法中的下面這行代碼獲得的:
接着, process
方法加載這個 servlet. 爲了加載這個 servlet, 你必須要創建一個 class loader 並告訴這個 class loader 這個類的位置. 這個 servlet container 指示 class loader 在 Constants.WEB_ROOT
指向的路徑下查找
. Constants.WEB_ROOT
指向工作路徑下的 webroot/ 目錄.
要加載一個 servlet, 請使用 java.net.URLClassLoader
類, 這是java.lang.ClassLoader
的間接子類
. 一旦你有了 URLClassLoader
類的實例, 就可以使用它的 loadClass
方法加載一個 servlet 類. 初始化 URLClassLoader
類非常簡單. 這個類有3種構造器, 最簡單的如下:
urls
是 java.net.URL
對象的數組, java.net.URL
對象指向當加載一個類時所要搜尋的位置. 任何以 /
結尾的 URL 都假定是一個目錄. 否則, URL 假定應用的是一個 .jar 文件, 必要的時候, 會下載並打開這個 .jar 文件.
在 servlet container 裏, 一個 class loader 能夠查找到 servlet 類的地方叫做 repository.
在我們的程序裏, 只有一處 class loader 必須查看 — 位於工作路徑下的 webroot/ 目錄. 因此, 我們先創建了一個包含單個 URL 的數組. URL
類提供了好幾種構造器, 所以有很多方式去構造一個 URL 對象. 對於本程序, 我使用了與 Tomcat 中的另一個類所使用的相同的構造器. 該構造器有如下名稱:
你可以這樣使用這個構造器: 傳遞一個 specification 作爲第二個參數, null
作爲它的第一個和第三個參數
. 但是, 還有另一種構造器, 它也接收3個參數:
因此, 如果你像下面這樣寫, 編譯器將不知道你要用的是哪個構造器:
你可以這樣解決: 告訴編譯器第三個參數的類型:
對於第二個參數, 傳遞一個包含 repository 的 String
(the directory where servlet classes can be found). 使用下面的代碼創建:
把所有部分綜合起來, 下面就是 process
方法如何構造了正確的 URLClassLoader
實例:
生成 repository 的代碼來自 org.apache.catalina.startup.ClassLoaderFactory
類的
createClassLoader
方法, 生成 URL 的代碼取自 org.apache.catalina.loader.StandardClassLoader
類的 addRepository
方法. 但, 在這裏你不必擔心這些類.
有了 class loader, 你就能使用 loadClass
方法裝載 servlet 類:
接着, process
方法爲裝載進來的 servlet 類創建一個實例, 向下造型爲 javax.servlet.Servlet
, 並調用 servlet 的 service
方法:
Compiling and Running the Application
要編譯程序, 在工作路徑敲入如下命令:
如果在 Windows 上運行程序, 需在工作路徑下敲入如下命令:
在 Linux 上, 各包間使用冒號進行分隔.
要測試程序, 在瀏覽器中敲入如下URL:
or
在你的瀏覽器中會看到如下文本:
注意你看不到第二個字符串 (Violets are blue
) 因爲只有第一個字符串才 flush 到瀏覽器. How Tomcat Works 這本書的後面章節所附帶的程序會給你演示如何解決這個問題.
Application 2
在第一個程序裏有一個嚴重的問題. 在 ServletProcessor1
類的 process
方法, 我們向上類型轉換了 ex02.pyrmont.Request
to javax.servlet.ServletRequest
的實例
, 把它作爲第一個參數傳遞給了 servlet 的 service
方法. 我們也向上造型了 ex02.pyrmont.Response
to javax.servlet.ServletResponse
的實例並把它作爲第二個參數傳遞給了 servlet 的 service
方法.
這危及 security. 知道我們的 servlet container 內部工作機制的 Servlet 程序員能夠向下造型 ServletRequest
和 ServletResponse
實例到 Request
和 Response
並調用它們的 public 方法. 有了 Request
實例, 他們就可以調用它的 parse
方法. 有了 Response
實例, 他們就能夠調用它的 sendStaticResource
方法.
你不能把 parse
和 sendStaticResource
方法設置爲 private, 因爲在 ex02.pyrmont
包裏的其它類會調用. 但是, 我們不打算讓這兩個方法在一個 servlet 內部可以使用. 一種解決方案是給 Request
和 Response
類設置一個默認的訪問修飾符, 這樣它們就不能在 ex02.pyrmont
包的外部使用. 但是, 有一個更優雅的解決方案: 使用 facade 類.
在第二個程序裏, 我們加了2個 facade 類: RequestFacade
和 ResponseFacade
. RequestFacade
類實現了 ServletRequest
接口, 傳遞一個 Request
實例進行初始化, 這個 Request 實例被指派給了它的構造器中的 ServletRequest 對象引用. ServletRequest
接口中的每個方法實現都調用 Request
對象的相應方法, 但 ServletRequest
對象自己則是 private 並且在類外不能被訪問到. 與將 Request
對象向上造型爲 ServletRequest
並傳遞給 service
方法相反, 我們構造了一個 RequestFacade
對象並傳遞給 service
方法. servlet programmer 將 ServletRequest
實例向下造型回 RequestFacade
; 可是, 他不能只訪問 ServletRequest
接口中的可用方法. 現在, parseUri
方法已經安全了.
Listing 2.5 展示了不完整的 RequestFacade
類.
Listing 2.5. The RequestFacade
class
注意 RequestFacade
的構造器
. 它接收一個 Request
對象但馬上指派給了私有的 servletRequest
對象引用. 也要注意 RequestFacade
類裏的每個方法都調用了 ServletRequest
對象中的相應方法.
在 ResponseFacade
類中也是如此.
這些是在 Application 2 中使用到的類:
-
HttpServer2
-
Request
-
Response
-
StaticResourceProcessor
-
ServletProcessor2
-
Constants
HttpServer2
類類似於 HttpServer1
, 除了在 await
方法中它使用的是 ServletProcessor2
, 而不是 ServletProcessor1
:
ServletProcessor2
類類似於 ServletProcessor1
, 除了 process
方法中的如下這部分代碼:
Compiling and Running the Application
在工作目錄敲入如下命令編譯程序.
如果在 Windows 運行這個程序, type the following command from the working directory:
在 Linux 上, 各包之間使用分號分隔.
你可以使用與 Application1 一樣的 URLs 接收到同樣的結果.
Summary
本文討論了一個簡單的 Servlet 容器, 可以用來提供靜態資源服務, 也能處理像 PrimitiveServlet
這樣簡單的
servlet
. 它也提供了javax.servlet.Servlet
接口的背景信息.
Budi Kurniawan is a senior J2EE architect and author.