JavaWeb——Servlet

1、基本概念

Servlet是運行在Web服務器上的小程序,經過http協議和客戶端進行交互。html

這裏的客戶端通常爲瀏覽器,發送http請求(request)給服務器(如Tomcat)。服務器接收到請求後選擇相應的Servlet進行處理,並給出響應(response)。java

 

從這裏能夠看出Servlet並非獨立運行的程序,而是以服務器爲宿主,由服務器進行調度的。一般咱們把可以運行Servlet的服務器稱做Servlet容器,如Tomcat。web

這裏Tomcat爲何可以根據客戶端的請求去選擇相應的Servlet去執行的呢?答案是:Servlet規範。由於Servlet和Servlet容器都是遵守Servlet規範去開發的。簡單點說:咱們要寫一個Servlet,就須要直接或間接實現javax.servlet.Servlet。而且在web.xml中進行相應的配置。Tomcat在接收到客戶端的請求時,會根據web.xml裏面的配置去加載、初始化對應的Servlet實例。這個就是規範,就是雙方約定好的。小程序

2、樣例分析

在進一步解釋Servlet原理、分析源碼以前,咱們先介紹下如何在JavaWeb中使用Servlet。方法很簡單:1.編寫本身的Servlet類,這裏可使用開發工具(STS、Myeclipse等)根據嚮導快速的生成一個Servlet類。2.在web.xml中配置servlet。這裏的知識很簡單,因此不作過多贅述。直接上代碼。(這裏須要注意的是,servlet3.0以後提供了註解WebServlet的方式配置servlet,這裏就不作介紹了,感興趣的能夠自行去百度,只是配置的形式不一樣而已,沒有本質區別。因此下文仍是爲web.xml爲例)瀏覽器

TestServlet.java緩存

 1 public class TestServlet extends HttpServlet {
 2     private static final long serialVersionUID = 1L;
 3 
 4     public TestServlet() {
 5     }
 6 
 7     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 8         response.getWriter().append("Served at: ").append(request.getContextPath());
 9     }
10 
11     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
12         doGet(request, response);
13     }
14 }

web.xml服務器

1 <servlet>
2     <servlet-name>TestServlet</servlet-name>
3     <servlet-class>com.nantang.servlet.TestServlet</servlet-class>
4 </servlet>
5 <servlet-mapping>
6     <servlet-name>TestServlet</servlet-name>
7     <url-pattern>/test</url-pattern>
8 </servlet-mapping>

啓動Tomcat,瀏覽器訪問/test。將會訪問TestServlet。返回客戶端請求的上下文路徑。併發

這裏須要擴展的有幾點:app

1.若是一個servlet須要映射多個url-pattern,那麼就在<servlet-mapping></servlet-mapping>標籤下寫多個<url-pattern></url-pattern>,如:框架

1 <servlet-mapping>
2     <servlet-name>TestServlet</servlet-name>
3     <url-pattern>/test1</url-pattern>
4     <url-pattern>/test2</url-pattern>
5 </servlet-mapping>

2.對於不一樣的servlet,不容許出現相同的url-pattern。

3.若是不一樣的servlet,它們的url-patter存在包含關係,那麼容器會調用更具象的servlet去處理客戶端請求。好比有兩個servlet,servlet1的url-pattern是"/*",servlet2的url-pattern是"/test"。那麼這個時候若是客戶端調用的url是http://localhost:8080/demo/test,容器會使用servlet2去處理客戶端的請求。雖說"/*""/test"都匹配客戶請求的url,可是容器會選擇更貼切的。這裏不會出現多個servlet處理同一個請求的現象。

3、源碼分析

上面說過,咱們本身編寫的Servlet類都必須直接或間接實現javax.servlet.Servlet。但是上面的例子TestServlet繼承的是HttpServlet,那是由於HttpServlet間接的實現了javax.servlet.Servlet。下面是HttpServlet的繼承層級(類圖中的方法並無一一列舉,由於下面會逐一解釋):

下面咱們由上往下層層分析:

1 ServletContext

一個web應用對應一個ServletContext實例,關於ServletContext的詳細介紹,能夠參考另外一篇博文ServletContext

2.ServletConfig

