調派請求


在 Web 應用程式中,有時需要多個 Servlet 來完成請求,像是將另一個 Servlet 的回應包括進來,或將請求轉發給別的 Servlet 處理。例如,在〈關於 MVC/Model 2〉曾經看過範例中出現這段程式碼:

request.getRequestDispatcher("hello.jsp").forward(request, response);

透過 HttpServletRequestgetRequestDispatcher() 取得的是一個實作了 RequestDispatcher 介面的物件,呼叫 HttpServletRequestgetRequestDispatcher() 時需要傳入一個相對於目前請求 URL 的路徑資訊。

(你還有另外兩個方式,可以取得 RequestDispatcher 的方式還有兩個,即透過 ServletContextgetRequestDispatcher()getNamedDispatcher() ,之後談到 ServletContext 時會再介紹。)

RequestDispatcher 上有個 include() 方法,可以讓你將另一個 Servlet 回應包括至目前的回應之中。例如:

package cc.openhome;

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/some")
public class Some extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        out.println("Some do one...");
        RequestDispatcher dispatcher = request.getRequestDispatcher("other");
        dispatcher.include(request, response);
        out.println("Some do two...");
    } 
}

other 實際上會循 URL 模式取得對應的 Servlet。呼叫 include() 時,必須分別傳入實作 HttpServletRequestHttpServletResponse 介面的物件,這可以是 service() 方法上傳入的物件,或者是自定義的物件或包裹器(Wrapper)。如果被 include() 的 Servlet 是這麼撰寫的:

package cc.openhome;

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/other")
public class Other extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.getWriter().println("Other do one...");
    }
}

則網頁上見到的順序是:

Some do one... Other do one... Some do two... 

在取得 RequestDispatcher 時,也可以包括查詢字串,這會在被包括(Include)或轉發(Forward,透過 forward() 方法)的 Servlet 中取得。例如:

request.getRequestDispatcher("other?data=123456").include(request, response);

則被包括的 Servlet,可以使用 requset.getParameter("data") 來取得請求參數值。

Servlet 實例之前彼此獨立,在調派請求的過程中,如果有必須共用的物件,必須透過容器來做溝通,方式之一是設定給請求物件成為屬性,稱之為請求範圍屬性(Request Scope Attribute)。HttpServletRequest 上與請求範圍屬性有關的幾個方法是:

  • getAttribute()
  • getAttributeNames()
  • setAttribute()
  • removeAttribute()

例如有個 Servlet 會根據某些條件查詢資料:

List<Book> books = bookDAO.query("some books");
request.setAttribute("books", books); 
request.getRequestDispatcher("result").include(request, response); 

假設 result 這個 URL 是個負責回應的 Servlet 實例,則它可以利用 HttpServletRequest 物件的 getAttribute() 取得查詢結果:

List<Book> books = (List<Book>) request.getAttribute("books");

調派請求

由於請求物件僅在此次請求週期內有效,在請求/回應之後,請求物件會被銷毀回收資源,設定在請求物件中的屬性自然也就消失了,所以透過 setAttribute() 設定的屬性才稱之為請求範圍屬性。

在設定請求範圍屬性時,需注意屬性名稱由 java.javax. 開頭的名稱通常保留給規格書中某些特定意義之屬性。例如:

  • javax.servlet.include.request_uri
  • javax.servlet.include.context_path
  • javax.servlet.include.servlet_path
  • javax.servlet.include.path_info
  • javax.servlet.include.query_string
  • javax.servlet.include.mapping(Servlet 4.0 新增)

以上的屬性名稱在被包括的 Servlet 中,分別表示被包含的 Servlet 的 Request URI、Context path、Servlet path、Path info 與取得 RequestDispatcher 時給定的請求參數,如果被包括的 Servlet 還有包括其他的Servlet,則這些屬性名稱的對應值也會被代換。

