1. 程式人生 > >JavaWeb——Servlet(全網最詳細教程包括Servlet原始碼分析)

JavaWeb——Servlet(全網最詳細教程包括Servlet原始碼分析)

JavaWeb——Servlet

什麼是Servlet

    Servlet(Server Applet),全稱Java Servlet,未有中文譯文。是用Java編寫的伺服器程式。其主要功能在於互動式地瀏覽和修改資料,生成動態Web內容。狹義的Servlet是指Java語言實現的一個介面,廣義的Servlet是指任何實現了這個Servlet介面的,一般情況下,人們將Servlet理解為後者。

    Servlet運行於支援Java的應用伺服器中。從實現上講,Servlet可以響應任何型別的請求,但絕大多數情況下Servlet只用來擴充套件基於HTTP協議Web伺服器

Servlet的工作模式

    
  • 客戶端傳送請求至伺服器
  • 伺服器啟動並呼叫Servlet,Servlet根據客戶端請求生成響應內容並將其傳給伺服器
  • 伺服器將響應返回客戶端

Servlet API 概覽

    Servlet API 包含以下4個Java包:

1.javax.servlet   其中包含定義servlet和servlet容器之間契約的類和介面。

2.javax.servlet.http   其中包含定義HTTP Servlet 和Servlet容器之間的關係。

3.javax.servlet.annotation   其中包含標註servlet,Filter,Listener的標註。它還為被標註元件定義元資料。

4.javax.servlet.descriptor,其中包含提供程式化登入Web應用程式的配置資訊的型別。

Servlet 的主要型別



Servlet 的使用方法

Servlet技術的核心是Servlet,它是所有Servlet類必須直接或者間接實現的一個介面。在編寫實現Servlet的Servlet類時,直接實現它。在擴充套件實現這個這個介面的類時,間接實現它。

Servlet 的工作原理


    Servlet介面定義了Servletservlet容器之間的契約。這個契約是:Servlet容器將Servlet類載入記憶體,併產生Servlet例項和呼叫它具體的方法。但是要注意的是,在一個應用程式中,每種Servlet型別只能有一個例項

    使用者請求致使Servlet容器呼叫Servlet的Service()方法,並傳入一個ServletRequest物件和一個ServletResponse物件。ServletRequest物件和ServletResponse物件都是由Servlet容器(例如TomCat)封裝好的,並不需要程式設計師去實現,程式設計師可以直接使用這兩個物件。

    ServletRequest中封裝了當前的Http請求,因此,開發人員不必解析和操作原始的Http資料。ServletResponse表示當前使用者的Http響應,程式設計師只需直接操作ServletResponse物件就能把響應輕鬆的發回給使用者。

    對於每一個應用程式,Servlet容器還會建立一個ServletContext物件。這個物件中封裝了上下文(應用程式)的環境詳情。每個應用程式只有一個ServletContext。每個Servlet物件也都有一個封裝Servlet配置的ServletConfig物件。


Servlet 介面中定義的方法

    讓我們首先來看一看Servlet介面中定義了哪些方法吧。

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

Servlet 的生命週期

其中,init( ),service( ),destroy( )是Servlet生命週期的方法。代表了Servlet從“出生”到“工作”再到“死亡 ”的過程。Servlet容器(例如TomCat)會根據下面的規則來呼叫這三個方法:

1.init( ),當Servlet第一次被請求時,Servlet容器就會開始呼叫這個方法來初始化一個Servlet物件出來,但是這個方法在後續請求中不會在被Servlet容器呼叫,就像人只能“出生”一次一樣。我們可以利用init( )方法來執行相應的初始化工作。呼叫這個方法時,Servlet容器會傳入一個ServletConfig物件進來從而對Servlet物件進行初始化

2.service( )方法,每當請求Servlet時,Servlet容器就會呼叫這個方法。就像人一樣,需要不停的接受老闆的指令並且“工作”。第一次請求時,Servlet容器會先呼叫init( )方法初始化一個Servlet物件出來,然後會呼叫它的service( )方法進行工作,但在後續的請求中,Servlet容器只會呼叫service方法了。

3.destory,當要銷燬Servlet時,Servlet容器就會呼叫這個方法,就如人一樣,到時期了就得死亡。在解除安裝應用程式或者關閉Servlet容器時,就會發生這種情況,一般在這個方法中會寫一些清除程式碼。

    首先,我們來編寫一個簡單的Servlet來驗證一下它的生命週期:

public class MyFirstServlrt implements Servlet {

    @Override
public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("Servlet正在初始化");
    }

    @Override
