Java Servlet理論篇

轉載自 http://blog.csdn.net/justloveyou_/

摘要:

  Web 技術成爲當今主流的互聯網 Web 應用技術之一,而 Servlet 是 Java Web 技術的核心基礎。本文首先從請求/響應架構應用的大背景談起 Servlet 的由來,明確 Servlet 的產生動機,並揭示了 Servlet 的本質以及其在標準MVC模式中所扮演的角色。緊接着,給出了 Servlet族的繼承結構,並對族內的接口和抽象類作了進一步的介紹,並給出開發一個Servlet程序的常用做法。在此基礎上,我們圖文並茂地介紹了 Servlet 的生命週期與執行流程,清晰展現 Servlet 的原理。特別地,由於Servlet容器默認採用單實例多線程的方式來處理請求,我們進一步介紹了Servlet 與併發的聯繫,並就Servlet容器如何同時來處理多個請求和如何開發線程安全的Servlet兩個問題進行討論和總結。最後,結合Java Web 應用的結構演變歷程,給出了MVC架構的基本組成要素、內在聯繫和作用分工,並給出了 Servlet 在其中所扮演的角色。

  本篇主要介紹 Servlet 理論方面的知識,更多關注於以下幾個問題:

  • 爲什麼會有 Servlet;
  • Servlet 是什麼;
  • Servlet 如何實現預期效果;
  • Servlet 的作用原理;
  • Servlet 與 併發;
  • Servlet 與 Java Web 應用的結構演變歷程;
  • Servlet 與 MVC 的聯繫。

  更多關於Servlet使用、實踐方面的介紹以及Servlet新特性(Servlet 異步處理、Servlet 非阻塞IO 以及 Servlet 文件上傳等)的總結見本文的姊妹篇《Servlet 綜述(實踐篇)》。 
   
  更多關於Java併發相關知識請移步我的專欄《Java併發編程學習筆記》。該專欄全面記錄了Java併發編程的相關知識,並結合操作系統、Java內存模型和相關源碼對併發編程的原理、技術、設計、底層實現進行深入分析和總結,並持續跟進併發相關技術。


一. Servlet 概述

1、從請求/響應架構談 Servlet 的由來

  我們之前在博文《Java Web基礎 — Jsp 綜述(下)》中對 請求/響應架構 有過比較系統的敘述,現在我們作進一步敘述。

  我們日常所接觸到的應用有很大一部分都是基於 請求/響應架構 的,如下圖所示。在這種架構中,一般由兩個角色組成,即:Server 和 User Agent。特別地,根據 User Agent 的不同,我們可以將應用分爲 B/S模式(User Agent 爲瀏覽器時) 和 C/S模式。但無論哪種模式,Server 與 User Agent 的交互使用的都是同一個請求和應答的標準,即 HTTP 協議

  一般地,以瀏覽器爲例,User Agent 的作用就是根據用戶的請求URL生成相應的 HTTP請求報文發送給服務器,並對服務器的響應進行解析(或渲染),使用戶看到一個豐富多彩的頁面。但是,如果我們需要在網頁上完成一些業務邏輯(比如,登陸驗證),或者需要從服務器的數據庫中取一些數據作爲網頁的顯示內容,那麼除了負責顯示的HTML標記之外,必須還要有完成這些業務功能的代碼存在,這種網頁我們稱之爲 動態網頁

  對於靜態網頁而言,服務器上存在的是一個個純HTML文件。當客戶端瀏覽器發出HTTP請求時,服務器可以根據請求的URL找到對應的HTML文件,並將HTML代碼返回給客戶端瀏覽器。但是對於動態網頁,服務器上除了找到需要顯示的HTML標記外,還必須執行所需要的業務邏輯,然後將業務邏輯運算後的結果和需要顯示的HTML標記一起生成新的HTML代碼。最後,將新的帶有業務邏輯運算結果的HTML代碼返回給客戶端。爲了實現動態網頁的目標,Servlet技術(利用輸出流動態生成 HTML 頁面)應運而生,它能夠以一種可移植的方法來提供動態的、面向用戶的內容。

            請求響應.png-211.4kB