之所以會需要這些請求屬性名稱,是因為在 RequestDispatcher 執行 include() 時,必須傳入 requestresponse 物件,而這兩個物件來自於最前端的 Servlet,後續的 Servlet 若使用 requestresponse 物件,也就會是一開始最前端 Servlet 收到的兩個物件,此時嘗試在後續的 Servlet 中使用 request 物件的 getRequestURI() 等方法,得到的資訊跟第一個 Servlet 中執行getRequestURI() 等方法是相同的。

然而,有時必須取得 include() 時傳入的路徑資訊,而不是第一個 Servlet 的路徑資訊,這時候就必須透過方才的幾個屬性名稱來取得,你不用記憶那些屬性名稱,可以透過 RequestDispatcher 定義的常數來取得:

  • RequestDispatcher.INCLUDE_REQUEST_URI
  • RequestDispatcher.INCLUDE_CONTEXT_PATH
  • RequestDispatcher.INCLUDE_SERVLET_PATH
  • RequestDispatcher.INCLUDE_PATH_INFO
  • RequestDispatcher.INCLUDE_QUERY_STRING
  • RequestDispatcher.INCLUDE_MAPPING(Servlet 4.0 新增)

前五個取得屬性都是字串,而 RequestDispatcher.INCLUDE_MAPPING 取得的屬性會是 HttpServletMapping 實例,因此可以透過它的 getMappingMatch() 等方法取得相關的 URL 匹配資訊(詳見〈URL 模式〉)。

在使用 include() 時,被包括的 Servlet 中可以使用 getSession() 方法取得 HttpSession 物件(之後會介紹,預設會在回應中加個一個 Cookie 請求標頭),除了這個之外,在被包括的 Servlet 中任何對請求標頭的設定都會被忽略。

RequestDispatcher 有個 forward() 方法,呼叫時同樣必須傳入請求與回應物件,這表示你要將請求處理轉發給別的 Servlet,回應亦轉發給另一個 Servlet,因此要呼叫 forward() 方法的話,目前的 Servlet 不能有任何回應確認(Commit),如果在目前的 Servlet 的有透過回應物件設定了一些回應但未確認(回應緩衝區未滿或未呼叫任何出清方法),則所有回應設定會被忽略,如果已經有回應確認且呼叫了 forward() 方法,則會丟出 IllegalStateException

在被轉發請求的 Servlet 中,亦可透過以下的請求範圍屬性名稱取得對應資訊:

  • javax.servlet.forward.request_uri
  • javax.servlet.forward.context_path
  • javax.servlet.forward.servlet_path
  • javax.servlet.forward.path_info
  • javax.servlet.forward.query_string
  • javax.servlet.forward.mapping(Servlet 4.0 新增)

同樣地,會需要這些請求屬性的原因在於,在 RequestDispatcher 執行 forward() 時,必須傳入 requestresponse 物件,而這兩個物件來自於最前端的 Servlet,後續的 Servlet 若使用 requestresponse 物件,也就會是一開始最前端 Servlet 收到的兩個物件,此時嘗試在後續的 Servlet 中使用 request 物件的 getRequestURI() 等方法,得到的資訊跟第一個 Servlet 中執行getRequestURI() 等方法是相同的。

然而,有時必須取得 forward() 時傳入的路徑資訊,而不是第一個 Servlet 的路徑資訊,這時候就必須透過方才的幾個屬性名稱來取得,你不用記憶那些屬性名稱,可以透過 RequestDispatcher 定義的常數來取得:

  • RequestDispatcher.FORWARD_REQUEST_URI
  • RequestDispatcher.FORWARD_CONTEXT_PATH
  • RequestDispatcher.FORWARD_SERVLET_PATH
  • RequestDispatcher.FORWARD_PATH_INFO
  • RequestDispatcher.FORWARD_QUERY_STRING
  • RequestDispatcher.FORWARD_MAPPING(Servlet 4.0 新增)

由於請求的 include()forward(),是屬於容器內部流程的調派,而不是在回應中要求瀏覽器重新請求某些 URL,因此瀏覽器不會知道實際的流程調派,也就是說,瀏覽器的網址列上也就不會有任何變化。