public ServletConfig getServletConfig() {
        return null;
    }

    @Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        //專門向客服端提供響應的方法
System.out.println("Servlet正在提供服務");

    }

    @Override
public String getServletInfo() {
        return null;
    }

    @Override
public void destroy() {
        System.out.println("Servlet正在銷燬");
    }
}      

然後在xml中配置正確的對映關係,在瀏覽器中訪問Servlet,第一次訪問時,控制檯輸出瞭如下資訊:

然後,我們在瀏覽器中重新整理3遍

控制檯輸出的資訊變成了下面這樣:

接下來,我們關閉Servlet容器

控制檯輸出了Servlet的銷燬資訊,這就是一個Servlet的完整生命週期。

Servlet 的其它兩個方法

    getServletInfo( ),這個方法會返回Servlet的一段描述,可以返回一段字串。

    getServletConfig( ),這個方法會返回由Servlet容器傳給init( )方法的ServletConfig物件。

ServletRequset介面

    Servlet容器對於接受到的每一個Http請求,都會建立一個ServletRequest物件,並把這個物件傳遞給Servlet的Sevice( )方法。其中,ServletRequest物件內封裝了關於這個請求的許多詳細資訊。

讓我們來看一看ServletRequest介面的部分內容:

public interface ServletRequest {
  

    int getContentLength();//返回請求主體的位元組數

    String getContentType();//返回主體的MIME型別

    String getParameter(String var1);//返回請求引數的值

}

其中,getParameter是在ServletRequest中最常用的方法,可用於獲取查詢字串的值

ServletResponse介面

    javax.servlet.ServletResponse介面表示一個Servlet響應,在呼叫Servlet的Service( )方法前,Servlet容器會先建立一個ServletResponse物件,並把它作為第二個引數傳給Service( )方法。ServletResponse隱藏了向瀏覽器傳送響應的複雜過程。

    讓我們也來看看ServletResponse內部定義了哪些方法:

public interface ServletResponse {
    String getCharacterEncoding();

    String getContentType();

    ServletOutputStream getOutputStream() throws IOException;

    PrintWriter getWriter() throws IOException;

    void setCharacterEncoding(String var1);

    void setContentLength(int var1);

    void setContentType(String var1);

    void setBufferSize(int var1);

    int getBufferSize();

    void flushBuffer() throws IOException;

    void resetBuffer();

    boolean isCommitted();

    void reset();

    void setLocale(Locale var1);

    Locale getLocale();
}

   其中的getWriter方法,它返回了一個可以向客戶端傳送文字的的Java.io.PrintWriter物件。預設情況下,PrintWriter物件使用ISO-8859-1編碼(該編碼在輸入中文時會發生亂碼)。

    在向客戶端傳送響應時,大多數都是使用該物件向客戶端傳送HTML。

還有一個方法也可以用來向瀏覽器傳送資料,它就是getOutputStream,從名字就可以看出這是一個二進位制流物件,因此這個方法是用來發送二進位制資料的。

在傳送任何HTML之前,應該先呼叫setContentType()方法,設定響應的內容型別,並將“text/html”作為一個引數傳入,這是在告訴瀏覽器響應的內容型別為HTML,需要以HTML的方法解釋響應內容而不是普通的文字,或者也可以加上“charset=UTF-8”改變響應的編碼方式以防止發生中文亂碼現象。

ServletConfig介面

    當Servlet容器初始化Servlet時,Servlet容器會給Servlet的init( )方式傳入一個ServletConfig物件。

其中幾個方法如下:


ServletContext物件

    ServletContext物件表示Servlet應用程式。每個Web應用程式都只有一個ServletContext對象。在將一個應用程式同時部署到多個容器的分散式環境中,每臺Java虛擬機器上的Web應用都會有一個ServletContext物件。

通過在ServletConfig中呼叫getServletContext方法,也可以獲得ServletContext物件。

那麼為什麼要存在一個ServletContext物件呢?存在肯定是有它的道理,因為有了ServletContext物件,就可以共享從應用程式中的所有資料處訪問到的資訊,並且可以動態註冊Web物件。前者將物件儲存在ServletContext中的一個內部Map中。儲存在ServletContext中的物件被稱作屬性。

ServletContext中的下列方法負責處理屬性:

Object getAttribute(String var1);

Enumeration<String> getAttributeNames();

void setAttribute(String var1, Object var2);

void removeAttribute(String var1);