2、Servlet 的本質與角色

  Web 技術成爲當今主流的互聯網 Web 應用技術之一,而 Servlet 是 Java Web 技術的核心基礎。包括我們在前面的博文中談到的JSP,也只是爲了彌補使用 Servlet 作爲表現層的不足而提出的。JSP規範通過實現普通靜態HTML和動態部分的混合編碼,使得邏輯內容與外觀相分離,大大簡化了表示層的實現。但是,JSP並沒有增加任何本質上不能用Servlet實現的功能,只是在JSP中編寫靜態HTML更加方便。事實上,JSP的本質仍然是Servlet,並且站在表現層的角度上來看,JSP 是 Servlet 的一種就簡化。

  Servlet 是 J2EE 標準的一部分,是一種運行在Web服務器端的小型Java程序,更具體的說,Servlet 是按照Servlet規範編寫的一個Java類,用於交互式地瀏覽和修改數據,生成動態Web內容。要注意的是,由於 Servlet 是服務器端小程序,所以 Servlet 必須部署在 Servlet 容器中才能使用,例如 Tomcat,Jetty 等。

  在標準的MVC模式中,Servlet 僅作爲控制器使用,而控制器角色的作用是:負責接收客戶端的請求,它既不直接對客戶端輸出響應,也不處理用戶請求,只是調用業務邏輯組件(JavaBean)來處理用戶請求。一旦業務邏輯組件處理結束後,控制器會根據處理結果,調用不同的表現層頁面向瀏覽器呈現處理結果。


二. Servlet API

  關於 Servlet 的接口主要在以下兩個包中,Servlet 繼承結構如下圖所示:

  • javax.servlet.* :存放與HTTP 協議無關的一般性Servlet 類;

  • javax.servlet.http.* :除了繼承 javax.servlet.* 之外,還增加了與HTTP協議有關的功能。

      特別需要說明的是,所有的 Servlet 都必須實現 javax.servlet.Servlet 接口。若我們開發的 Servlet程序與HTTP協議無關,那麼可以直接繼承 javax.servlet.GenericServlet抽象類 ;否則,若我們開發的Servlet程序和HTTP協議有關,那麼可以直接繼承 javax.servlet.http.HttpServlet 抽象類

                Servlet-API.png-14.4kB              


  下面我們分別看一下 Servlet接口、ServletConfig接口、GenericServlet抽象類 和 HttpServlet抽象類給我們提供的接口:

(1) Interface Servlet

             InterfaceServlet.png-22.9kB

  Servlet 接口定義了所有Servlet都必須實現的方法。其中,destroy()方法、init()方法 和 service()方法 由Servlet容器來調用。特別地,service()方法用於處理並響應請求。


(2) Interface ServletConfig

  A servlet configuration object used by a servlet Container to pass information to a servlet during initialization.

             InterfaceServletConfig.png-14kB


(3) Abstract Class GenericServlet

  GenericServlet implements the Servlet and ServletConfig interfaces. GenericServlet may be directly extended by a servlet, although it’s more common to extend a protocol-specific subclass such as HttpServlet.

  GenericServlet makes writing servlets easier. It provides simple versions of the lifecycle methods init and destroy and of the methods in the ServletConfig interface. GenericServlet also implements the log method, declared in the ServletContext interface.

  To write a generic servlet, you need only override the abstract service method.

             ServletGeneric.png-38.2kB


