只有 doGet()、doPost()?



HTTP 定義了 GETPOSTPUTDELETEHEADOPTIONSTRACE 等請求方式,如果要使用 Servlet 處理對應的請求,則要在繼承 HttpServlet 之後,重新定義對應的 doXXX() 方法:

  • doGet() 處理 HTTP GET 請求
  • doPost() 處理 HTTP POST 請求
  • doPut() 處理 HTTP PUT 請求
  • doDelete() 處理 HTTP DELETE 請求
  • doHead() 處理 HTTP HEAD 請求
  • doOptions() 處理 HTTP OPTIONS 請求
  • doTrace() 處理 HTTP TRACE 請求

當請求來到容器之後,容器會剖析請求,產生 HttpServletRequestHttpServletResponse,分別代表該次求在 JVM 中的請求物件與回應物件,之後呼叫 Servletservice() 方法,將 HttpServletRequestHttpServletResponse 物件傳入,而 HttpServletservice() 方法實作是(以 Tomcat 9 為例):

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;
            try {
                ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
                // Invalid date header - proceed as if none was set
                ifModifiedSince = -1;
            }
            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)) {
        … 略
}

這也是為何你在繼承 HttpServlet 之後,必須實作與 HTTP 方法對應的 doXXX() 方法來處理請求(這是 〈Template Method 模式〉的實現)。如果客戶端發出了你沒有實作的請求又會如何?這必須看 HttpServletdoXXX() 方法如何實作。例如,HttpServletdoXXX() 等方法僅實作以下內容(以 doGet() 為例):

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    String protocol = req.getProtocol();
    String msg = lStrings.getString("http.method_get_not_supported");
    if (protocol.endsWith("1.1")) {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    } else {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    }
}

舉例來說,如果你在繼承 HttpServlet 之後,沒有重新定義 doGet() 方法,而客戶端對該 Servlet 發出了 GET 請求,則會收到錯誤訊息。

而在上面 HttpServletservice() 方法中,你可以看到,對於 GET 請求,你可以實作 getLastModified() 方法(預設傳回 -1,也就是預設不支援 if-modified-since 標頭),來決定是否呼叫 doGet() 方法,getLastModified() 方法傳回自1970年1月1日午夜至資源最後一次更新期間所經過的毫秒數。

在 JavaScript 尚未興起,前端工程還未當道的那個年代,Web 應用程式多只需處理 GETPOST 請求,這是因為過去發送客戶端請求,主要以 HTML 表單發送為主,而 HTML 的 <form>method 屬性上,只支援 getpost

