在上一篇文章中,咱們演示也證實了Servlet 是一種動態web資源開發的技術,即我能夠在瀏覽器中輸入URL,而後就能夠在瀏覽器中看到咱們編寫的Servlet資源。java
那當咱們在瀏覽器上一塊兒一個HTTP請求以後,具體的流程是怎麼樣的呢?借用LinkinStar博文中的圖:web
上面這副圖講解了整個HTTP請求到相應的過程,在這裏進行下解釋:瀏覽器
當在瀏覽器中輸入URL後,會經過hosts文件/dns服務器解析爲IP地址,進而找對應的服務器來提供服務,當服務器端收到請求後:tomcat
1)分析當前的這個請求訪問的是當前服務器中的那個WEB應用安全
這個能夠從請求行中的請求資源部分來分析出來。服務器
2)分析當前請求要訪問的這個WEB應用的那個資源:多線程
從上述請求行的資源部分能夠分析出請求的是什麼資源併發
3)查找web.xml文件,查看有沒有對應的虛擬路徑,若是有則用這個虛擬路徑對應的資源作出相應。app
4)服務器從response對象中獲取以前寫入的數據,而後組成響應發送到瀏覽器中。less
一個HTTP響應表明服務器向客戶端回送的數據,它包括:一個狀態行、若干消息頭、以及實體內容。
2、Servlet的運行過程及生命週期
Servlet程序由WEB服務器調用,web服務器收到客戶端的Servlet請求後,會首先檢查是否已經裝載並建立了該Servlet的實例對象;若是是,則直接跳到第四步,不然,執行第二步。
1)裝載並建立該Servlet實例對象【加載:容器經過類家長羣使用Servlet類對應的文件加載Servlet,建立:經過調用servlet構造函數建立一個Servlet對象】
2)調用Servlet實例對象的init方法
3)處理客戶請求:每當有一個客戶請求,容器會建立一個線程來處理客戶請求,用於封裝HTTP請求消息的HttpServletRequest對象和一個表明HTTP響應消息的HttpServletRespons對象,而後調用Servlet的service方法並將請求和響應對象做爲參數傳遞進去。
4)卸載:調用destroy方法讓servlet本身釋放其佔用的資源。
所以Servlet的生命週期能夠分爲5個階段:加載、建立、初始化、處理客戶請求、卸載。
(1)一般狀況下,服務器會再Servlet第一次調用時候加載並建立此Servlet的實例對象,同時調用init方法作初始化操做。
(2)一旦此Servlet實例建立出來後,該實例就駐留在內存中,爲後續對這個Servlet的請求作出相應的服務,每次對這個Servlet的訪問都會致使Servlet中service方法執行;
(3)當web應用被移除容器或者關閉服務器的時候,隨着web應用的銷燬,Servlet也會被銷燬,在銷燬以前服務器會調用Servlet的destroy方法作一些善後的工做。
下面3個方法能夠基本表明Servlet的生命週期:
*init方法,負責初始化Servlet對象
*service方法,負責響應客戶的請求(調用doGET 或 doPost 方法)
*destory方法,當Servlet對象退出生命週期的時候,負責釋放佔用的資源。
PS:在Servlet的整個生命週期內,Servlet的init方法只有在Servlet被建立的時候被調用一次,每次對這個Servlet的訪問都會致使Servlet中的service方法執行。
例如:如今瀏覽器連續訪問Servlet 5次,內存中只有一個Sevlet對象。Servlet對象由服務器建立(建立一次),request和response由Servlet容器建立(建立5次),會調用5次service方法。
發送一次請求:http://localhost:8000/Servlet_Demo01/servlet/ServletDemo01
咱們看下控制檯的輸出:
可見,正常Servlet是在被第一次訪問的時候加載並建立,同時開始調用service方法。
那咱們再發送5次請求看下結果如何:
可見沒有打印Servlet 初始化,但發送了請求,可見service方法就被調用了,緊接着咱們中止容器看下,是否會調用destory方法。
可見當中止web服務器的時候,servlet被銷燬了。
總結:能夠看到,Servlet只會初始化一次,以後的話,咱們屢次訪問的是同一個Servlet對象,此時,即便關掉網頁,Servlet也不會銷燬,只有tomcat服務器纔會銷燬servlet.
那麼咱們有沒有辦法,啓動web服務器的時候同時加載並建立servlet呢?方法是有的,須要在web.xml中配置:<load-on-startup>1</load-on-startup>
<servlet> <description>This is the description of my J2EE component</description> <display-name>This is the display name of my J2EE component</display-name> <servlet-name>ServletDemo01</servlet-name> <servlet-class>mdj.servlet.study.ServletDemo01</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ServletDemo01</servlet-name> <url-pattern>/servlet/ServletDemo01</url-pattern> </servlet-mapping>
而後咱們看下現象:
可見啓動的時候,就進行了建立和加載,那麼咱們再次訪問URL請求的時候,是否還會建立呢,答案應該是不會,看下面結果。
3、Servlet的結構
上面咱們講解了Servlet的運行過程和生命週期,下面咱們看看其結構:
1)Servlet接口有五個方法
init初始化,就是把servlet裝載到內存中,只會被調用一次
getServletConfig獲取servletConfig對象
service主要的服務方法,放業務邏輯,每次都會被調用
getServletInfo獲得servlet配置信息
destroy銷燬該servlet,從內存中清除掉
2)繼承GenericServlet
GenericServlet實現了servlet接口
而後只有一個抽象方法須要你本身去重寫
那就是service方法,因此相比來講init別的方法他都幫你實現好了,只要你寫service方法就能夠了。
至少看起來繼承GenericServlet比直接實現servlet接口要方便
3)繼承HttpServlet
由於後來發現servlet主要是爲了服務於http請求的,並且發現GenericServlet對於http來講還不夠好
因此有了HttpServlet,首先它是繼承自GenericServlet
而後它有不少http相關的方法,post,get,put等待
用戶能夠根據本身須要來實現這些方法
每一個過來的請求都會調用service方法,最後service會根據不用的請求分發到不一樣的地方去作。
相比較而言,HttpServlet覆寫了GenericServlet,service方法體內的代碼會自動判斷用戶的請求方式,如爲GET的請求則調用doGet方法,如爲Post請求,則調用doPost方法,所以開發人員在編寫servlet的時候,一般指須要繼承HttpServlet,而後覆寫doGet和doPost方法,不要去覆寫service方法。
咱們來看一下源碼:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { // servlet doesn't support if-modified-since, no reason // to go through further expensive logic doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < (lastModified / 1000 * 1000)) { // If the servlet mod time is later, call doGet() // Round down to the nearest second for a proper compare // A ifModifiedSince of -1 will always be less maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { // // Note that this means NO servlet supports whatever // method was requested, anywhere on this server. // String errMsg = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); } }
這個就是HttpServlet中service方法中的源碼;
首先咱們看到的就是String method = req.getMethod();這個就是經過request獲取方法,而後根據方法判斷調用哪個方法,要說明的是前面它已經定義好了這些字符串
private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_GET = "GET";
private static final String METHOD_OPTIONS = "OPTIONS";
private static final String METHOD_POST = "POST";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_TRACE = "TRACE";
須要指出的是,前面說過 HttpServlet是繼承自GenericServlet,而GenericServlet須要用戶實現一個service,剛纔咱們看到的是HttpServlet本身的。
在GenericServlet中也有一個service方法:
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest) req; response = (HttpServletResponse) res; } catch (ClassCastException e) { throw new ServletException("non-HTTP request or response"); } service(request, response); }
源碼是這樣的,它會把原來的ServletRequest 請求直接強轉換成HttpServletRequest而後再去調用它正真的service方法。
這點是須要指出的,HttpServletRequest比ServletRequest進行了進一步的封裝,方法更適合http。
4、Servlet映射匹配問題
因爲客戶端是經過URL的地址找到對應web服務器中的資源,因此Servlet程序若是想要被外界訪問,那麼必須把servlet程序映射到一個URL地址上,這個工做在web.xml文件中<servlet>和<servlet-mapping>元素組成。
<servlet>元素用於註冊Servlet,它包含有兩個主要的子元素:<servlet-name>和<servlet-class>,分別用於設置Servlet的註冊名稱和Servlet的完整類名。
一個<servlet-mapping>元素用於映射一個已註冊的Servlet的一個對外訪問路徑,它包含有兩個子元素:<servlet-name>和<url-pattern>,分別用於指定Servlet的註冊名稱和Servlet的對外訪問路徑。
須要注意的是:
1.一個servlet能夠對應多個servlet-mapping,從而一個servlet能夠有多個路徑訪問。
2.url-pattern中的路徑也可使用*通配符,可是隻有兩種固定的格式:一種格式「*.擴展名」,另外一種是正斜槓/ 開頭並以/* 結尾。
一般匹配的優先級是哪一個精確找哪一個,*.後綴的格式永遠匹配級最低。
5、線程安全問題
Servlet引擎採用多線程模式運行,它爲併發的每一個訪問請求都使用一個獨立的線程來進行響應。
可是因爲默認狀況下Servlet在內存中只有實例存在,所以當多個瀏覽器併發訪問Servlet的時候,就會有可能產生線程的安全問題。
Servlet線程不安全,至始至終,之維護一個實例對象,當同一個資源被多個線程同時訪問操做的時候,就可能會互相干擾。
解決的方法是:
一、SingleThreadModel接口(標記接口,單線程模型接口):不能真的防止線程安全問題(已過期)
Servlet實現了SingleThreadModel接口,那麼Servlet引擎將以單線程模式來調用Servlet的service方法。對於實現了SingleThreadModel接口的Servlet,Servlet引擎仍然支持對該Servlet的多線程併發訪問,其採用的方式是產生多個Servlet實例對象,併發的每一個線程分別調用獨立的一個Servlet實例對象。
注:此接口在API 2.4中就已通過時,雖然解決了線程安全問題,可是消耗了大量性能(不一樣的客戶端同時訪問會建立不一樣的Servlet實例),因此此方法建議不用,實際開發中也不用。
二、使用同步代碼塊,但效率低。在Servlet中儘可能少用類變量(成員變量),若是必定要用類變量則用鎖來防止線程安全問題,可是要注意鎖住內容應該是形成線程安全問題的核心代碼,儘可能的少鎖主內容,減小等待時間提升servlet的響應速度。
幾個小題目:
下面有關servlet service描述錯誤的是?
一、不論是post仍是get方法提交過來的鏈接,都會在service中處理
答:每次請求都會調用service方法,最終都會在service中處理,正確;
二、doGet/doPost 則是在 javax.servlet.GenericServlet 中實現的
答:GenericServlet只是繼承了Servlet的接口,實現了它其中的5個方法,其中須要用戶重寫的是service方法,而doGet/doPost是由於以後出現了HttpServlet纔有的,是針對http請求才有了這個類,纔有了doPost和doGet,因此是錯誤的。
三、service()是在javax.servlet.Servlet接口中定義的
答:Servlet接口一共定義了5個方法,其中就有service(),正確;
四、service判斷請求類型,決定是調用doGet仍是doPost方法
答:正確,緣由見上面。
今天就複習到此,下一節講解Servlet中比較經常使用到的ServletConfig 和ServletContext 對象。