GenericServlet抽象類 

    前面我們編寫Servlet一直是通過實現Servlet介面來編寫的,但是,使用這種方法,則必須要實現Servlet介面中定義的所有的方法,即使有一些方法中沒有任何東西也要去實現,並且還需要自己手動的維護ServletConfig這個物件的引用。因此,這樣去實現Servlet是比較麻煩的。
void init(ServletConfig var1) throws ServletException;
    幸好,GenericServlet抽象類的出現很好的解決了這個問題。本著儘可能使程式碼簡潔的原則,GenericServlet實現了Servlet和ServletConfig介面,下面是GenericServlet抽象類的具體程式碼:
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
    private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");
    private transient ServletConfig config;

    public GenericServlet() {
    }

    public void destroy() {
    }

    public String getInitParameter(String name) {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameter(name);
        }
    }

    public Enumeration<String> getInitParameterNames() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameterNames();
        }
    }

    public ServletConfig getServletConfig() {
        return this.config;
    }

    public ServletContext getServletContext() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletContext();
        }
    }

    public String getServletInfo() {
        return "";
    }

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

    public void init() throws ServletException {
    }

    public void log(String msg) {
        this.getServletContext().log(this.getServletName() + ": " + msg);
    }

    public void log(String message, Throwable t) {
        this.getServletContext().log(this.getServletName() + ": " + message, t);
    }

    public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    public String getServletName() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletName();
        }
    }
}
    其中,GenericServlet抽象類相比於直接實現Servlet介面,有以下幾個好處:

1.為Servlet介面中的所有方法提供了預設的實現,則程式設計師需要什麼就直接改什麼,不再需要把所有的方法都自己實現了。

2.提供方法,包圍ServletConfig物件中的方法。

3.將init( )方法中的ServletConfig引數賦給了一個內部的ServletConfig引用從而來儲存ServletConfig物件,不需要程式設計師自己去維護ServletConfig了。

public void init(ServletConfig config) throws ServletException {
    this.config = config;
    this.init();
}

但是,我們發現在GenericServlet抽象類中還存在著另一個沒有任何引數的Init()方法:

public void init() throws ServletException {
}

設計者的初衷到底是為了什麼呢?在第一個帶引數的init()方法中就已經把ServletConfig物件傳入並且通過引用儲存好了,完成了Servlet的初始化過程,那麼為什麼後面還要加上一個不帶任何引數的init()方法呢?這不是多此一舉嗎?

    當然不是多此一舉了,存在必然有存在它的道理。我們知道,抽象類是無法直接產生例項的,需要另一個類去繼承這個抽象類,那麼就會發生方法覆蓋的問題,如果在類中覆蓋了GenericServlet抽象類的init()方法,那麼程式設計師就必須手動的去維護ServletConfig物件了,還得呼叫super.init(servletConfig)方法去呼叫父類GenericServlet的初始化方法來儲存ServletConfig物件,這樣會給程式設計師帶來很大的麻煩。GenericServlet提供的第二個不帶引數的init( )方法,就是為了解決上述問題的。

    這個不帶引數的init()方法,是在ServletConfig物件被賦給ServletConfig引用後,由第一個帶引數的init(ServletConfig servletconfig)方法呼叫的,那麼這意味著,當程式設計師如果需要覆蓋這個GenericServlet的初始化方法,則只需要覆蓋那個不帶引數的init( )方法就好了,此時,servletConfig物件仍然有GenericServlet儲存著。

    說了這麼多,通過擴充套件GenericServlet抽象類,就不需要覆蓋沒有計劃改變的方法。因此,程式碼將會變得更加的簡潔,程式設計師的工作也會減少很多。

    然而,雖然GenricServlet是對Servlet一個很好的加強,但是也不經常用,因為他不像HttpServlet那麼高階。HttpServlet才是主角,在現實的應用程式中被廣泛使用。那麼我們接下來就看看傳說中的HttpServlet到底厲害在哪裡吧。

javax.servlet.http包內容

    之所以所HttpServlet要比GenericServlet強大,其實也是有道理的。HttpServlet是由GenericServlet抽象類擴充套件而來的,HttpServlet抽象類的宣告如下所示:

public abstract class HttpServlet extends GenericServlet implements Serializable 

HttpServlet之所以運用廣泛的另一個原因是現在大部分的應用程式都要與HTTP結合起來使用。這意味著我們可以利用HTTP的特性完成更多更強大的任務。Javax。servlet.http包是Servlet API中的第二個包,其中包含了用於編寫Servlet應用程式的類和介面。Javax.servlet.http中的許多型別都覆蓋了Javax.servlet中的型別。


