Java網絡編程基本功之Servlet與Servlet容器
Servlet與Servlet容器關系
Servlet
比較這兩個的區別, 就得先搞清楚Servlet 的含義, Servlet (/?s?rvlit/ ) 翻譯成中文就是小型應用程序或者小服務程序, 與之相類似的是Server (/?s??rv?r/), 翻譯過來是服務器的意思, 可見這二者承擔類似的功能,但是Servlet更輕量。
web開發的本質就一句話:客戶端和服務器交換數據。于是使用 Java 的 Socket 套接字進行編程,去處理客戶端來的 tcp 請求,經過編解碼處理讀取請求體,獲取請求行,然后找到請求行對應的處理邏輯步入服務器的處理中,處理完畢把對應的結果返回給當前的 Socket 鏈接,響應完畢,關閉 Socket。
上述過程中, 建立連接、傳輸數據、關閉連接等過程是tomcat容器幫你做了這些事情, 而拿到請求行之后去找對應的 url 路由,這一部分是誰做的呢?是Servlet ! 簡單來說Servlet就是一段處理 web 請求的邏輯。
具體來說Servlet具有以下幾個特點:
- Servlet是用Java編寫的Server端程序,它與協議和平臺無關。
- Servlet運行于Java-enabled Web Server中。
- Java Servlet可以動態地擴展Server的能力,并采用請求-響應模式提供Web服務。
- 最早支持Servlet技術的是JavaSoft的Java Web Server。
- 此后,一些其它的基于Java的Web Server開始支持標準的Servlet API。
- Servlet的主要功能在于交互式地瀏覽和修改數據,生成動態Web內容。
上面六點中,最需要被記住的是Servlet可以動態地擴展Server的能力,并采用請求-響應模式提供Web服務。
JDK中的Servlet是一個接口:
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}
可以看到Servlet 是一個接口, 規定了請求從容器到達 web 服務端的規范,詳細內容在后面的Servlet生命周期中詳細梳理,這兒簡單概括三個重要步驟是:
- init():初始化請求的時候要做什么。
- service():拿到請求的時候要做什么。
- destory():處理完請求銷毀的時候要做什么。
所有實現 Servlet 的實現方都是在這個規范的基礎上進行開發。那么 Servlet 中的數據是從哪里來的呢?答案就是 Servlet 容器。容器才是真正與客戶端打交道的那一方。一個容器中 Servlet 可以有多個, 常見的Servlet容器Tomcat,它監聽了客戶端的請求端口,根據請求行信息確定將請求交給哪個Servlet 處理,找到處理的Servlet之后,調用該Servlet的 service() 方法,處理完畢將對應的處理結果包裝成ServletResponse 對象返回給客戶端。
Servlet容器
現在講講Servlet容器, 前面說過看Servlet只是一個接口或者說是規范, 那么就勢必有具體實現, 而Servlet具體實現或者說包裝器是Wrapper, 直接管理Wrapper的容器就是Context, 一個 Context 對應一個 Web 工程, 也就是說Context 容器如何運行將直接影響 Servlet 的工作。
由圖可以知道, Tomcat底層是Context, Context負責管理Servlet包裝類Wrapper。
下面創建一個實例對象并調用 start 方法就可以很容易啟動 Tomcat,我們還可以通過這個對象來增加和修改 Tomcat 的配置參數,如可以動態增加 Context、Servlet 等。我們就選擇 Tomcat7 自帶的 examples Web 工程,并看看它是如何加到這個 Context 容器中的。
//給 Tomcat 增加一個 Web 工程:
Tomcat tomcat = getTomcatInstance();
File appDir = new File(getBuildDirectory(), "webapps/examples");
tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath());
tomcat.start();
ByteChunk res = getUrl("http://localhost:" + getPort() +
"/examples/servlets/servlet/HelloWorldExample");
assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);
上述代碼是創建一個 Tomcat 實例并新增一個 Web 應用,然后啟動 Tomcat 并調用其中的一個 HelloWorldExample Servlet,看有沒有正確返回預期的數據。
//Tomcat 的 addWebapp 方法的代碼如下:
public Context addWebapp(Host host, String url, String path) {
silence(url);
Context ctx = new StandardContext();
ctx.setPath( url );
ctx.setDocBase(path);
if (defaultRealm == null) {
initSimpleAuth();
}
ctx.setRealm(defaultRealm);
ctx.addLifecycleListener(new DefaultWebXmlListener());
ContextConfig ctxCfg = new ContextConfig();
ctx.addLifecycleListener(ctxCfg);
ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML");
if (host == null) {
getHost().addChild(ctx);
} else {
host.addChild(ctx);
}
return ctx;
}
添加一個 Web 應用時將會創建一個 StandardContext 容器,并且給這個 Context 容器設置必要的參數(url 代表這個應用在 Tomcat 中的訪問路徑; path 代表這個應用實際的物理路徑) 其中最重要的一個配置是 ContextConfig,【ContextConfig監聽器】繼承了 【LifecycleListener 監聽器接口】,它是在調用清單 2 時被加入到 StandardContext 容器中。 當 Context 容器初始化狀態設為 init 時,添加在 Context 容器的 Listener 將會被調用。【ContextConfig監聽器】將會負責整個 Web 應用配置文件的解析工作。最后將這個 Context 容器加到父容器 Host 中。
Servlet生命周期
Servlet生命周期分為四個部分: 實例化==>初始化==>執行處理==>銷毀。
實例化
new , 服務器第一次被訪問時,加載一個Servlet容器,只會被加載一次。
初始化
init:創建完Servlet容器后,會調用僅執行一次的init()初始化方法,用于初始化Servlet對象,無論多少臺客戶端在服務器運行期間訪問都不會再執行init()方法。
可以在繼承的GenericServlet這個抽象類中看到初始化方法:
public void init() throws ServletException {
}
而在我們的Servlet類中應繼承調用該方法:
public void init() throws ServletException {
super.init();
}
創建Servlet對象的時機:
- Servlet容器啟動時:讀取web.xml配置文件中的信息,構造指定的Servlet對象,創建ServletConfig對象,同時將ServletConfig對象作為參數來調用Servlet對象的init方法。
- 在Servlet容器啟動后:客戶首次向Servlet發出請求,Servlet容器會判斷內存中是否存在指定的Servlet對象,如果沒有則創建它,然后根據客戶的請求創建HttpRequest、HttpResponse對象,從而調用Servlet 對象的service方法。
- Servlet:Servlet容器在啟動時自動創建Servlet,這是由在web.xml文件中為Servlet設置的屬性決定的。從中我們也能看到同一個類型的Servlet對象在Servlet容器中以單例的形式存在。
執行處理
執行處理——service()方法
它是Servlet的核心,負責響應客戶的請求。每當一個客戶請求一個HttpServlet對象,該對象的Service()方法就要調用,而且傳遞給這個方法一個“請求”(ServletRequest)對象和一個“響應”(ServletResponse)對象作為參數。在HttpServlet中已存在Service()方法。默認的服務功能是調用與HTTP請求的方法相應的do功能。
HttpServlet的抽象類提供了doGet()、doPost()……等方法。對應了request請求的發送方法,與之相匹配:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
上面是操作性最高的部分。
銷毀
銷毀——destroy:在服務器關閉或重啟時,Servlet會調用destroy方法來銷毀,將Servlet容器標記為垃圾文件,讓GC做回收處理。我們編寫的Servlet是調用了GenericServlet抽象類的destroy方法:
@Override
public void destroy() {
super.destroy();
}
Servlet工作原理
1、首先簡單解釋一下Servlet接收和響應客戶請求的過程:
客戶發送一個請求,Servlet是調用service()方法對請求進行響應,service()方法中對請求的方式進行了匹配。選擇調用doGet,doPost等這些方法,然后再進入對應的方法中調用邏輯層的方法,實現對客戶的響應。在Servlet接口和GenericServlet中是沒有doGet()、doPost()等等這些方法的,HttpServlet中定義了這些方法,但是都是返回error信息,所以,我們每次定義一個Servlet的時候,都必須實現doGet或doPost等這些方法。
2、每一個自定義的Servlet都必須實現Servlet的接口,Servlet接口中定義了五個方法,其中比較重要的三個方法涉及到Servlet的生命周期,分別是上文提到的init(),service(),destroy()方法。GenericServlet是一個通用的,不特定于任何協議的Servlet,它實現了Servlet接口。而HttpServlet繼承于GenericServlet,因此HttpServlet也實現了Servlet接口。所以我們定義Servlet的時候只需要繼承HttpServlet即可。
3、Servlet接口和GenericServlet是不特定于任何協議的,而HttpServlet是特定于HTTP協議的類,所以HttpServlet中實現了service()方法,并將請求ServletRequest、ServletResponse 強轉為HttpRequest 和 HttpResponse。
4、另外,Servlet是單例模式,線程是不安全的,因此在service()方法中盡量不要操作全局變量。但實際上,可以通過使用session和application來代替全局變量,只是會加大服務器負載。
Servlet處理請求的過程
- 客戶端發送請求給服務器。
- 容器根據請求及web.xml判斷對應的Servlet是否存在,如果不存在則返回404。
- 容器根據請求及web.xml判斷對應的Servlet是否已經被實例化,若是相應的Servlet沒有被實例化,則容器將會加載相應的Servlet到Java虛擬機并實例化。
- 調用實例對象的service()方法,并開啟一個新的線程去執行相關處理。調用servce方法,判斷是調用doGet方法還是doPost方法。
- 業務完成后響應相關的頁面發送給客戶端。