ServletConfig實例是由servlet容器構造的,當須要初始化servlet的時候,容器根據web.xml中的配置以及運行時環境構造出ServletConfig實例,並經過回調servlet的init方法傳遞給servlet(這個方法後面會講到)。因此一個servlet實例對應一個ServletConfig實例

 

 1 public interface ServletConfig {
 2 
 3     public String getServletName();
 4 
 5     public ServletContext getServletContext();
 6 
 7     public String getInitParameter(String name);
 8 
 9     public Enumeration getInitParameterNames();
10 }

2.1 getServletName

getServletName方法返回servlet實例的名稱,這個就是咱們在web.xml中<servlet-name>標籤中配置的名字,固然也能夠在服務器控制檯去配置。若是這兩個地方都沒有配置servlet名稱,那麼將會返回servlet的類名。

2.2 getServletContext

getServletContext方法返回ServletContext實例,也就是咱們上面說的應用上下文。

2.3 getInitParameter和getInitParameterNames

這兩個方法是用來獲取servlet的初始化參數的,這個參數是在web.xml裏面配置的(以下所示)。getInitParameter是根據參數名獲取參數值,getInitParameterNames獲取參數名集合。

這裏須要注意的是當須要配置多個初始化參數時,應該寫多個<init-param></init-param>對,而不是在一個<init-param></init-param>對裏面寫多個<param-name></param-name>和<param-value></param-value>對。

 1 <servlet>
 2     <servlet-name>TestServlet</servlet-name>
 3     <servlet-class>com.nantang.servlet.TestServlet</servlet-class>
 4     <init-param>
 5         <param-name>a</param-name>
 6         <param-value>1</param-value>
 7     </init-param>
 8     <init-param>
 9         <param-name>b</param-name>
10         <param-value>2</param-value>
11     </init-param>
12 </servlet>

3 Servlet

最原始最簡單的JaveWeb模型,就是一個servlet容器上運行着若干個servlet用來處理客戶端的請求。因此說servlet是JavaWeb最核心的東西,咱們的業務邏輯基本上都是經過servlet實現的(雖然如今有各類框架,不用去直接編寫servlet,但本質上仍是在使用servlet)。

 1 public interface Servlet {
 2     
 3     public void init(ServletConfig config) throws ServletException;
 4 
 5     public ServletConfig getServletConfig();
 6 
 7     public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
 8 
 9     public String getServletInfo();
10 
11     public void destroy();
12 }

全部的servlet都是javax.servlet.Servlet的子類,就像Java裏面全部的類都是Object的子類同樣。Servlet類規定了每一個servlet應該實現的方法,這個是遵循Servlet規範的。可是自定義的servlet通常不用直接實現Servlet,而是繼承javax.servlet.GenericServlet或者javax.servlet.http.HttpServlet就好了。咱們上面的TestServlet就是繼承HttpServlet,這是由於HttpServlet間接實現了Servlet,提供了通用的功能。因此咱們在自定義的TestServlet裏面只須要專一實現業務邏輯就好了。

Servlet裏面有三個比較重要的方法:init、service、destroy。它們被稱做是servlet生命週期的方法,它們都是由servlet容器調用。另外兩個方法用於獲取servlet相關信息的,須要根據業務邏輯進行實現和調用。

3.1 init

init方法是servlet的初始化方法,當客戶端第一次請求servlet的時候,JVM對servlet類進行加載和實例化。(若是須要容器啓動時就初始化servlet,能夠在web.xml配置<load-on-startup>1</load-on-startup>)

這裏須要注意的是,servlet會先執行默認的構造函數,而後回調servlet實例的init方法,傳入ServletConfig參數。這個參數上面說過,是servlet容器根據web.xml中的配置和運行時環境構造的實例。經過init方法注入到servlet。init方法在servlet的生命週期中只會被調用一次,在客戶端的後續請求中將不會再調用。

3.2 service

service方法是處理業務邏輯的核心方法。當servlet容器接收到客戶端的請求後,會根據web.xml中配置的<url-pattern>找到相應的servlet,回調service方法處理客戶端的請求並給出響應。

3.3 destroy

JDK文檔解釋這個方法說:這個方法會在全部的線程的service()方法執行完成或者超時後執行。這裏只是說明了,當servlet容器要去調用destroy方式的時候,須要等待一會,等待全部線程都執行完或者達到超時的限制。

這裏並無說清楚什麼狀況下servlet容器會觸發這個動做。How Tomcat Works一書中對這個作了解釋:當servlet容器關閉或須要更多內存的時候,會銷燬servlet。這個方法就使得servlet容器擁有回收資源的能力。

一樣地,destroy方法在servlet的生命週期中只會被調用一次。

