1. 概述
Web 应用通常依赖用户输入来完成核心业务流程,因此表单提交是数据采集和处理的关键手段。
本文将深入探讨 Spring 提供的 flash attributes 如何在表单提交场景中,安全且可靠地传递数据。这不仅避免了重复提交的风险,还能保证用户体验流畅。
如果你在开发中遇到过“刷新页面导致表单重复提交”这类问题,那这篇文章就是为你准备的。✅
2. Flash Attributes 基础概念
在正式使用 flash attributes 之前,我们得先搞清楚它背后的机制和相关概念,否则容易踩坑 ❌。
2.1. Post/Redirect/Get(PRG)模式
一个常见的错误做法是:用一个 POST 接口处理表单提交,并直接返回一个结果页面。看似简单粗暴,但问题很大——用户一旦刷新页面,POST 请求就会被重新执行,可能导致数据重复插入或业务逻辑重复执行。
为了解决这个问题,业界广泛采用 Post/Redirect/Get(PRG)模式:
- POST:提交表单数据
- Redirect:服务端处理完成后,返回一个 302 重定向响应
- GET:浏览器自动发起新的 GET 请求,加载结果页面
这样一来,最终用户看到的是一个 GET 请求的结果页。即使刷新页面,也只是重复执行无副作用的 GET,不会重复提交数据。✅
2.2. Flash Attributes 的生命周期
在 PRG 模式中,我们需要把 POST 中处理的数据(比如提交成功的信息)传递给后续的 GET 请求。但常规手段都不太合适:
- ❌
RequestAttributes
:无法跨请求保留,重定向后就丢了 - ❌
SessionAttributes
:生命周期太长,会一直留在 session 中,容易造成内存泄漏或数据污染
这时候就得靠 Spring 的 flash attributes 出场了。
它专为“一次性的跨重定向数据传递”而设计。核心方法都在 RedirectAttributes
接口中:
RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue);
RedirectAttributes addFlashAttribute(Object attributeValue);
Map<String, ?> getFlashAttributes();
⚠️ 关键特性:
- 数据仅在下一次请求中有效
- 重定向前自动存入临时存储(如 session)
- 下一个请求读取后自动清除
- 典型应用场景:提交成功提示、错误消息传递
2.3. FlashMap 数据结构
Spring 使用 FlashMap
来管理 flash attributes,本质上是一个带扩展功能的 HashMap
。
public final class FlashMap extends HashMap<String, Object> implements Comparable<FlashMap> {
@Nullable
private String targetRequestPath;
private final MultiValueMap<String, String> targetRequestParams
= new LinkedMultiValueMap<>(4);
private long expirationTime = -1;
}
关键点:
- 继承自
HashMap
,支持 key-value 存储 ✅ - 可绑定目标 URL(
targetRequestPath
),确保 flash 数据只在指定接口可用 - 支持设置过期时间,防止长期滞留
每个请求上下文中都有两个 FlashMap
实例:
- Output FlashMap:在 POST 阶段使用,存放即将传递的数据
- Input FlashMap:在重定向后的 GET 阶段使用,读取之前传递的数据(只读)
2.4. FlashMapManager 与 RequestContextUtils
FlashMapManager
是管理 FlashMap
的核心组件,定义了存储和读取策略:
public interface FlashMapManager {
@Nullable
FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}
实际开发中我们更多使用 RequestContextUtils
这个工具类,它提供了静态方法直接操作 flash map:
public static Map<String, ?> getInputFlashMap(HttpServletRequest request);
public static FlashMap getOutputFlashMap(HttpServletRequest request);
public static FlashMapManager getFlashMapManager(HttpServletRequest request);
public static void saveOutputFlashMap(String location,
HttpServletRequest request, HttpServletResponse response);
常用场景:
getInputFlashMap()
:在 GET 接口中获取传来的 flash 数据getOutputFlashMap()
:手动构造 flash 数据(较少用)saveOutputFlashMap()
:显式保存 flash map(框架通常自动处理)
3. 实战:诗歌投稿系统
光讲理论不够直观,下面我们通过一个简单的“诗歌投稿”功能,演示 flash attributes 的完整用法。
3.1. Thymeleaf 配置
前端使用 Thymeleaf 模板引擎,只需引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>3.1.5</version>
</dependency>
application.properties
中配置模板路径:
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
模板文件放在 src/main/resources/templates/
目录下即可自动加载。
3.2. 领域模型
定义 Poem
实体类:
public class Poem {
private String title;
private String author;
private String body;
// getter/setter 省略
}
添加校验逻辑:
public static boolean isValidPoem(Poem poem) {
return poem != null && Strings.isNotBlank(poem.getAuthor())
&& Strings.isNotBlank(poem.getBody())
&& Strings.isNotBlank(poem.getTitle());
}
3.3. 创建表单页面
先提供一个 GET 接口展示投稿表单:
@GetMapping("/poem/submit")
public String submitGet(Model model) {
model.addAttribute("poem", new Poem());
return "submit";
}
对应 HTML 模板(submit.html
):
<form action="#" method="post" th:action="@{/poem/submit}" th:object="${poem}">
<input type="text" th:field="*{title}" placeholder="诗歌标题" />
<input type="text" th:field="*{author}" placeholder="作者" />
<textarea th:field="*{body}" placeholder="内容"></textarea>
<button type="submit">提交</button>
</form>
3.4. 实现 PRG 提交流程
处理表单提交的 POST 接口:
@PostMapping("/poem/submit")
public RedirectView submitPost(
HttpServletRequest request,
@ModelAttribute Poem poem,
RedirectAttributes redirectAttributes) {
if (Poem.isValidPoem(poem)) {
redirectAttributes.addFlashAttribute("poem", poem);
return new RedirectView("/poem/success", true);
} else {
return new RedirectView("/poem/submit", true);
}
}
关键点:
- 校验通过后,调用
addFlashAttribute
将poem
存入 flash - 返回
RedirectView
触发 302 重定向到/poem/success
接下来实现重定向后的 GET 接口:
@GetMapping("/poem/success")
public String getSuccess(HttpServletRequest request) {
Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
if (inputFlashMap != null) {
Poem poem = (Poem) inputFlashMap.get("poem");
// 可选:将数据再放回 model,供模板使用
return "success";
} else {
return "redirect:/poem/submit";
}
}
⚠️ 注意:必须检查 inputFlashMap
是否为空,防止用户直接访问 /poem/success
导致空指针。
最后是成功页面模板(success.html
):
<h1 th:if="${poem}">
<p th:text="${'You have successfully submitted poem titled - '+ poem?.title}"/>
Click <a th:href="@{/poem/submit}"> here</a> to submit more.
</h1>
页面中可以直接使用 ${poem}
,因为 Thymeleaf 会自动从 flash attributes 中提取并暴露到视图。
4. 总结
通过本文,你应该已经掌握:
- ✅ PRG 模式如何避免重复提交
- ✅ flash attributes 的生命周期与适用场景
- ✅
RedirectAttributes
和RequestContextUtils
的正确用法 - ✅ 在 Spring Boot + Thymeleaf 项目中完整实现表单提交流程
核心口诀:
POST 处理完,addFlashAttribute,
Redirect 跟上,GET 接着拿,
数据用完自动删,安全又省心。
完整示例代码已托管至 GitHub:https://github.com/tech-tutorial/spring-mvc-flash-attributes