HttpServlet抽象類

    HttpServlet抽象類是繼承於GenericServlet抽象類而來的。使用HttpServlet抽象類時,還需要藉助分別代表Servlet請求和Servlet響應的HttpServletRequest和HttpServletResponse物件。

HttpServletRequest介面擴充套件於javax.servlet.ServletRequest介面,HttpServletResponse介面擴充套件於javax.servlet.servletResponse介面。

public interface HttpServletRequest extends ServletRequest
public interface HttpServletResponse extends ServletResponse

HttpServlet抽象類覆蓋了GenericServlet抽象類中的Service( )方法,並且添加了一個自己獨有的Service(HttpServletRequest request,HttpServletResponse方法。

讓我們來具體的看一看HttpServlet抽象類是如何實現自己的service方法吧:

    首先來看GenericServlet抽象類中是如何定義service方法的:

public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

我們看到是一個抽象方法,也就是HttpServlet要自己去實現這個service方法,我們在看看HttpServlet是怎麼覆蓋這個service方法的:

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    HttpServletRequest request;
    HttpServletResponse response;
    try {
        request = (HttpServletRequest)req;
        response = (HttpServletResponse)res;
    } catch (ClassCastException var6) {
        throw new ServletException("non-HTTP request or response");
    }

    this.service(request, response);
}

    我們發現,HttpServlet中的service方法把接收到的ServletRequsest型別的物件轉換成了HttpServletRequest型別的物件,把ServletResponse型別的物件轉換成了HttpServletResponse型別的物件。之所以能夠這樣強制的轉換,是因為在呼叫Servlet的Service方法時,Servlet容器總會傳入一個HttpServletRequest物件和HttpServletResponse物件,預備使用HTTP。因此,轉換型別當然不會出錯了。

    轉換之後,service方法把兩個轉換後的物件傳入了另一個service方法,那麼我們再來看看這個方法是如何實現的:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    long lastModified;
    if (method.equals("GET")) {
        lastModified = this.getLastModified(req);
        if (lastModified == -1L) {
            this.doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader("If-Modified-Since");
            if (ifModifiedSince < lastModified) {
                this.maybeSetLastModified(resp, lastModified);
                this.doGet(req, resp);
            } else {
                resp.setStatus(304);
            }
        }
    } else if (method.equals("HEAD")) {
        lastModified = this.getLastModified(req);
        this.maybeSetLastModified(resp, lastModified);
        this.doHead(req, resp);
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
        this.doPut(req, resp);
    } else if (method.equals("DELETE")) {
        this.doDelete(req, resp);
    } else if (method.equals("OPTIONS")) {
        this.doOptions(req, resp);
    } else if (method.equals("TRACE")) {
        this.doTrace(req, resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[]{method};
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(501, errMsg);
    }

}

    我們發現,這個service方法的引數是HttpServletRequest物件和HttpServletResponse物件,剛好接收了上一個service方法傳過來的兩個物件。

    接下來我們再看看service方法是如何工作的,我們會發現在service方法中還是沒有任何的服務邏輯,但是卻在解析HttpServletRequest中的方法引數,並呼叫以下方法之一:doGet,doPost,doHead,doPut,doTrace,doOptions和doDelete。這7種方法中,每一種方法都表示一個Http方法。doGet和doPost是最常用的。所以,如果我們需要實現具體的服務邏輯,不再需要覆蓋service方法了,只需要覆蓋doGet或者doPost就好了。

    總之,HttpServlet有兩個特性是GenericServlet所不具備的:

    1.不用覆蓋service方法,而是覆蓋doGet或者doPost方法。在少數情況,還會覆蓋其他的5個方法。

    2.使用的是HttpServletRequest和HttpServletResponse物件。

HttpServletRequest介面

    

    HttpServletRequest表示Http環境中的Servlet請求。它擴充套件於javax.servlet.ServletRequest介面,並添加了幾個方法。

String getContextPath();//返回請求上下文的請求URI部分
Cookie[] getCookies();//返回一個cookie物件陣列
String getHeader(String var1);//返回指定HTTP標題的值
String getMethod();//返回生成這個請求HTTP的方法名稱
String getQueryString();//返回請求URL中的查詢字串
HttpSession getSession();//返回與這個請求相關的會話物件

HttpServletRequest內封裝的請求

    因為Request代表請求,所以我們可以通過該物件分別獲得HTTP請求的請求行,請求頭和請求體。

    關於HTTP具體的詳細解釋,可以參考我的另一篇博文:JavaWeb——HTTP。


通過request獲得請求行

假設查詢字串為:username=zhangsan&password=123

