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實例。這個就是規範,就是雙方約定好的。小程序
在進一步解釋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處理同一個請求的現象。
上面說過,咱們本身編寫的Servlet類都必須直接或間接實現javax.servlet.Servlet。但是上面的例子TestServlet繼承的是HttpServlet,那是由於HttpServlet間接的實現了javax.servlet.Servlet。下面是HttpServlet的繼承層級(類圖中的方法並無一一列舉,由於下面會逐一解釋):
下面咱們由上往下層層分析:
一個web應用對應一個ServletContext實例,關於ServletContext的詳細介紹,能夠參考另外一篇博文ServletContext。
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 }
getServletName方法返回servlet實例的名稱,這個就是咱們在web.xml中<servlet-name>標籤中配置的名字,固然也能夠在服務器控制檯去配置。若是這兩個地方都沒有配置servlet名稱,那麼將會返回servlet的類名。
getServletContext方法返回ServletContext實例,也就是咱們上面說的應用上下文。
這兩個方法是用來獲取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>
最原始最簡單的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相關信息的,須要根據業務邏輯進行實現和調用。
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的生命週期中只會被調用一次,在客戶端的後續請求中將不會再調用。
service方法是處理業務邏輯的核心方法。當servlet容器接收到客戶端的請求後,會根據web.xml中配置的<url-pattern>找到相應的servlet,回調service方法處理客戶端的請求並給出響應。
JDK文檔解釋這個方法說:這個方法會在全部的線程的service()方法執行完成或者超時後執行。這裏只是說明了,當servlet容器要去調用destroy方式的時候,須要等待一會,等待全部線程都執行完或者達到超時的限制。
這裏並無說清楚什麼狀況下servlet容器會觸發這個動做。How Tomcat Works一書中對這個作了解釋:當servlet容器關閉或須要更多內存的時候,會銷燬servlet。這個方法就使得servlet容器擁有回收資源的能力。
一樣地,destroy方法在servlet的生命週期中只會被調用一次。
這個方法返回ServletConfig實例,這個對象即爲servlet容器回調init方法的時候傳入的實例。因此自定義的Servlet通常的實現方式爲:在init方法裏面把傳入的ServletConfig存儲到servlet的屬性字段。在getServletConfig的實現裏返回該實例。這個在後續解釋javax.servlet.GenericServlet的源碼時,可以看到。
返回關於servlet的信息,這個由自定義的servlet自行實現,不過通常建議返回servlet的做者、版本號、版權等信息。
GenericServlet從名字就能看的出來是servlet的通常實現,實現了servlet具備的通用功能,因此咱們自定義的servlet通常不須要直接實現Servlet接口,只須要集成GenericServlet。GenericServlet實現了Servlet和ServletConfig接口。
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就是返回空字符串,若是有業務須要,能夠在子類裏面重寫。
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();」這些冗餘代碼了。除此以外,沒有別的意義。
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方法將參數轉換成HttpServletRequest和HttpServletResponse,並調用本身的另外一個重載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也是這樣的代碼模板,因此若是有業務須要的話,咱們都要重寫對應的方法。
講了這麼多,能夠這麼說:Servlet是JavaWeb裏面最核心的組件。只有對它徹底融會貫通,才能去進一步去理解上層框架Struts、Spring等。
另外須要明確的是:一個Web應用對應一個ServletContext,一個Servlet對應一個ServletConfig。每一個Servlet都是單例的,因此須要本身處理好併發的場景。
做者:南唐三少
出處:http://www.cnblogs.com/nantang
若是您以爲閱讀本文對您有幫助,請點一下「推薦」按鈕,您的「推薦」將是咱們最大的寫做動力!歡迎各位轉載,可是未經做者本人贊成,轉載文章以後必須在文章頁面明顯位置給出做者和原文連接,不然保留追究法律責任的權利。