文章目录
- 前言
- 一、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";
}