獲得客戶端的請求方式:String getMethod()

獲得請求的資源:

String getRequestURI()

StringBuffer getRequestURL()

String getContextPath() ---web應用的名稱

String getQueryString() ---- get提交url地址後的引數字串

通過request獲得請求頭

long getDateHeader(String name)

String getHeader(String name)

Enumeration getHeaderNames()

Enumeration getHeaders(String name)

int getIntHeader(String name)

referer頭的作用:執行該此訪問的的來源,做防盜鏈

通過request獲得請求體

請求體中的內容是通過post提交的請求引數,格式是:

username=zhangsan&password=123&hobby=football&hobby=basketball

key ---------------------- value

username                               [zhangsan]

password                               [123]

hobby                                          [football,basketball]                                       

以上面引數為例,通過一下方法獲得請求引數:

String getParameter(String name)

String[] getParameterValues(String name)

Enumeration getParameterNames()

Map<String,String[]> getParameterMap()

注意:get請求方式的請求引數 上述的方法一樣可以獲得。

Request亂碼問題的解決方法

    在前面我們講過,在service中使用的編碼解碼方式預設為:ISO-8859-1編碼,但此編碼並不支援中文,因此會出現亂碼問題,所以我們需要手動修改編碼方式為UTF-8編碼,才能解決中文亂碼問題,下面是發生亂碼的具體細節:

解決post提交方式的亂碼:request.setCharacterEncoding("UTF-8");

 解決get提交的方式的亂碼:

parameter = newString(parameter.getbytes("iso8859-1"),"utf-8");


HttpServletResponse介面

    在Service API中,定義了一個HttpServletResponse介面,它繼承自ServletResponse介面,專門用來封裝HTTP響應訊息。    由於HTTP請求訊息分為狀態行,響應訊息頭,響應訊息體三部分,因此,在HttpServletResponse介面中定義了向客戶端傳送響應狀態碼,響應訊息頭,響應訊息體的方法。

HttpServletResponse內封裝的響應


通過Response設定響應


void addCookie(Cookie var1);//給這個響應新增一個cookie
void addHeader(String var1, String var2);//給這個請求新增一個響應頭
void sendRedirect(String var1) throws IOException;//傳送一條響應碼,講瀏覽器跳轉到指定的位置
void setStatus(int var1);//設定響應行的狀態碼

addHeader(String name, String value)

addIntHeader(String name, int value)

addDateHeader(String name, long date)

setHeader(String name, String value)

setDateHeader(String name, long date)

setIntHeader(String name, int value)

其中,add表示新增,而set表示設定

PrintWriter getWriter()

獲得字元流,通過字元流的write(String s)方法可以將字串設定到response   緩衝區中,隨後Tomcat會將response緩衝區中的內容組裝成Http響應返回給瀏覽器端。

ServletOutputStream getOutputStream()

獲得位元組流,通過該位元組流的write(byte[] bytes)可以向response緩衝區中寫入位元組,再由Tomcat伺服器將位元組內容組成Http響應返回給瀏覽器。

 注意:雖然response物件的getOutSream()和getWriter()方法都可以傳送響應訊息體,但是他們之間相互排斥,不可以同時使用,否則會發生異常。

Response的亂碼問題

原因:response緩衝區的預設編碼是iso8859-1,此碼錶中沒有中文。所以需要更改response的編碼方式:

    通過更改response的編碼方式為UTF-8,任然無法解決亂碼問題,因為傳送端服務端雖然改變了編碼方式為UTF-8,但是接收端瀏覽器端仍然使用GB2312編碼方式解碼,還是無法還原正常的中文,因此還需要告知瀏覽器端使用UTF-8編碼去解碼

    上面通過呼叫兩個方式分別改變服務端對於Response的編碼方式以及瀏覽器的解碼方式為同樣的UTF-8編碼來解決編碼方式不一樣發生亂碼的問題。

response.setContentType("text/html;charset=UTF-8")這個方法包含了上面的兩個方法的呼叫,因此在實際的開發中,只需要呼叫一個response.setContentType("text/html;charset=UTF-8")方法即可。

Response的工作流程

Servlet的工作流程

編寫第一個Servlet

    首先,我們來寫一個簡單的使用者名稱,密碼的登入介面的html檔案:

<form action="/form" method="get">

該html檔案在最後點選提交按鈕時,把表單所有資料通過Get方式傳送到/form虛擬路徑下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/form" method="get">
    <span>使用者名稱</span><input type="text" name="username"><br>
    <span>密碼</span><input type="password" name="password"><br>
    <inp