- Spring实战(第6版)
- (美)克雷格·沃斯
- 2881字
- 2022-12-20 19:14:51
2.2 处理表单提交
仔细看一下视图中的<form>标签,你将会发现它的method属性被设置成了POST。除此之外,<form>并没有声明action属性。这意味着当表单提交的时候,浏览器会收集表单中的所有数据,并以HTTP POST请求的形式将其发送至服务器端,发送路径与渲染表单的GET请求路径相同,也就是“/design”。
因此,在该POST请求的接收端,我们需要有一个控制器处理方法。在DesignTacoController中,我们会编写一个新的处理器方法来处理针对“/design”的POST请求。
在程序清单2.4中,我们曾经使用@GetMapping注解声明showDesignForm()方法要处理针对“/design”的HTTP GET请求。与@GetMapping处理GET请求类似,我们可以使用@PostMapping来处理POST请求。为了处理taco设计的表单提交,在DesignTacoController中添加如程序清单2.6所述的processTaco()方法。
程序清单2.6 使用@PostMapping来处理POST请求
@PostMapping
public String processTaco(Taco taco,
@ModelAttribute TacoOrder tacoOrder) {
tacoOrder.addTaco(taco);
log.info("Processing taco: {}", taco);
return "redirect:/orders/current";
}
如processTaco()方法所示,@PostMapping与类级别的@RequestMapping协作,指定processTaco()方法要处理针对“/design”的POST请求。我们所需要的正是以这种方式处理taco艺术家的表单提交。
表单提交时,表单中的输入域会绑定到Taco对象(这个类会在下面的程序清单中进行介绍)的属性中,该对象会以参数的形式传递给processTaco()。从这里开始,processTaco()就可以针对Taco对象采取任意想要的操作了。在本例中,它将Taco添加到了TacoOrder对象中(后者是以参数的形式传递到方法中来的),然后将taco以日志的形式打印出来。TacoOrder参数上所使用的@ModelAttribute表明它应该使用模型中的TacoOrder对象,这个对象是我们在前面的程序清单2.4中借助带有@ModelAttribute注解的order()方法放到模型中的。
回过头来再看一下程序清单2.5中的表单,你会发现其中包含多个checkbox元素,它们的名字都是ingredients,另外还有一个名为name的文本输入元素。表单中的这些输入域直接对应Taco类的ingredients和name属性。
表单中的name输入域只需要捕获一个简单的文本值。因此,Taco的name属性是String类型的。配料的复选框也有文本值,但是用户可能会选择零个或多个,所以它们所绑定的ingredients属性是一个List<Ingredient>,能够捕获选中的每种配料。
但是,稍等一下!如果配料的复选框是文本型(比如String)的值,而Taco对象以List<Ingredient>的形式表示一个配料的列表,那么这里是不是存在不匹配的情况呢?像["FLTO", "GRBF", "LETC"]这样的文本列表该如何绑定到一个Ingredient对象的列表上呢?要知道,Ingredient是一个更丰富的类型,不仅包括ID,还包括一个描述性的名字和配料类型。
这就是转换器(converter)的用武之地了。转换器是实现了Spring的Converter接口并实现了convert()方法的类,该方法会接收一个值并将其转换成另外一个值。要将String转换成Ingredient,我们要用到如程序清单2.7所示的IngredientByIdConverter。
程序清单2.7 将String转换为Ingredient
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);
}
}
因为我们现在还没有用来获取Ingredient对象的数据库,所以IngredientByIdConverter的构造器创建了一个Map,其中键(key)是String类型,代表了配料的ID,值则是Ingredient对象。在第3章,我们会调整这个转换器,让它从数据库中获取配料数据,而不是像这样硬编码。convert()方法只是简单地获取String类型的配料ID,然后使用它去Map中查找Ingredient。
注意,IngredientByIdConverter使用了@Component注解,使其能够被Spring识别为bean。Spring Boot的自动配置功能会发现它和其他Converter bean。它们会被自动注册到Spring MVC中,在请求参数与绑定属性需要转换时会用到。
现在,processTaco()方法没有对Taco对象进行任何处理。它其实什么都没做。目前,这样是可以的。在第3章,我们会添加一些持久化的逻辑,从而将提交的Taco保存到数据库中。
与showDesignForm()方法类似,processTaco()最后也返回了一个String类型的值。同样与showDesignForm()相似,返回的这个值代表了一个要展现给用户的视图。但是,区别在于processTaco()返回的值带有“redirect:”前缀,表明这是一个重定向视图。更具体地讲,它表明在processDesign()完成之后,用户的浏览器将会重定向到相对路径“/order/current”。
这里的想法是:在创建完taco后,用户将会被重定向到一个订单表单页面,在这里,用户可以创建一个订单,将他们所创建的taco快递过去。但是,我们现在还没有处理“/orders/current”请求的控制器。
根据已经学到的关于@Controller、@RequestMapping和@GetMapping的知识,我们可以很容易地创建这样的控制器。它应该如程序清单2.8所示。
程序清单2.8 展现taco订单表单的控制器
package tacos.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import lombok.extern.slf4j.Slf4j;
import tacos.TacoOrder;
@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("tacoOrder")
public class OrderController {
@GetMapping("/current")
public String orderForm() {
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所示。
程序清单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}"/>
<h3>Your tacos in this order:</h3>
<a th:href = "@{/design}" id = "another">Design another taco</a><br/>
<ul>
<li th:each = "taco : ${tacoOrder.tacos}">
<span th:text = "${taco.name}">taco name</span></li>
</ul>
<h3>Deliver my taco masterpieces to...</h3>
<label for = "deliveryName">Name: </label>
<input type = "text" th:field = "*{deliveryName}"/>
<br/>
<label for = "deliveryStreet">Street address: </label>
<input type = "text" th:field = "*{deliveryStreet}"/>
<br/>
<label for = "deliveryCity">City: </label>
<input type = "text" th:field = "*{deliveryCity}"/>
<br/>
<label for = "deliveryState">State: </label>
<input type = "text" th:field = "*{deliveryState}"/>
<br/>
<label for = "deliveryZip">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内容,不需要过多关注。它首先列出了添加到订单中的taco。这里,使用了Thymeleaf的th:each来遍历订单的tacos属性以创建列表。然后渲染了订单的表单。
但是,需要注意一点,那就是这里的<form>标签和程序清单2.5中的<form>标签不同,指定了一个表单的action。如果不指定action,表单将会以HTTP POST的形式提交到与展现该表单相同的URL上。在这里,我们明确指明表单要POST提交到“/orders”上(使用Thymeleaf的@{}操作符指定相对上下文的路径)。
因此,我们需要在OrderController中添加另外一个方法以便于处理针对“/orders”的POST请求。我们在第3章才会对订单进行持久化,在此之前,我们让它尽可能简单,如程序清单2.10所示。
程序清单2.10 处理taco订单的提交
@PostMapping
public String processOrder(TacoOrder order,
SessionStatus sessionStatus) {
log.info("Order submitted: {}", order);
sessionStatus.setComplete();
return "redirect:/";
}
调用processOrder()方法处理所提交的订单时,我们会得到一个Order对象,它的属性绑定了所提交的表单域。TacoOrder与Taco非常相似,是一个非常简单的类,其中包含了订单的信息。
在这个processOrder()方法中,我们只是以日志的方式记录了TacoOrder对象。在第3章,我们将会看到如何将其持久化到数据库中。但是,processOrder()方法在完成之前,还调用了SessionStatus对象的setComplete()方法,这个SessionStatus对象是以参数的形式传递进来的。当用户创建他们的第一个taco时,TacoOrder对象会被初始创建并放到会话中。通过调用setComplete(),我们能够确保会话被清理掉,从而为用户在下次创建taco时为新的订单做好准备。
现在,我们已经开发了OrderController和订单表单的视图,接下来可以尝试运行一下。打开浏览器并访问http://localhost:8080/design ,为taco选择一些配料,并点击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 = [Taco(name = Awesome Sauce, ingredients = [
Ingredient(id = FLTO, name = Flour Tortilla, type = WRAP), Ingredient(id = GRBF,
name = Ground Beef, type = PROTEIN), Ingredient(id = CHED, name = Cheddar,
type = CHEESE), Ingredient(id = TMTO, name = Diced Tomatoes, type = VEGGIES),
Ingredient(id = SLSA, name = Salsa, type = SAUCE), Ingredient(id = SRCR,
name = Sour Cream, type = SAUCE)]), Taco(name = Quesoriffic, ingredients =
[Ingredient(id = FLTO, name = Flour Tortilla, type = WRAP), Ingredient(id = CHED,
name = Cheddar, type = CHEESE), Ingredient(id = JACK, name = Monterrey Jack,
type = CHEESE), Ingredient(id = TMTO, name = Diced Tomatoes, type = VEGGIES),
Ingredient(id = SRCR,name = Sour Cream, type = SAUCE)])])
似乎processOrder()完成了它的任务,通过日志记录订单详情来完成表单提交的处理。但是,如果仔细查看上述测试订单的日志,会发现它让一些“坏信息”混了进来。表单中的大多数输入域包含的可能都是不正确的数据。我们接下来添加一些校验,确保所提交的数据至少与所需的信息比较相似。