一、传统 Servlet 方式的回顾
正如你所说,在传统 Servlet 中,我们使用原始的 API 来实现页面跳转。
1. 请求转发 (Forward)
-
特点:
- 在服务器内部完成,客户端浏览器无感知。
- 浏览器地址栏的 URL 不会改变。
- 共享同一个
request
对象和response
对象,因此数据可以通过request.setAttribute()
传递。 - 只能跳转到当前 Web 应用内的资源。
-
代码:
// 获取请求转发器,"/success" 是服务器内部的资源路径(URI) RequestDispatcher dispatcher = request.getRequestDispatcher("/success"); // 调用 forward 方法完成转发 dispatcher.forward(request, response);
2. 重定向 (Redirect)
-
特点:
- 通过客户端浏览器重新发送请求。
- 服务器返回一个状态码(302)和一个
Location
响应头,浏览器接收到后会自动向新的Location
发送请求。 - 浏览器地址栏的 URL 会变为新的地址。
- 不共享
request
对象,因为是两次独立的请求。数据传递通常通过 URL 参数(如?key=value
)或 Session。 - 可以跳转到任意 URL,包括其他网站。
-
代码:
// 重定向到站内资源:"/contextPath/success" response.sendRedirect("/myapp/success"); // 需要加上应用上下文路径(Context Path),否则容易出错 // 重定向到站外资源 response.sendRedirect("https://www.baidu.com");
传统方式的痛点:
- 代码繁琐:需要手动获取
RequestDispatcher
或调用sendRedirect
。 - 地址易错:重定向时,如果跳转站内资源,必须考虑上下文路径(Context Path),写绝对路径非常麻烦。
- 理解成本:需要明确理解转发和重定向的原理差异。
二、Spring MVC 的方式
Spring MVC 的 DispatcherServlet
作为核心控制器,极大地简化了这两种操作。它通过解析控制器方法的返回值来实现跳转,统一了地址书写格式。
核心机制:解析视图名
Spring MVC 处理完业务后,会查看控制器方法的返回值,并将其解析为一个具体的视图(如 JSP 页面)或一个跳转指令。
1. 默认请求转发
- 格式:
return "视图名";
- 说明:这是最简单的方式。默认情况下,返回的字符串会被 Spring MVC 的
InternalResourceViewResolver
(视图解析器)当作一个视图名,并将其拼接上前缀(如/WEB-INF/views/
)和后缀(如.jsp
),最终执行一个请求转发操作。 - 示例:
@Controller @RequestMapping("/user") public class UserController { @PostMapping("/add") public String addUser(User user) { // ... 处理添加用户的逻辑 // 默认使用请求转发,跳转到 /WEB-INF/views/success.jsp (假设配置了相应前后缀) return "success"; } }
2. 显式请求转发
- 格式:
return "forward:/路径";
- 说明:使用
forward:
前缀。这告诉 Spring MVC,不要走视图解析器的拼接流程,而是直接使用RequestDispatcher.forward()
方法转发到指定的服务器内部路径。这个路径必须是同一个 Web 应用下的。 - 特点:地址栏不变,共享 request。
- 示例:
@Controller @RequestMapping("/user") public class UserController { @GetMapping("/detail") public String showDetail() { // 显式地转发到另一个控制器的方法 // 浏览器地址栏仍显示 /user/detail,但内容来自 /internal/api/userinfo return "forward:/internal/api/userinfo"; } }
3. 显式重定向
- 格式:
return "redirect:/路径";
- 说明:使用
redirect:
前缀。这告诉 Spring MVC 发送一个重定向响应。最大的优点是:Spring MVC 会自动帮你加上当前应用的上下文路径(Context Path),你不再需要手动拼接。 - 特点:地址栏改变,不共享 request(两次请求)。
- 示例:
为什么要用重定向?:防止表单重复提交。用户刷新浏览器时,只会重新请求@Controller @RequestMapping("/order") public class OrderController { @PostMapping("/create") public String createOrder(Order order) { // ... 处理创建订单的逻辑 // 重定向到 /order/list // 实际响应头为: Location: /你的应用上下文路径/order/list return "redirect:/order/list"; } @GetMapping("/list") public String showOrders() { return "order-list"; } }
GET /order/list
,而不会再次提交POST /order/create
的表单数据。
三、对比总结与最佳实践
特性 | 传统 Servlet | Spring MVC | Spring MVC 地址写法 |
---|---|---|---|
请求转发 | request.getRequestDispatcher(...).forward(...) |
return "forward:/uri" 或 return "view-name" |
/path (应用内路径) |
重定向 | response.sendRedirect(...) |
return "redirect:/uri" |
/path (Spring 自动加 Context Path) |
地址书写 | 重定向需手动处理 Context Path,易错 | 统一简化,无需关心 Context Path | 极其简单清晰 |
代码简洁度 | 繁琐 | 非常简洁 | |
可读性 | 较低 | 非常高 |
最佳实践建议:
-
POST-Redirect-GET 模式:
- 在处理完 POST 请求(如表单提交)后,务必使用重定向 (
redirect:
) 到一个 GET 请求的页面。这是 Web 开发的最佳实践,能有效避免重复提交问题。 - 错误示范:
POST /add -> 转发到 success.jsp
(刷新浏览器会重复提交表单)。 - 正确示范:
POST /add -> 重定向到 /list
->GET /list -> 转发到 list.jsp
。
- 在处理完 POST 请求(如表单提交)后,务必使用重定向 (
-
内部跳转用转发:
- 如果只是在一个请求内部,将处理流程委托给另一个组件(如另一个控制器),且不需要改变浏览器地址,使用
forward:
。
- 如果只是在一个请求内部,将处理流程委托给另一个组件(如另一个控制器),且不需要改变浏览器地址,使用
-
默认视图用默认转发:
- 直接
return "view-name";
是最常用的方式,用于渲染一个普通的 JSP/Thymeleaf 等模板页面。
- 直接
四、示例代码
假设应用上下文路径为 /myapp
。
@Controller
public class DemoController {
// 1. 默认请求转发 (到 /WEB-INF/views/index.jsp)
@GetMapping("/default")
public String defaultForward() {
return "index";
}
// 2. 显式请求转发到另一个控制器
@GetMapping("/forward")
public String explicitForward() {
// 转发后,地址栏仍是 /myapp/forward
return "forward:/api/data";
}
@GetMapping("/api/data")
@ResponseBody
public String getData() {
return "Data from API";
}
// 3. 重定向
@PostMapping("/submit")
public String handleSubmit() {
// 处理提交...
// 重定向后,地址栏变为 /myapp/success
return "redirect:/success";
}
@GetMapping("/success")
public String successPage() {
return "success"; // 转发到 success.jsp
}
}