3.4 getServletConfig

這個方法返回ServletConfig實例,這個對象即爲servlet容器回調init方法的時候傳入的實例。因此自定義的Servlet通常的實現方式爲:在init方法裏面把傳入的ServletConfig存儲到servlet的屬性字段。在getServletConfig的實現裏返回該實例。這個在後續解釋javax.servlet.GenericServlet的源碼時,可以看到。

3.5 getServletInfo

返回關於servlet的信息,這個由自定義的servlet自行實現,不過通常建議返回servlet的做者、版本號、版權等信息。

4.GenericServlet

GenericServlet從名字就能看的出來是servlet的通常實現,實現了servlet具備的通用功能,因此咱們自定義的servlet通常不須要直接實現Servlet接口,只須要集成GenericServletGenericServlet實現了Servlet和ServletConfig接口。

4.1 GenericServlet對Servlet接口的實現

 1 private transient ServletConfig config;
 2 
 3 public void init(ServletConfig config) throws ServletException {
 4     this.config = config;
 5     this.init();
 6 }
 7 
 8 public void init() throws ServletException {}
 9 
10 public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
11 
12 public void destroy() {}
13 
14 public ServletConfig getServletConfig() {
15     return config;
16 }
17 
18 public String getServletInfo() {
19     return "";
20 }

能夠說,GenericServlet對Servlet方法的實現邏輯很是簡單。就是把一些必要的邏輯寫了下。

1.init方法就是把容器傳入的ServletConfig實力存儲在類的私有屬性conifg裏面,而後調用一個init無參的空方法。這麼作的意義在於,咱們若是想在自定義的servlet類裏面在初始化的時候添加些業務邏輯,只須要重寫無參的init方法就行了,咱們不須要關注ServletConfig實例的存儲細節了。

2.service和destroy方法並未實現具體邏輯。

3.getServletConfig就是返回init方法裏面存儲的config。getServletInfo就是返回空字符串,若是有業務須要,能夠在子類裏面重寫。

4.2 GenericServlet對於ServletConfig接口的實現

 1 public String getServletName() {
 2     ServletConfig sc = getServletConfig();
 3     if (sc == null) {
 4         throw new IllegalStateException(
 5             lStrings.getString("err.servlet_config_not_initialized"));
 6     }
 7     return sc.getServletName();
 8 }
 9 
10 public ServletContext getServletContext() {
11     ServletConfig sc = getServletConfig();
12     if (sc == null) {
13         throw new IllegalStateException(
14             lStrings.getString("err.servlet_config_not_initialized"));
15     }
16     return sc.getServletContext();
17 }
18 
19 public String getInitParameter(String name) {
20     ServletConfig sc = getServletConfig();
21     if (sc == null) {
22         throw new IllegalStateException(
23             lStrings.getString("err.servlet_config_not_initialized"));
24     }
25     return sc.getInitParameter(name);
26 }
27 
28 public Enumeration getInitParameterNames() {
29     ServletConfig sc = getServletConfig();
30     if (sc == null) {
31         throw new IllegalStateException(
32             lStrings.getString("err.servlet_config_not_initialized"));
33     }
34     return sc.getInitParameterNames();
35 }

這四個方法的實現就跟一個模子刻出來的同樣,都是取得ServletConfig實例,而後調用相應的方法。其實GenericServlet徹底沒有必要實現ServletConfig,這麼作僅僅是爲了方便。當咱們集成GenericServlet寫本身的servlet的時候,若是須要獲取servlet的配置信息如初始化參數,就不須要寫形如:「ServletConfig sc = getServletConfig();if (sc == null) ...;return sc.getInitParameterNames();」這些冗餘代碼了。除此以外,沒有別的意義。

5 HttpServlet

HttpServlet是一個針對HTTP協議的通用實現,它實現了HTTP協議中的基本方法get、post等,經過重寫service方法實現方法的分派。

 1 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException{
 2     HttpServletRequest    request;
 3     HttpServletResponse    response;
 4     try {
 5         request = (HttpServletRequest) req;
 6         response = (HttpServletResponse) res;
 7     } catch (ClassCastException e) {
 8         throw new ServletException("non-HTTP request or response");
 9     }
10     service(request, response);
11 }

