forward与redirect
本章是"响应数据与视图解析"章节的收尾,讲解 Spring MVC 中两种特殊的请求转发机制。前面章节中,Controller 方法返回视图名,由 ViewResolver 解析为模板文件并渲染。而返回
"forward:/path"或"redirect:/path"时,Spring MVC 会触发完全不同的处理流程——前者是服务器内部转发,后者是客户端重定向。理解两者的差异,是避免表单重复提交、实现页面跳转的关键。
定义与作用
Spring MVC 支持在视图名前加特殊前缀,改变默认的视图解析行为:
forward: 前缀——服务器内部转发
return "forward:/employees/list";
服务器内部将请求转发给另一个 URL,客户端完全无感知。URL 地址栏不变,请求和响应对象在服务器内部传递。
redirect: 前缀——客户端重定向
return "redirect:/employees/list";
服务器返回 HTTP 302 响应,指示客户端重新发起请求到新的 URL。客户端地址栏会变化,是一次全新的 HTTP 请求。
生活类比:公司内部转接 vs 挂断重拨
想象飞翔科技的总机系统:
- forward(内部转接):你拨打总机 8080,说"找技术部"。总机小姐(DispatcherServlet)直接把电话线插到技术部分机(另一个 Controller),你全程只拨了一次号,但通话对象变了。你的手机上显示的号码始终是 8080
- redirect(挂断重拨):你拨打总机 8080,说"找技术部"。总机小姐说"技术部电话是 8081,请重拨"。你挂断电话,重新拨打 8081。手机上显示的号码变成了 8081
forward 是一次请求的服务器内部分发;redirect 是两次独立的 HTTP 请求。
核心原理
forward vs redirect 流程对比
核心差异:
| 维度 | forward | redirect |
|---|---|---|
| HTTP 请求次数 | 1 次 | 2 次 |
| 地址栏变化 | 不变 | 变化 |
| 请求属性共享 | 共享(同一个 request) | 不共享(新 request) |
| 刷新行为 | 重复原请求 | 重复目标 GET 请求 |
| 适用场景 | 内部流程分发 | 表单提交后跳转、跨域跳转 |
| 性能 | 快(服务器内部) | 慢(两次往返) |
适用位置与常用属性
forward: 和 redirect: 作为 Controller 方法的返回值 中的视图名前缀使用。
| 前缀 | 语法 | 说明 |
|---|---|---|
forward: | forward:/path | 服务器内部转发到指定 URL |
redirect: | redirect:/path | 302 重定向到指定 URL |
redirect: | redirect:/path?key=value | 重定向时携带查询参数 |
Flash 属性传递
redirect 后请求属性不共享,但可以通过 Flash Attributes 传递一次性数据:
@PostMapping("/create")
public String create(RedirectAttributes redirectAttrs) {
// 保存员工...
redirectAttrs.addFlashAttribute("message", "员工创建成功");
return "redirect:/employees/list";
}
Flash 属性存储在 Session 中,重定向后的第一次请求可用,读取后自动删除。
完整示例
场景
飞翔科技员工管理系统的"新增员工"功能:表单提交后,如果直接返回视图名,用户刷新页面会重复提交表单(导致重复创建员工)。架构师白歌要求使用 POST-Redirect-GET 模式 解决此问题。
代码实现
package com.feixiang.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/employees")
public class EmployeeController {
private final List<Employee> employees = new ArrayList<>();
/**
* 显示新增表单(GET)
*/
@GetMapping("/new")
public String showForm() {
return "employee-form"; // 普通视图解析
}
/**
* 处理表单提交(POST)
* POST-Redirect-GET 模式:提交后重定向到列表页
*/
@PostMapping
public String createEmployee(@ModelAttribute Employee employee,
RedirectAttributes redirectAttrs) {
employees.add(employee);
// Flash 属性:重定向后可用,读取一次即删除
redirectAttrs.addFlashAttribute("message",
"员工 " + employee.getName() + " 创建成功");
return "redirect:/employees/list"; // 重定向到列表页
}
/**
* 员工列表(GET)
*/
@GetMapping("/list")
public String list(Model model) {
model.addAttribute("employees", employees);
return "employee-list";
}
/**
* forward 示例:内部转发到列表
*/
@GetMapping("/forward-list")
public String forwardList() {
return "forward:/employees/list"; // 服务器内部转发
}
/**
* redirect 带查询参数
*/
@GetMapping("/search")
public String search(@RequestParam String name,
RedirectAttributes redirectAttrs) {
redirectAttrs.addAttribute("keyword", name); // 会附加到 URL
return "redirect:/employees/list";
}
}
// Employee.java
package com.feixiang.web;
public class Employee {
private String name;
private String department;
private Integer salary;
public Employee() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public Integer getSalary() { return salary; }
public void setSalary(Integer salary) { this.salary = salary; }
}
HTTP 请求示例
示例 1:POST-Redirect-GET 模式
# 第一步:POST 提交表单
curl -i -X POST "http://localhost:8080/employees" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=张三" \
-d "department=技术部" \
-d "salary=25000"
响应(第一步):
HTTP/1.1 302
Location: /employees/list
浏览器自动跟随重定向:
# 第二步:GET 列表页(浏览器自动发起)
curl "http://localhost:8080/employees/list"
响应(第二步):
<!DOCTYPE html>
<html>
<head><title>员工列表</title></head>
<body>
<div class="alert">员工 张三 创建成功</div> <!-- Flash 消息 -->
<h1>员工列表</h1>
<table>...</table>
</body>
</html>
此时用户刷新页面,只会重复 GET /employees/list,不会重复提交 POST 请求。
示例 2:forward 内部转发
curl -i "http://localhost:8080/employees/forward-list"
响应:
HTTP/1.1 200
客户端看到的 URL 仍然是 /employees/forward-list,但实际内容是 /employees/list 渲染的结果。地址栏不变。
示例 3:redirect 带查询参数
curl -i "http://localhost:8080/employees/search?name=张三"
响应:
HTTP/1.1 302
Location: /employees/list?keyword=张三
addAttribute 添加的参数会自动附加到重定向 URL 的查询字符串中。
易错场景与面试考点
误区一:POST 请求 forward 到 GET 处理方法的误解
错误认知:"return "forward:/employees/list" 可以把 POST 请求变成 GET 请求的内部调用"
纠正:forward 保持原请求方法不变。如果原请求是 POST,forward 到的目标也必须支持 POST。否则报错 405 Method Not Allowed。
@PostMapping("/create")
public String create() {
return "forward:/employees/list"; // 错误!/employees/list 是 GET
}
解决:POST 提交后想显示列表,应该用 redirect,而不是 forward。
误区二:redirect 后丢失表单数据
现象:表单提交后 redirect 到成功页面,但成功页面无法显示刚才提交的数据。
纠正:redirect 是全新请求,原 request 中的属性全部丢失。解决方案:
- Flash Attributes(推荐):
redirectAttrs.addFlashAttribute("key", value) - URL 参数:
redirectAttrs.addAttribute("key", value)(数据暴露在 URL 中,不适合敏感信息) - 数据库/Session:将数据持久化,重定向后重新查询
误区三:forward 和 redirect 路径前忘记加 /
现象:return "forward:employees/list"(缺少前导 /),路径解析错误。
纠正:
- 加
/:forward:/employees/list→ 从应用根路径开始解析 - 不加
/:forward:employees/list→ 相对当前路径解析,容易出错
最佳实践:始终使用前导 /,确保路径从应用根目录开始。
面试高频:POST-Redirect-GET 模式是什么?
标准回答:
- 问题:用户提交表单(POST)后,如果直接返回页面,刷新浏览器会重复提交表单,导致数据重复创建
- 解决:POST 请求处理完后,不直接返回视图,而是
redirect到一个 GET 页面 - 流程:POST 提交 → 服务器处理 → 302 重定向 → 浏览器 GET 新页面 → 显示结果
- 优点:刷新页面只会重复 GET 请求,不会重复提交数据
- 数据传递:使用 Flash Attributes 在重定向后传递一次性消息(如"操作成功")
小结
forward: 和 redirect: 是 Spring MVC 中改变默认视图解析行为的两个特殊前缀。forward 在服务器内部转发请求,地址栏不变;redirect 通知客户端重新请求,地址栏变化。
核心要点:
forward:1 次请求,共享 request,地址栏不变,适合内部流程分发redirect:2 次请求,不共享 request,地址栏变化,适合表单提交后跳转- POST 提交后必须使用
redirect防止重复提交(POST-Redirect-GET 模式) - Flash Attributes 用于在
redirect后传递一次性数据 - 路径始终使用前导
/,避免相对路径解析错误
本章与全局的关系:本章完成了"响应数据与视图解析"章节的讲解。从 @ResponseBody 的直接输出,到 ResponseEntity 的完整响应控制,再到 ViewResolver 的视图解析、ModelAndView 的动态视图,最后到 forward 与 redirect 的转发机制,形成了完整的响应数据流出链路。至此,Spring MVC 的请求-响应核心流程已全部覆盖。