1. 概述
Web 应用程序通常依赖于用户输入来满足其多个用例。因此,表单提交是一种大量使用的机制,用于收集和处理此类应用的数据。
在本教程中,我们将了解 Spring 的 flash 属性如何帮助我们安全可靠地完成表单提交工作流程。
2. 闪光属性基础知识
在我们可以轻松使用 flash 属性之前,我们需要对表单提交工作流程和一些关键相关概念有很好的理解。
2.1. 发布/重定向/获取模式
设计 Web 表单的一种天真方法是使用单个 HTTP POST 请求来处理提交并通过其响应返回确认。但是,这种设计暴露了重复处理 POST 请求的风险,以防用户最终刷新页面。
为了减少重复处理的问题,我们可以将工作流创建为按特定顺序(即 POST、重定向和 GET)的互连请求序列。简而言之,我们称之为表单提交的发布/重定向/获取 (PRG) 模式。
收到 POST 请求后,服务器会处理它,然后转移控制权以发出 GET 请求。随后,根据 GET 请求的响应显示确认页面。理想情况下,即使最后一个GET请求被多次尝试,也不应该有任何不利的副作用。
2.2. 闪存属性的生命周期
要使用 PRG 模式完成表单提交,我们需要在重定向后将信息从初始 POST 请求传输到最终 GET 请求。
不幸的是,我们既不能使用 RequestAttributes,也不能使用 SessionAttributes。 这是因为前者不会在不同的控制器之间重定向后幸存下来,而后者即使在表单提交结束后也会持续整个会话。
但是,我们不需要担心,因为Spring的Web框架提供了可以解决这个确切问题的flash属性。
让我们看看 RedirectAttributes 接口中可以帮助我们在项目中使用 flash 属性的方法:
RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue);
RedirectAttributes addFlashAttribute(Object attributeValue);
Map<String, ?> getFlashAttributes();
闪存属性的生存期很短。因此,它们在重定向之前临时存储在某个基础存储中。重定向后,它们仍可用于后续请求,然后它们就消失了。
2.3. 闪图数据结构
Spring 提供了一个名为 FlashMap 的抽象数据结构,用于将 flash 属性存储为键值对。
让我们看一下 FlashMap 类的定义:
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;
}
我们可以注意到,FlashMap 类从 HashMap 类继承了它的行为。因此,FlashMap 实例可以存储属性的键值映射。此外,我们可以绑定一个 FlashMap 实例,使其仅由特定的重定向 URL 使用。
此外,每个请求都有两个 FlashMap 实例,即输入 FlashMap 和 Output FlashMap,它们在 PRG 模式中起着重要作用:
- 输出闪光图在 POST 请求中用于临时保存闪光属性,并在重定向后将它们发送到下一个 GET 请求
- 输入闪光图在最终的GET请求中用于访问重定向前上一个POST请求发送的只读闪存属性
2.4. FlashMapManager 和 RequestContextUtils
顾名思义,我们可以使用FlashMapManager来管理FlashMap实例。
首先,我们来看看这个策略接口的定义:
public interface FlashMapManager {
@Nullable
FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response);
void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response);
}
简而言之,我们可以说FlashMapManager允许我们读取,更新和保存FlashMap实例在某些底层存储中。
接下来,让我们熟悉 RequestContextUtils 抽象实用程序类中可用的一些静态方法。
为了将重点保持在本教程的范围内,我们将介绍与 flash 属性相关的方法:
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);
我们可以使用这些方法来检索输入/输出的 FlashMap 实例,获取请求的 FlashMapManager,并保存 FlashMap 实例。
3. 表单提交用例
到目前为止,我们已经对围绕闪光属性的不同概念有了基本的了解。因此,让我们进一步,在诗歌比赛 Web 应用程序中使用它们。
我们的诗歌比赛应用程序有一个简单的用例,通过提交表格接受来自不同诗人的诗歌作品。此外,参赛作品将具有与诗歌相关的必要信息,例如标题、正文和作者姓名。
3.1. 百里香叶配置
我们将使用Thymeleaf,这是一个Java模板引擎,用于通过简单的HTML模板创建动态网页。
首先,我们需要将 spring-boot-starter-thymeleaf 依赖项添加到项目的 pom 中.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
接下来,我们可以在位于 src/main/resources 目录中的pplication.properties 文件中定义一些特定于 Thymeleaf 的属性:
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
定义这些属性后,我们现在可以在 /src/main/resources/templates 目录下创建所有视图。反过来,Spring 会将.html后缀附加到控制器内命名的所有视图中。
3.2. 域模型
接下来,让我们在 Poem 类中定义我们的领域模型:
public class Poem {
private String title;
private String author;
private String body;
}
此外,我们可以在 Poem 类中添加 isValidPoem() 静态方法,以帮助我们验证字段是否不允许空字符串:
public static boolean isValidPoem(Poem poem) {
return poem != null && Strings.isNotBlank(poem.getAuthor())
&& Strings.isNotBlank(poem.getBody())
&& Strings.isNotBlank(poem.getTitle());
}
3.3. 创建表单
现在,我们已准备好创建提交表单。为此,我们需要一个端点 /poem/submit 来提供 GET 请求以向用户显示表单:
@GetMapping("/poem/submit")
public String submitGet(Model model) {
model.addAttribute("poem", new Poem());
return "submit";
}
在这里,我们使用一个模型作为容器来保存用户提供的特定于诗歌的数据。此外,提交获取方法返回由提交视图提供的视图。
此外,我们希望将 POST 表单与模型属性诗绑定:
<form action="#" method="post" th:action="@{/poem/submit}" th:object="${poem}">
<!-- form fields for poem title, body, and author -->
</form>
3.4. 发布/重定向/获取提交流程
现在,让我们为表单启用 POST 操作。为此,我们将在 PoemSubmit 控制器中创建 /poem/submit 端点来为 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);
}
}
我们可以注意到,如果提交成功,则控制权将转移到 /poem/success 端点。此外,我们在启动重定向之前将诗歌数据添加为 flash 属性。
现在,我们需要向用户显示一个确认页面,因此让我们实现将为 GET 请求提供服务的 /poem/success 端点的功能:
@GetMapping("/poem/success")
public String getSuccess(HttpServletRequest request) {
Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
if (inputFlashMap != null) {
Poem poem = (Poem) inputFlashMap.get("poem");
return "success";
} else {
return "redirect:/poem/submit";
}
}
这里需要注意的是,在我们决定重定向到成功页面之前,我们需要验证闪光图。
最后,让我们使用成功页面中的 flash 属性诗来显示用户提交的诗的标题:
<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>
4. 结论
在本教程中,我们学习了有关发布/重定向/获取模式和闪存属性的一些概念。而且,我们还看到了 flash 属性在 Spring Boot Web 应用程序中通过简单的表单提交而起作用。
与往常一样,本教程的完整源代码可在 GitHub 上找到。