重寫的service方法將參數轉換成HttpServletRequestHttpServletResponse,並調用本身的另外一個重載service方法。

 1 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
 2     String method = req.getMethod();
 3     if (method.equals(METHOD_GET)) {
 4         long lastModified = getLastModified(req);
 5         if (lastModified == -1) {
 6             doGet(req, resp);
 7         } else {
 8             long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
 9             if (ifModifiedSince < (lastModified / 1000 * 1000)) {
10                 maybeSetLastModified(resp, lastModified);
11                 doGet(req, resp);
12             } else {
13                 resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
14             }
15         }
16     } else if (method.equals(METHOD_HEAD)) {
17         long lastModified = getLastModified(req);
18         maybeSetLastModified(resp, lastModified);
19         doHead(req, resp);
20     } else if (method.equals(METHOD_POST)) {
21         doPost(req, resp);        
22     } else if (method.equals(METHOD_PUT)) {
23         doPut(req, resp);            
24     } else if (method.equals(METHOD_DELETE)) {
25         doDelete(req, resp);        
26     } else if (method.equals(METHOD_OPTIONS)) {
27         doOptions(req,resp);        
28     } else if (method.equals(METHOD_TRACE)) {
29         doTrace(req,resp);        
30     } else {
31         String errMsg = lStrings.getString("http.method_not_implemented");
32         Object[] errArgs = new Object[1];
33         errArgs[0] = method;
34         errMsg = MessageFormat.format(errMsg, errArgs);        
35         resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
36     }
37 }

這個方法的的邏輯也很簡單,就是解析出客戶端的request是哪一種中方法,若是是get方法則調用doGet,若是是post則調用doPost等等。這樣咱們在繼承HttpServlet的時候就無需重寫service方法,咱們能夠根據本身的業務重寫相應的方法。通常狀況下咱們的應用基本就是get和post調用。那麼咱們只須要重寫doGet和doPost就好了。

這裏須要注意的是3-15行代碼,這裏對資源(好比頁面)的修改時間進行驗證,判斷客戶端是不是第一次請求該資源,或者該資源是否被修改過。若是這兩個條件有一個被知足那麼就調用doGet方法。不然返回狀態304(HttpServletResponse.SC_NOT_MODIFIED),這個狀態就是告訴客戶端(瀏覽器),能夠只用本身上一次對該資源的緩存。

不過HttpServlet對於判斷資源修改時間的邏輯很是簡單粗暴:

1 protected long getLastModified(HttpServletRequest req) {
2     return -1;
3 }

方法始終返回-1,這樣就會致使每次都會調用doGet方法從服務器取資源而不會使用瀏覽器的本地緩存。因此若是咱們本身的servlet要使用瀏覽器的緩存,下降服務器的壓力,就須要重寫getLastModified方法。

最後咱們來看一下HttpServlet對http一些方法的實現,在全部的方法中,HttpServlet已經對doOptions和doTrace方法實現了通用的邏輯,因此咱們通常不用重寫這兩個方法,感興趣的能夠本身去看下源碼。

這裏咱們列舉下最經常使用的兩個方法doGet和doPost:

 1 protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException{
 2     String protocol = req.getProtocol();
 3     String msg = lStrings.getString("http.method_get_not_supported");
 4     if (protocol.endsWith("1.1")) {
 5         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
 6     } else {
 7         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
 8     }
 9 }
10     
11 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
12     String protocol = req.getProtocol();
13     String msg = lStrings.getString("http.method_post_not_supported");
14     if (protocol.endsWith("1.1")) {
15         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
16     } else {
17         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
18     }
19 }

其實這兩個實現裏面也沒作什麼有用的邏輯,因此通常狀況下都要重寫這兩個方法,就像咱們最初的TestServlet那樣。doHead、doPut、doDelete也是這樣的代碼模板,因此若是有業務須要的話,咱們都要重寫對應的方法。

4、總結

講了這麼多,能夠這麼說:Servlet是JavaWeb裏面最核心的組件。只有對它徹底融會貫通,才能去進一步去理解上層框架Struts、Spring等。

另外須要明確的是:一個Web應用對應一個ServletContext,一個Servlet對應一個ServletConfig。每一個Servlet都是單例的,因此須要本身處理好併發的場景。

 

做者:南唐三少
出處:http://www.cnblogs.com/nantang
若是您以爲閱讀本文對您有幫助,請點一下「推薦」按鈕,您的「推薦」將是咱們最大的寫做動力!歡迎各位轉載,可是未經做者本人贊成,轉載文章以後必須在文章頁面明顯位置給出做者和原文連接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索