(4) Abstract Class HttpServlet

             HttpServlet.png-44.5kB

  通過繼承 HttpServlet 可以很方便的幫助我們創建一個 HTTP Servlet。我們可以看到,HttpServlet 爲各種Http請求都提供了相應的處理方式。但是,我們從Servlet接口中我們瞭解到,service()方法用於生成對客戶端的響應,那麼 service()方法與圖中所示的七種Http請求處理方法有什麼聯繫呢?下面請看 HttpServlet 對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) {
            doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                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 {
        // Error
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

  我們知道,service()方法用於生成對客戶端的響應。但對於 HTTPServlet 而言,service()方法會按照具體的請求類型將請求進一步分發到對應的處理方法進行處理。由於在大部分時候,Servlet 對各種類型的請求的處理方式都是一樣的,因此我們只需重寫service()方法即可響應客戶端的所有請求。或者,另一種處理方式是,由於客戶端的請求通常只有 GET 和 POST 兩種,因此我們只需重寫doGet() 和 doPost()兩個方法即可。

  實際上,普通Servlet類裏的service()方法的作用,完全等同於JSP轉譯所生成Servlet類裏的_jspService()方法。


三. Servlet的生命週期與執行流程

1、Servlet的生命週期

  當 Servlet 在容器中運行時,其實例的創建及銷燬等都不是由程序員所決定的,而是由Web容器進行控制的。一個Servlet對象的創建有兩個時機:用戶請求之時或應用啓動之時。

 (1) 客戶端第一次請求某個Servlet時,容器創建該Servlet實例,這也是大部分Servlet創建實例的時機; 
 (2) Web應用啓動時立即創建Servlet實例,即 load-on-startup Servlet。 

  但每個Servlet都遵循一個生命週期,即:Servlet 實例化–>Servlet 初始化—>服務—>銷燬。具體流程如下圖所示:

  • 創建Servlet實例;

  • Web容器調用Servlet的init()方法,對Servlet進行初始化。特別地,在Servlet的生命週期中,僅執行一次init()方法;

  • Servlet 被初始化後,將一直存在於Web容器中,用於響應客戶端請求。默認的請求處理、響應方式是調用與HTTP請求的方法相應的do方法;

  • Web容器決定銷燬Servlet時,先調用Servlet的 destroy() 的方法回收資源,通常在關閉Web應用之時銷燬 Servlet。和 init() 方法類似,destroy()方法在Servlet的生命週期中也僅執行一次。當Servlet對象退出生命週期時,負責釋放佔用的資源。一個Servlet在運行service()方法時可能會產生其他的線程,因此需要確保在調用destroy()方法時,這些線程已經終止或完成。

              Servlet生命週期.png-235.9kB

      Ps:本圖來源於CSDN博友何靜媛JAVA學習篇–Servlet詳解一文。


2、Servlet的執行流程

 Servlet的執行流程如下圖所示:

  (1) User Agent 向 Servlet容器(Tomcat)發出Http請求; 
  (2) Servle容器接收 User Agent 發來的請求; 
  (3) Servle容器根據web.xml文件中Servlet相關配置信息,將請求轉發到相應的Servlet; 
  (4) Servlet容器創建一個 HttpServlet對象,用於處理請求; 
  (5) Servlet容器創建一個 HttpServletRequest對象,將請求信息封裝到這個對象中; 
  (6) Servlet容器創建一個HttpServletResponse對象; 
  (7) Servlet容器調用HttpServlet對象的service方法,並把HttpServltRequest對象與HttpServltResponse對象作爲參數傳給 HttpServlet 對象; 
  (8) HttpServlet調用HttpServletRequest對象的有關方法,獲取Http請求信息; 
  (9) HttpServlet調用JavaBean對象(業務邏輯組件)處理Http請求; 
  (10) HttpServlet調用HttpServletResponse對象的有關方法,生成響應數據; 
  (11) Servlet容器將HttpServlet的響應結果傳給 User Agent。

             servlet執行流程.png-14.8kB


3、load-on-servlet

  load-on-servlet 指的是應用啓動時就創建的Servlet,這些Servlet通常是用於後臺服務的Servlet或者需要攔截很多請求的Servlet。也就是說,這些Servlet常常作爲應用的基礎Servlet使用,提供重要的後臺服務。例如:

@WebServlet(loadOnStartup=1)
public class TimerServlet extends HttpServlet
{
    public void init(ServletConfig config)throws ServletException
    {
        super.init(config);
        Timer t = new Timer(1000,new ActionListener()       // 匿名內部類
        {
            public void actionPerformed(ActionEvent e)
            {
                System.out.println(new Date());
            }
        });
        t.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

  我們看到,這個 load-on-servlet 沒有提供 service()方法,這表明它不能響應用戶請求,所以無需爲它配置URL映射。由於它不能接收用戶請求,所以只能在應用啓動時實例化。

  特別需要注意的是,loadOnStartup屬性用於標記容器是否在啓動的時候加載這個servlet。當值爲0或者大於0時,表示容器在應用啓動時就加載這個servlet;當是一個負數時或者沒有指定時,則指示容器在該servlet被選擇時才加載。其中,正數值越小,啓動該servlet的優先級越高。


四. Servlet的配置

  爲了讓Servlet能夠響應用戶請求,必須將Servlet配置到Web應用中。從 J2EE 6(即 Servlet 3.0)開始,配置Servlet有兩種方式,即 @WebServlet註解配置 和 傳統的 web.xml 配置,這部分內容在我的下一篇博客《Servlet 綜述(實踐篇)》有詳細介紹,此不贅述。


五. Servlet 與 併發

1、Servlet容器如何同時來處理多個請求

  Servlet 採用多線程來處理多個請求同時訪問。更具體地,Servlet 依賴於一個線程池來服務請求,所謂線程池實際上是一系列的工作者線程集合,該集合包含的是一組等待執行任務的線程。此外,Servlet 使用一個調度線程來管理這些工作者線程。

  當Servlet容器收到一個Servlet請求時,調度線程會從線程池中選出一個工作者線程,並將請求傳遞給該工作者線程,然後由該線程來執行Servlet的service()方法。當這個線程正在執行的時候,如果容器收到另外一個請求,調度線程將同樣從線程池中選出另一個工作者線程來服務新的請求,特別需要注意的是,容器並不關心這個請求是否訪問的是同一個Servlet。當容器同時收到對同一個Servlet的多個請求時,那麼這個Servlet的service()方法將在多線程中併發執行。

  Servlet容器默認採用單實例多線程的方式來處理請求,這樣減少產生Servlet實例的開銷,提升了對請求的響應時間,對於Tomcat容器,我們可以在其server.xml中通過元素設置線程池中的線程數目。


2、如何開發線程安全的Servlet

  Servlet容器採用多線程來處理請求,提高性能的同時也造成了線程安全問題。要開發線程安全的 Servlet應該從這幾個方面進行:

(1). 變量的線程安全: 多線程並不共享局部變量,所以我們要儘可能的在Servlet中使用局部變量;

(2). 代碼塊的線程安全: 可以使用Synchronized、Lock 和 原子操作(java.util.concurrent.atomic)來保證多線程對共享變量的協同訪問;但是要注意的是,要儘可能得縮小同步代碼的範圍,儘量不要在service方法和響應方法上直接使用同步,這會嚴重影響性能;

(3). 屬性的線程安全: ServletContext,HttpSession,ServletRequest對象中屬性是線程安全的;

(4). 使用線程安全容器: 使用 java.util.concurrent 包下的線程安全容器代替 ArrayList、HashMap 等非線程安全容器;


  更多關於Java併發相關知識請移步我的專欄《 Java併發編程學習筆記》。該專欄全面記錄了Java併發編程的相關知識,並結合操作系統、Java內存模型和相關源碼對併發編程的原理、技術、設計、底層實現進行深入分析和總結,並持續跟進併發相關技術。


六. Servlet 與 MVC

  Java Web 應用的結構經歷了 Model1 和 Model2 兩個時代。在 Model1 模式下,整個 Web 應用幾乎全部用JSP頁面組成,只用少量的JavaBean來處理數據庫連接、訪問等操作。從工程化角度來看,JSP 不但充當了表現層角色,還充當了控制器角色,將控制邏輯和表現邏輯混雜在一起,導致代碼重用率極低,使得應用極難擴展和維護。

  Model2 已經是基於MVC架構的設計模式。在 Model2 中,Servlet 作爲前端控制器,負責接收客戶端發送的請求,在 Servlet 中只包含控制邏輯和簡單的前端處理;然後,Servlet 調用後端的JavaBean(業務邏輯組件)來處理業務邏輯;最後,根據處理結果轉發到相應的JSP頁面處理顯示邏輯。在 Model2 模式下,模型(Model)由 JavaBean 充當,視圖(View)由JSP頁面充當,而控制器則由 Servlet 充當。Model2 的流程示意圖如下:

              Model2.png-32.7kB

  更具體地,在 Model2(標準MVC)中,角色分工如下:

  • Model:由 JavaBean 充當,所有的業務邏輯、數據庫訪問都在Model中實現;

  • View:由 JSP 充當,負責收集用戶請求參數並將應用處理結果、狀態數據呈現給用戶;

  • Controller:由 Servlet 充當,作用類似於調度員,即所有用戶請求都發送給 Servlet,Servlet 調用 Model 來處理用戶請求,並根據處理結果調用 JSP 來呈現結果;或者Servlet直接調用JSP將應用處理結果展現給用戶。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章