1. 概述

Web 应用通常依赖用户输入来完成核心业务流程,因此表单提交是数据采集和处理的关键手段。

本文将深入探讨 Spring 提供的 flash attributes 如何在表单提交场景中,安全且可靠地传递数据。这不仅避免了重复提交的风险,还能保证用户体验流畅。

如果你在开发中遇到过“刷新页面导致表单重复提交”这类问题,那这篇文章就是为你准备的。✅


2. Flash Attributes 基础概念

在正式使用 flash attributes 之前,我们得先搞清楚它背后的机制和相关概念,否则容易踩坑 ❌。

2.1. Post/Redirect/Get(PRG)模式

一个常见的错误做法是:用一个 POST 接口处理表单提交,并直接返回一个结果页面。看似简单粗暴,但问题很大——用户一旦刷新页面,POST 请求就会被重新执行,可能导致数据重复插入或业务逻辑重复执行。

为了解决这个问题,业界广泛采用 Post/Redirect/Get(PRG)模式

  1. POST:提交表单数据
  2. Redirect:服务端处理完成后,返回一个 302 重定向响应
  3. 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);
    }
}

关键点:

  • 校验通过后,调用 addFlashAttributepoem 存入 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 的生命周期与适用场景
  • RedirectAttributesRequestContextUtils 的正确用法
  • ✅ 在 Spring Boot + Thymeleaf 项目中完整实现表单提交流程

核心口诀

POST 处理完,addFlashAttribute,
Redirect 跟上,GET 接着拿,
数据用完自动删,安全又省心。

完整示例代码已托管至 GitHub:https://github.com/tech-tutorial/spring-mvc-flash-attributes


原始标题:Guide to Flash Attributes in a Spring Web Application