2.2 处理表单提交
如果在视图中查看 <form>
标签,可以看到它的 method
属性被设置为 POST。而且,<form>
没有声明 action 属性。这意味着在提交表单时,浏览器将收集表单中的所有数据,并通过 HTTP POST 请求将其发送到服务器,发送到显示表单的 GET 请求的同一路径 —— /design
路径。
因此,需要在该 POST 请求的接收端上有一个控制器处理程序方法。需要在 DesignTacoController 中编写一个新的处理程序方法来处理 /design
接口的 POST 请求。
在程序清单 2.4 中,使用 @GetMapping 注释指定 showDesignForm()
方法应该处理 HTTP GET 请求 /design
。与 @GetMapping 处理 GET 请求一样,可以使用 @PostMapping
处理 POST 请求。为了处理玉米卷艺术家提交的设计,将以下程序清单中的 processDesign()
方法添加到 DesignTacoController 中。
程序清单 2.6 使用 @PostMapping 处理 POST 请求
@PostMapping
public String processTaco(Taco taco) {
// Save the taco...
// We'll do this in chapter 3
log.info("Processing taco: " + taco);
return "redirect:/orders/current";
}
当应用到 processTaco()
方法时,@PostMapping 与类级别 @RequestMapping
相协调,以表明 processTaco()
应该处理 /design
接口的 POST 请求。这正是需要处理的一个玉米卷艺术家提交的作品。
提交表单时,表单中的字段被绑定到 Taco 对象的属性(其类在下一个程序清单中显示),该对象作为参数传递给 processTaco()
。从这里开始,processTaco()
方法可以对 Taco 对象做任何它想做的事情。
如果查看程序清单 2.5 中的表单,将看到几个 checkbox 元素,它们都带有 ingredients 名称和一个名为 name 的文本输入元素。表单中的这些字段直接对应于 Taco 类的 ingredients 和 name 属性。
表单上的 Name 字段只需要捕获一个简单的文本值。因此 Taco 的 name 属性的类型是 String。配料复选框也有文本值,但是因为可能选择了零个或多个配料,所以它们绑定到的 ingredients 属性是一个 List<String>
,它将捕获每个选择的配料。
但是等等。如果“配料”复选框是文本值,但 Taco 对象中的配料列表为 list<Ingredient>
,那么这根本就不匹配啊?您怎么能把 [“FLTO”、“GRBF”、“LETC”] 这样的文本列表绑定到一个包含更丰富的对象,不仅包含 ID,还包含描述性名称和配料类型上呢?
这就是转换器派上用场的地方。转换器是实现 Spring 的 Converter 接口并实现其 convert()
方法,获取一个值并将其转换为另一个对象的类。要将字符串转换为配料,我们将使用 IngredientByIdConverter 如下所示:
程序清单 2.7 将字符串转换为配料对象
package tacos.web;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import tacos.Ingredient;
import tacos.Ingredient.Type;
@Component
public class IngredientByIdConverter implements Converter<String, Ingredient> {
private Map<String, Ingredient> ingredientMap = new HashMap<>();
public IngredientByIdConverter() {
ingredientMap.put("FLTO",
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
ingredientMap.put("COTO",
new Ingredient("COTO", "Corn Tortilla", Type.WRAP));
ingredientMap.put("GRBF",
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN));
ingredientMap.put("CARN",
new Ingredient("CARN", "Carnitas", Type.PROTEIN));
ingredientMap.put("TMTO",
new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES));
ingredientMap.put("LETC",
new Ingredient("LETC", "Lettuce", Type.VEGGIES));
ingredientMap.put("CHED",
new Ingredient("CHED", "Cheddar", Type.CHEESE));
ingredientMap.put("JACK",
new Ingredient("JACK", "Monterrey Jack", Type.CHEESE));
ingredientMap.put("SLSA",
new Ingredient("SLSA", "Salsa", Type.SAUCE));
ingredientMap.put("SRCR",
new Ingredient("SRCR", "Sour Cream", Type.SAUCE));
}
@Override
public Ingredient convert(String id) {
return ingredientMap.get(id);
}
}
因为我们还没有配料对象数据库,IngredientByIdConverter 的构造函数创建一个 Map,该 Map 的键是字符串类型,该字符串是配料的 ID,其值是配料对象。在第 3 章中,我们将对该转换器进行调整,从数据库中获取配料数据,而不是像这样硬编码。convert()
方法简单地获取一个配料 ID 字符串,并使用它来查找 Map 中的配料。
请注意,IngredientByIdConverter 用了 @Component
注解,使其成为 Spring 应用程序上下文中的 bean。Spring Boot 自动配置将
发现这个,以及其他转换器 bean,并自动向 Spring MVC 注册它们,并在将请求参数转换为绑定属性时使用。
目前,processTaco()
方法对 Taco 对象没有任何作用。事实上,它什么都做不了。没关系。在第 3 章中,将添加一些持久性逻辑,将提交的 Taco 保存到数据库中。
与 showDesignForm()
方法一样,processTaco()
通过返回一个 String 结束。与 showDesignForm()
类似,返回的值指示将显示给用户的视图。但是不同的是,从 processTaco()
返回的值的前缀是 “redirect:”,表示这是一个重定向视图。更具体地说,它表明在 processTaco()
完成之后,用户的浏览器应该被重定向到相对路径 /order/current。
这样做的想法源于,在创建了一个玉米卷之后,用户将被重定向到一个订单表单,他们可以从该表单下订单,以交付他们的玉米卷。但是还没有一个控制器来处理 /orders/current 请求。
根据现在对 @Controller
、@RequestMapping
和 @GetMapping
的了解,可以轻松地创建这样的控制器。它可能类似于下面的清单。
程序清单 2.8 展现玉米卷订单表单的控制器
package tacos.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
import tacos.TacoOrder;
@Slf4j
@Controller
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/current")
public String orderForm(Model model) {
model.addAttribute("tacoOrder", new TacoOrder());
return "orderForm";
}
}
同样,可以使用 Lombok 的 @Slf4j
注释在运行时创建一个 SLF4J Logger 对象。稍后,将使用这个 Logger 来记录提交的订单的详细信息。
类级别的 @RequestMapping
指定该控制器中的任何请求处理方法都将处理路径以 /orders 开头的请求。当与方法级 @GetMapping
结合使用时,它指定 orderForm()
方法将处理 /orders/current 的 HTTP GET 请求。
至于 orderForm()
方法本身,它非常简单,只返回 orderForm 的逻辑视图名。在第 3 章中,一旦有了把创建的 taco 持久化到数据库的方法,将重新访问该方法并修改它,以使用 taco 对象的列表填充模型,这些对象将按顺序放置。
orderForm 视图由一个名为 orderForm.html 的 Thymeleaf 模板提供,如下面显示的。
程序清单 2.9 taco 订单表单视图
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloud</title>
<link rel="stylesheet" th:href="@{/styles.css}" />
</head>
<body>
<form method="POST" th:action="@{/orders}" th:object="${tacoOrder}">
<h1>Order your taco creations!</h1>
<img th:src="@{/images/TacoCloud.png}"/>
<a th:href="@{/design}" id="another">Design another taco</a><br/>
<div th:if="${#fields.hasErrors()}">
<span class="validationError">
Please correct the problems below and resubmit.
</span>
</div>
<h3>Deliver my taco masterpieces to...</h3>
<label for="name">Name: </label>
<input type="text" th:field="*{deliveryName}"/>
<br/>
<label for="street">Street address: </label>
<input type="text" th:field="*{deliveryStreet}"/>
<br/>
<label for="city">City: </label>
<input type="text" th:field="*{deliveryCity}"/>
<br/>
<label for="state">State: </label>
<input type="text" th:field="*{deliveryState}"/>
<br/>
<label for="zip">Zip code: </label>
<input type="text" th:field="*{deliveryZip}"/>
<br/>
<h3>Here's how I'll pay...</h3>
<label for="ccNumber">Credit Card #: </label>
<input type="text" th:field="*{ccNumber}"/>
<br/>
<label for="ccExpiration">Expiration: </label>
<input type="text" th:field="*{ccExpiration}"/>
<br/>
<label for="ccCVV">CVV: </label>
<input type="text" th:field="*{ccCVV}"/>
<br/>
<input type="submit" value="Submit order"/>
</form>
</body>
</html>
在大多数情况下,orderForm.html 视图是典型的 HTML/Thymeleaf 内容,没有什么值得注意的。但是注意,这里的 <form>
标记与程序清单 2.5 中使用的 <form>
标记不同,因为它还指定了一个表单操作。如果没有指定操作,表单将向呈现表单的相同 URL 提交 HTTP POST 请求。但是在这里,指定表单应该提交到 /orders(使用 Thymeleaf 的 @{…}
操作符作为上下文相关路径)。
因此,需要添加另外一个方法到 OrderController 类中,去处理 /orders 接口的 POST 请求。在进行到下一章之前,还没有办法将订单持久化,因此在这里简化它 —— 类似于在下一个程序清单中看到的内容。
程序清单 2.10 处理 taco 订单提交
@PostMapping
public String processOrder(TacoOrder order) {
log.info("Order submitted: " + order);
return "redirect:/";
}
当调用 processOrder()
方法来处理提交的订单时,它将获得一个 order 对象,其属性绑定到提交的表单字段。Order 非常像 Taco,是一个相当简单的类,它携带订单信息。
现在已经开发了一个 OrderController 和 order 表单视图,可以开始尝试运行了。打开浏览器访问 http://localhost:8080/design,为您的玉米卷选择一些原料,然后点击 Submit Your Taco
按钮。应该会看到类似于图 2.4 所示的表单。
图 2.4 taco 订单表单
在表单中填写一些字段,然后按 Submit Order
按钮。与此同时,请密切关注应用程序日志,以查看订单信息。当我尝试它,日志条目看起来像这样(重新格式化以适应这个页面的宽度):
Order submitted: TacoOrder(deliveryName=Craig Walls, deliveryStreet=1234 7th
Street, deliveryCity=Somewhere, deliveryState=Who knows?, deliveryZip=zipzap,
ccNumber=Who can guess?, ccExpiration=Some day, ccCVV=See-vee-vee, tacos=[])
如果仔细查看来自测试订单的日志条目,可以看到,虽然 processOrder()
方法完成了它的工作并处理了表单提交,但是它让一些错误的信息进来了。表单中的大多数字段包含的数据可能是不正确的。需要添加一些验证,以确保提供的数据至少与所需的信息类型相似。