How Servlet Containers Work

編輯注: 在這個系列中, 本文和上一篇, "How Web Servers Work," Tomcat 內部工作原理的指南書籍 How Tomcat Works 的節選. 如果你還沒有讀過上一篇, 那麼趕緊先去讀讀吧; 那篇文章告訴了你一些有用的背景信息. 在本文裏, 你會學習到怎樣去創建2servlet 容器. 本書附帶的程序可以下載. 如果你有興趣的話, 在限定的時間內, 作者的網站上允許下載本書的其它部分.

本文講解了一個簡單的 servlet 容器是怎樣工作的. 將會給您展示2servlet 容器應用程序; 第一個儘可能簡單, 第二個則在第一個基礎上做了美化. 我不想把第一個容器做的完美的唯一原因是讓它儘可能保持簡單. 更多複雜的 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 的時候. 這個方法只會在這個servletservice方法中的所有線程都已經退出來或者超時時間已過之後. servlet container 調用了 destroy之後, 它就不會再調用這個servlet上的 service 方法. destroy 方法給了servlet一個機會去清理該servlet正在把持的任何資源 (例如, 內存, 文件句柄, 和線程) 和確認任何持久性狀態信息都已經與內存中這個servlet的當前狀態進行了同步.

Listing 2.1 包含PrimitiveServlet的代碼, 這是一個非常簡單的 servlet,你可以用來測試本文這個 servlet container 程序. PrimitiveServlet 類實現了 javax.servlet.Servlet (所有servlet 都必須實現) 並且爲所有5servlet 方法都提供了實現. 它做的事情非常簡單: 每次 init, service, 或者 destroy 方法中的任意一個被調用, servlet 就向控制檯打印方法名. service 方法中的代碼也會從ServletResponse對象獲得一個 java.io.PrintWriter 對象向瀏覽器發送字符串.

Listing 2.1. PrimitiveServlet.java





























Application 1

現在, 讓我們從servlet container 的視角來看 servlet 編程. 簡而言之, 一個功能完備的 servlet container 需爲每個servletHTTP 請求完成如下工作:

  • 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:


因此, 如果你想使用瀏覽器請求一個叫PrimitiveServletservlet, 在瀏覽器地址欄中輸入如下 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 對象並傳給這個servletservice 方法.

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 實例. 它從 ServletRequestgetRequestUri方法獲得 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 .

在第二個程序裏, 我們加了2facade : 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.

 
發佈了2 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章