不過,在 JavaScript 興起以及前端工程當道之後,因為可以透過 JavaScript 來發出各種請求方法,也就不再侷促於 GETPOST 了,想知道應該選用哪個 HTTP 方法,最好的方式,就是對 HTTP 的各個方法規範有進一步的認識:

  • 敏感資訊

    就 HTTP/1.1 對 GET 的規範來說,是從指定的 URI「取得」想要的資訊,指定的 URI 包括了請求查詢(Query)部份,例如 GET /?id=0093。瀏覽器會將指定的 URI 顯示在網址列上。

    因此,像是密碼、會話 ID 等敏感資訊,就不適合使用 GET 發送,除了可能被鄰近之人偷窺,或者是被現代瀏覽器過於方便的網址自動補齊記錄下來之外,另一個問題在於 HTTP 的 Referer 標頭,這是用來表示從哪兒連結到目前的網頁,如果你的網址列出現了敏感資訊,之後連接到另一個網站,該網址就有可能透過 Referer 標頭得到敏感資訊。

    HTTP/1.1 對 POST 的規範,是要求指定的 URI「接受」請求中附上的實體(Entity),像是儲存為檔案、新增為資料庫中的一筆資料等。由於要求伺服器接受的資訊是附在請求本體(Body)而不是在 URI,瀏覽器網址列不會顯示附上的資訊,傳統上敏感資訊也因此常透過 POST 發送。

  • 發送的資料長度

    雖然 HTTP 標準中沒有限制 URI 長度,然而各家瀏覽器對網址列的長度限制不一,伺服器對 URI 長度也有限制,因此資料長度過長的話,就不適用 GET 請求。

    POST 的資料是附在請求本體(Body)而不是在 URI,不會受到網址列長度限制,因而 POST 在過去常被用來發送檔案等大量資訊。

  • 書籤設置考量

    由於瀏覽器書籤功能是針對網址列,因此想讓使用者可以針對查詢結果設定書籤的話,可以使用 GETPOST 後新增的資源不一定會有個 URI 作為識別,基本上無法讓使用者設定書籤。

  • 瀏覽器快取(Cache)

    只要符合 HTTP/1.1 第 13 節對快取的要求,GET 的回應是可以被快取的,最基本的就是指定的 URI 沒有變化時,許多瀏覽器會從快取中取得資料,不過,伺服端可以指定適當的 Cache-Control 標頭來避免 GET 回應被快取的問題。

    至於 POST 的回應,許多瀏覽器(但不是全部)並不會快取,不過 HTTP/1.1 中規範,如果伺服端指定適當的 Cache-ControlExpires 標頭,仍可以對 POST 的回應進行快取。

  • 安全與等冪

    由於傳統上發送敏感資訊時,並不會透過 GET,因而會有人誤解為 GET 不安全,這其實是個誤會,或者說對安全的定義不同,就 HTTP 而言,在 HTTP/1.1 第 9 節對 HTTP 方法的定義中,有區分了安全方法(Safe methods)與等冪方法(Idempotent methods)。

    安全方法是指在實作應用程式時,使用者採取的動作必須避免有他們非預期的結果。慣例上,GETHEAD(與 GET 同為取得資訊,不過僅取得回應標頭)對使用者來說就是「取得」資訊,不應該被用來「修改」與使用者相關的資訊,像是進行轉帳之類的動作,它們是安全方法,這與傳統印象中 GET 比較不安全相反。

    相對之下,POSTPUTDELETE 等其他方法就語義上來說,代表著對使用者來說可能會產生不安全的操作,像是刪除使用者的資料等。

    安全與否並不是指方法對伺服端是否產生副作用(Side effect),而是指對使用者來說該動作是否安全, GET 也有可能在伺服端產生副作用。

    對於副作用的進一步規範是在方法的等冪特性,GETHEADPUTDELETE 是等冪方法,也就是單一請求產生的副作用,與同樣請求進行多次的副作用必須是相同的,舉例來說,若使用 DELETE 的副作用就是某筆資料被刪除,相同請求再執行多次的結果就是該筆資料不存在,而不是造成更多的資料被刪除。 OPTIONSTRACE 本身就不該有副作用,所以他們也是等冪方法。

    HTTP/1.1 中的方法去除掉上述的等冪方法之後,換言之,只有 POST 不具有等冪特性。

    這是使得它與 PUT 有所區別的特性之一,在 HTTP/1.1 規範中,PUT 方法要求將附加的實體儲存於指定的 URI,如果指定的 URI 下已存在資源,則附加的實體是用來進行資源的更新,如果資源不存在,則將實體儲存下來並使用指定的 URI 來代表它,這亦符合等冪特性,例如用 PUT 來更新使用者基本資料,只要附加於請求的資訊相同,一次或多次請求的副作用都會是相同,也就是使用者資訊保持為指定的最新狀態。

  • REST 風格

    現在不少 Web 服務或框架支援 REST 風格的架構,REST 全名 REpresentational State Transfer,REST 架構由客戶端/伺服端組成,兩者間通訊機制是無狀態的(Stateless),在許多概念上,與 HTTP 規範不謀而合(REST 架構基於 HTTP 1.0,與 HTTP1.1 平行發展,但不限於HTTP)。

    符合 REST 架構原則的系統稱其為 RESTful,以基於 HTTP 的基本書籤程式來說,POST /bookmarks 是用來新增一筆資料,GET /bookmarks/1 用來取得 ID 為 1 的書籤,PUT /bookmarks/1 用來更新 ID 為 1 的書籤資料,而 DELETE /bookmarks/1 用來刪除 ID 為 1 的書籤資料。

    然而注意到以上的描述,並不是說 PUT 只能用於更新資源,也沒有說要新增資源只能用 POST。先前在等冪性時談過,PUT 在指定的URI下不存在資源時,也會新建請求中附上的資源。等冪性是在選用 POSTPUT 時考量的要素之一。

    另一個重要的考量要性,在 HTTP/1.1 中也有規範,也就是請求時指定的 URI 之作用。POST 中請求的 URI,是要求其背後資源必須處理附加的實體,而不是代表處理後實體的 URI;然而 PUT 時請求的 URI,就代表請求中附加實體的 URI,無論是更新或是新增實體。

單純就學習 Servlet/JSP 而言,為了避免範例的複雜度,基本上主要還是利用 GETPOST 發送請求,不過記得,你還有其他方法可以使用,實際的應用程式中,不會只是 doGet()doPost()