瀏覽器發給服務端的是一個HTTP格式的請求,HTTP伺服器收到這個請求後,需要呼叫服務端程式來處理,所謂的服務端程式就是你寫的Java類,一般來說不同的請求需要由不同的Java類來處理。
那麼問題來了,HTTP伺服器怎麼知道要呼叫哪個Java類的哪個方法呢。最直接的做法是在HTTP伺服器程式碼裡寫一大堆if else邏輯判斷:如果是A請求就調X類的M1方法,如果是B請求就調Y類的M2方法。但這樣做明顯有問題,因為HTTP伺服器的程式碼跟業務邏輯耦合在一起了,如果新加一個業務方法還要改HTTP伺服器的程式碼。
那該怎麼解決這個問題呢?我們知道,面向介面程式設計是解決耦合問題的法寶,於是有一夥人就定義了一個介面,各種業務類都必須實現這個介面,這個介面就叫Servlet介面,有時我們也把實現了Servlet介面的業務類叫作Servlet。
但是這裡還有一個問題,對於特定的請求,HTTP伺服器如何知道由哪個Servlet來處理呢?Servlet又是由誰來例項化呢?顯然HTTP伺服器不適合做這個工作,否則又和業務類耦合了。
於是,還是那夥人又發明了Servlet容器,Servlet容器用來載入和管理業務類。HTTP伺服器不直接跟業務類打交道,而是把請求交給Servlet容器去處理,Servlet容器會將請求轉發到具體的Servlet,如果這個Servlet還沒建立,就載入並例項化這個Servlet,然後呼叫這個Servlet的介面方法。因此Servlet介面其實是Servlet容器跟具體業務類之間的介面。下面我們透過一張圖來加深理解。
圖的左邊表示HTTP伺服器直接呼叫具體業務類,它們是緊耦合的。再看圖的右邊,HTTP伺服器不直接呼叫業務類,而是把請求交給容器來處理,容器透過Servlet介面呼叫業務類。因此Servlet介面和Servlet容器的出現,達到了HTTP伺服器與業務類解耦的目的。
而Servlet介面和Servlet容器這一整套規範叫作Servlet規範。Tomcat和Jetty都按照Servlet規範的要求實現了Servlet容器,同時它們也具有HTTP伺服器的功能。作為Java程式設計師,如果我們要實現新的業務功能,只需要實現一個Servlet,並把它註冊到Tomcat(Servlet容器)中,剩下的事情就由Tomcat幫我們處理了。
接下來我們來看看Servlet介面具體是怎麼定義的,以及Servlet規範又有哪些要重點關注的地方呢?
Servlet介面
Servlet介面定義了下面五個方法:
public interface Servlet {
void init(ServletConfig config) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
String getServletInfo();
void destroy();
}
其中最重要是的service方法,具體業務類在這個方法裡實現處理邏輯。這個方法有兩個引數:ServletRequest和ServletResponse。ServletRequest用來封裝請求資訊,ServletResponse用來封裝響應資訊,因此本質上這兩個類是對通訊協議的封裝。
比如HTTP協議中的請求和響應就是對應了HttpServletRequest和HttpServletResponse這兩個類。你可以透過HttpServletRequest來獲取所有請求相關的資訊,包括請求路徑、Cookie、HTTP頭、請求引數等。
你可以看到介面中還有兩個跟生命週期有關的方法init和destroy,這是一個比較貼心的設計,Servlet容器在載入Servlet類的時候會呼叫init方法,在解除安裝的時候會呼叫destroy方法。我們可能會在init方法裡初始化一些資源,並在destroy方法裡釋放這些資源,比如Spring MVC中的DispatcherServlet,就是在init方法裡建立了自己的Spring容器。
你還會注意到ServletConfig這個類,ServletConfig的作用就是封裝Servlet的初始化引數。你可以在web.xml給Servlet配置引數,並在程式裡透過getServletConfig方法拿到這些引數。
我們知道,有介面一般就有抽象類,抽象類用來實現介面和封裝通用的邏輯,因此Servlet規範提供了GenericServlet抽象類,我們可以透過擴充套件它來實現Servlet。雖然Servlet規範並不在乎通訊協議是什麼,但是大多數的Servlet都是在HTTP環境中處理的,因此Servet規範還提供了HttpServlet來繼承GenericServlet,並且加入了HTTP特性。這樣我們透過繼承HttpServlet類來實現自己的Servlet。
Servlet容器
我在前面提到,為了解耦,HTTP伺服器不直接呼叫Servlet,而是把請求交給Servlet容器來處理,那Servlet容器又是怎麼工作的呢?接下來我會介紹Servlet容器大體的工作流程,一起來聊聊我們非常關心的兩個話題:Web應用的目錄格式是什麼樣的,以及我該怎樣擴充套件和定製化Servlet容器的功能。
工作流程
當客戶請求某個資源時,HTTP伺服器會用一個ServletRequest物件把客戶的請求資訊封裝起來,然後呼叫Servlet容器的service方法,Servlet容器拿到請求後,根據請求的URL和Servlet的對映關係,找到相應的Servlet,如果Servlet還沒有被載入,就用反射機制建立這個Servlet,並呼叫Servlet的init方法來完成初始化,接著呼叫Servlet的service方法來處理請求,把ServletResponse物件返回給HTTP伺服器,HTTP伺服器會把響應傳送給客戶端。同樣我透過一張圖來幫助你理解。
Web應用
Servlet容器會例項化和呼叫Servlet,那Servlet是怎麼註冊到Servlet容器中的呢?一般來說,我們是以Web應用程式的方式來部署Servlet的,而根據Servlet規範,Web應用程式有一定的目錄結構,在這個目錄下分別放置了Servlet的類檔案、配置檔案以及靜態資源,Servlet容器透過讀取配置檔案,就能找到並載入Servlet。Web應用的目錄結構大概是下面這樣的:
| - MyWebApp
| - WEB-INF/web.xml -- 配置檔案,用來配置Servlet等
| - WEB-INF/lib/ -- 存放Web應用所需各種JAR包
| - WEB-INF/classes/ -- 存放你的應用類,比如Servlet類
| - META-INF/ -- 目錄存放工程的一些資訊
Servlet規範裡定義了ServletContext這個介面來對應一個Web應用。Web應用部署好後,Servlet容器在啟動時會載入Web應用,併為每個Web應用建立唯一的ServletContext物件。你可以把ServletContext看成是一個全域性物件,一個Web應用可能有多個Servlet,這些Servlet可以透過全域性的ServletContext來共享資料,這些資料包括Web應用的初始化引數、Web應用目錄下的檔案資源等。由於ServletContext持有所有Servlet例項,你還可以透過它來實現Servlet請求的轉發。
擴充套件機制
引入了Servlet規範後,你不需要關心Socket網路通訊、不需要關心HTTP協議,也不需要關心你的業務類是如何被例項化和呼叫的,因為這些都被Servlet規範標準化了,你只要關心怎麼實現的你的業務邏輯。這對於程式設計師來說是件好事,但也有不方便的一面。所謂規範就是說大家都要遵守,就會千篇一律,但是如果這個規範不能滿足你的業務的個性化需求,就有問題了,因此設計一個規範或者一箇中介軟體,要充分考慮到可擴充套件性。Servlet規範提供了兩種擴充套件機制:Filter和Listener。
Filter是過濾器,這個介面允許你對請求和響應做一些統一的定製化處理,比如你可以根據請求的頻率來限制訪問,或者根據國家地區的不同來修改響應內容。過濾器的工作原理是這樣的:Web應用部署完成後,Servlet容器需要例項化Filter並把Filter連結成一個FilterChain。當請求進來時,獲取第一個Filter並呼叫doFilter方法,doFilter方法負責呼叫這個FilterChain中的下一個Filter。
Listener是監聽器,這是另一種擴充套件機制。當Web應用在Servlet容器中執行時,Servlet容器內部會不斷的發生各種事件,如Web應用的啟動和停止、使用者請求到達等。 Servlet容器提供了一些預設的監聽器來監聽這些事件,當事件發生時,Servlet容器會負責呼叫監聽器的方法。當然,你可以定義自己的監聽器去監聽你感興趣的事件,將監聽器配置在web.xml中。比如Spring就實現了自己的監聽器,來監聽ServletContext的啟動事件,目的是當Servlet容器啟動時,建立並初始化全域性的Spring容器。
本作品採用《CC 協議》,轉載必須註明作者和本文連結