文章目錄
- 前言
- 一、Servlet 資源轉發是什么?
- 1. 為什么要資源轉發?
- 二、資源轉發 vs 重定向
- 三、如何使用 RequestDispatcher 進行資源轉發
- 1. 引入依賴
- 2. 獲取 RequestDispatcher
- 3. forward 示例
- 4. include 示例
- JSP 中 include 指令或動作
- Servlet 中 include
- 四、注意事項與常見陷阱
- 五、基于注解與 web.xml 配置
- 六、在現代框架中的類比
前言
在 Java Web 開發中,Servlet 作為服務端處理請求的重要組件,通常會在業務邏輯處理完成后,將請求或數據“轉發”到另一個資源(如 JSP、另一個 Servlet、靜態資源等)進行展示或后續處理。
一、Servlet 資源轉發是什么?
Servlet 資源轉發是指在服務器端,將當前 HTTP 請求(及其附帶的請求屬性、參數等)由一個資源(Servlet、JSP、HTML 等)內部傳遞到另一個資源處理或展示,而不再讓客戶端發起新的請求。典型方式是通過 javax.servlet.RequestDispatcher
(或 Jakarta EE 中的同等接口)完成 forward
或 include
操作。
- forward:將請求“完全”轉給目標資源處理,目標資源處理完成后,返回響應給客戶端;客戶端看到的 URL 不會改變,仍然是最初的請求 URL。
- include:在當前響應中“嵌入”另一個資源的輸出,通常用于頁面片段(如頭部、尾部、側邊欄等)的復用。
1. 為什么要資源轉發?
- 隱藏真實資源路徑:客戶端只看到請求 URL,內部可以組織不同資源處理和渲染。
- 共享請求數據:可以在轉發前通過
request.setAttribute(...)
設置數據,目標資源直接通過request.getAttribute(...)
獲取,無需重新請求或重定向傳參。 - 性能與體驗:服務器端無需額外與客戶端交互,只做內部轉發,減少一次 HTTP 往返。
- 職責分離:將業務邏輯與視圖渲染分離。Servlet 處理業務、準備數據后,通過轉發到 JSP/模板渲染頁面。
二、資源轉發 vs 重定向
特性 | 資源轉發(forward) | 重定向(redirect) |
---|---|---|
機制類型 | 服務器端內部轉發 | 服務器向客戶端發送 302 狀態碼,客戶端再發起新請求 |
URL 變化 | 不改變:瀏覽器地址欄仍顯示原始 URL | 改變:地址欄顯示重定向后的新 URL |
請求/響應完整性 | 保留同一次請求:request、response 對象同一實例 | 新請求:無法保留原 request 的屬性;需要通過參數或 Session 傳遞 |
性能 | 較好:一次請求、服務器內部跳轉 | 較差:多一次 HTTP 往返 |
適用場景 | 同一站內資源、內部分發、MVC 中轉發到視圖 | 跨站、登錄后重定向到新頁面、避免表單重復提交(POST-Redirect-GET) |
瀏覽器可見性 | 不可見:瀏覽器地址欄不知內部轉發 | 可見:地址欄顯示真實新的 URL |
例如:登錄表單提交后,如果想在同一請求中校驗并展示錯誤信息,通常用 forward;如果登錄成功后,想讓瀏覽器地址欄跳轉到首頁且避免刷新時重復提交,一般用 redirect。
三、如何使用 RequestDispatcher 進行資源轉發
以下以傳統 Servlet API(Java EE/Servlet 規范)為主。
1. 引入依賴
如果使用 Maven + Servlet 容器(如 Tomcat),通常在項目中引入如下依賴(在 Servlet 規范已由容器提供時,這里可僅在編譯時作用):
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope>
</dependency>
或 Jakarta EE:
<dependency><groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId><version>5.0.0</version><scope>provided</scope>
</dependency>
2. 獲取 RequestDispatcher
在 Servlet 的 doGet
或 doPost
中,可以通過以下方式獲取:
// 相對路徑,基于當前 Servlet 的 context 路徑
RequestDispatcher rd1 = request.getRequestDispatcher("/WEB-INF/views/result.jsp");// 也可通過 ServletContext 獲取,路徑以 “/” 開頭,代表相對于 webapp 根
RequestDispatcher rd2 = getServletContext().getRequestDispatcher("/WEB-INF/views/result.jsp");
request.getRequestDispatcher(path)
:路徑若以/
開頭,表示相對于當前 web 應用根;若不以/
開頭,表示相對于調用 Servlet 的路徑,需要注意常見誤區,建議始終以/
開頭。getServletContext().getRequestDispatcher(path)
:路徑必須以/
開頭,相對于 web 應用根。
3. forward 示例
下面示例為:用戶提交表單后,Servlet 處理邏輯,根據條件轉發到不同 JSP。
@WebServlet("/login")
public class LoginServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 1. 處理請求字符編碼(若 POST 表單)request.setCharacterEncoding("UTF-8");String username = request.getParameter("username");String password = request.getParameter("password");// 簡單校驗示例(真實應用要調用服務或 DAO 層)if ("admin".equals(username) && "password".equals(password)) {// 登錄成功:準備用戶信息User user = new User(username);request.getSession().setAttribute("currentUser", user);// 轉發到歡迎頁面RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/welcome.jsp");rd.forward(request, response);} else {// 登錄失敗:設置錯誤信息并轉發回登錄頁面request.setAttribute("errorMessage", "用戶名或密碼錯誤");RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/login.jsp");rd.forward(request, response);}}@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 直接跳轉到登錄頁面RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/login.jsp");rd.forward(request, response);}
}
- 注意:forward 之前不能執行
response.getWriter().write(...)
等會提交響應體或 commit response 的操作,否則會拋 IllegalStateException。 - 路徑:通常將 JSP 放在
WEB-INF
目錄下,以防用戶直接通過 URL 訪問,只能通過轉發訪問。
4. include 示例
當想在某頁面中復用一些公共片段(如頁頭、導航、頁腳),可以在 JSP/Servlet 中使用 include:
JSP 中 include 指令或動作
<%@ include file="/WEB-INF/views/common/header.jsp" %> <%-- 編譯時包含,靜態包含 --%>
<jsp:include page="/WEB-INF/views/common/navbar.jsp" /> <%-- 運行時包含 --%><!-- 頁面內容主體 --><jsp:include page="/WEB-INF/views/common/footer.jsp" />
Servlet 中 include
@WebServlet("/report")
public class ReportServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {// 先在響應中輸出一些信息,或通過 include 嵌入其他資源RequestDispatcher header = req.getRequestDispatcher("/WEB-INF/views/common/header.jsp");header.include(req, resp);// 處理主體邏輯List<Item> data = fetchData();req.setAttribute("items", data);RequestDispatcher body = req.getRequestDispatcher("/WEB-INF/views/reportBody.jsp");body.include(req, resp);// 頁腳RequestDispatcher footer = req.getRequestDispatcher("/WEB-INF/views/common/footer.jsp");footer.include(req, resp);}
}
include
不會改變請求的處理流,執行完 include 后,控制權回到原 Servlet,可繼續輸出內容。- 注意響應緩沖區和字符編碼:在 include 之前應設置好
response.setContentType("text/html;charset=UTF-8")
等。
四、注意事項與常見陷阱
-
response 已提交后不能 forward/include
- 如果輸出已 flush 到客戶端(如調用
response.getWriter()
并寫入大量內容后 flush),再調用forward
會拋IllegalStateException
。 - 建議:在 forward 前設置好全部屬性、編碼,不要提前輸出內容。
- 如果輸出已 flush 到客戶端(如調用
-
路徑書寫要正確
- 推薦用以
/
開頭的絕對路徑,基于 Web 應用根目錄,如/WEB-INF/views/...
。 - 切勿使用相對路徑(不以
/
開頭),可能導致尋找不到資源。
- 推薦用以
-
字符編碼
- 在接收 POST 請求參數前確保設置
request.setCharacterEncoding("UTF-8")
。 - JSP 頁面應有
<%@ page contentType="text/html;charset=UTF-8" %>
,且容器配置匹配。
- 在接收 POST 請求參數前確保設置
-
JSP 放置位置
- 通常將 JSP 放在
WEB-INF
目錄,避免用戶直接通過 URL 訪問,必須通過 Servlet forward 訪問,以控制流程和權限。 - 若靜態資源(CSS、JS、圖片等)需直接訪問,則放在 webapp 根或靜態目錄。
- 通常將 JSP 放在
-
在 Filter 中使用 forward
- 過濾器可在
doFilter
中通過chain.doFilter(request, response)
繼續執行或在某條件下request.getRequestDispatcher(...).forward(...)
。注意,一旦 forward,需要 return,否則后續代碼及 chain 可能出現邏輯混亂。
- 過濾器可在
-
跨 Context 轉發(不同 Web 應用之間)
- 標準 Servlet API 不支持跨 Context 直接 forward;可通過
ServletContext.getContext("/otherApp")
獲取另一個 context 的 RequestDispatcher,但大多數容器默認禁用或安全限制較多,需謹慎。 - 一般建議通過外部重定向或共享服務層實現跨應用調用。
- 標準 Servlet API 不支持跨 Context 直接 forward;可通過
-
并發和線程安全
- forward 本身線程安全,但若在請求作用域或 session 作用域中存儲可變共享對象,需注意并發訪問。
- Servlet 實例通常為單例,多線程并發執行
doGet/doPost
,請避免在 Servlet 成員變量中存儲與請求相關狀態。
-
調試技巧
- 容易遇到 404 Not Found(找不到 JSP),先檢查路徑是否正確,是否已編譯到目標目錄。
- 瀏覽器地址欄不變:在測試時注意 URL 不變特性,方便判斷是否發生了 forward。
五、基于注解與 web.xml 配置
- 注解方式(Servlet 3.0+ 推薦):在 Servlet 類上使用
@WebServlet("/path")
,無需在web.xml
再額外配置。 - web.xml 方式:老項目或精細配置時,可在
WEB-INF/web.xml
中配置<servlet>
和<servlet-mapping>
。兩者對 forward 使用無影響,關鍵在于 Dispatcher 的路徑。
示例(web.xml):
<servlet><servlet-name>UserListServlet</servlet-name><servlet-class>com.example.servlet.UserListServlet</servlet-class>
</servlet>
<servlet-mapping><servlet-name>UserListServlet</servlet-name><url-pattern>/users</url-pattern>
</servlet-mapping>
六、在現代框架中的類比
雖然本文聚焦原生 Servlet,但在常見 MVC 框架中(如 Spring MVC),也存在“轉發”概念。例如在 Spring MVC Controller 中,返回視圖名時,框架默認會將請求轉發到相應的視圖渲染器;若要重定向,可以使用 redirect:
前綴。理解 Servlet 原理有助于深入掌握框架行為。
@GetMapping("/hello")
public String hello(Model model) {model.addAttribute("msg", "Hello");// 返回視圖名,最終內部類似 forward 到 /WEB-INF/views/hello.jspreturn "hello";
}@GetMapping("/goRedirect")
public String goRedirect() {// 重定向示例:客戶端會看到 URL 變化return "redirect:/otherPage";
}