2.2 处理表单提交

2.2 处理表单提交

仔细看一下视图中的<form>标签,你将会发现它的method属性被设置成了POST。除此之外,<form>并没有声明action属性。这意味着当表单提交的时候,浏览器会收集表单中的所有数据,并以HTTP POST请求的形式将其发送至服务器端,发送路径与渲染表单的GET请求路径相同,也就是“/design”。

因此,在该POST请求的接收端,我们需要有一个控制器处理方法。在DesignTacoController中,我们会编写一个新的处理器方法来处理针对“/design”的POST请求。

在程序清单2.2中,我们曾经使用@GetMapping注解声明showDesignForm()方法要处理针对“/design”的HTTP GET请求。与@GetMapping处理GET请求类似,我们可以使用@PostMapping来处理POST请求。为了处理taco设计的表单提交,在DesignTacoController中添加程序清单2.4所述的processDesign()方法。

程序清单2.4 使用@PostMapping来处理POST请求

1
2
3
4
5
6
7
@PostMapping
public String processDesign(Taco design) {
// Save the taco design...
// We'll do this in chapter 3
log.info("Processing design: " + design);
return "redirect:/orders/current";
}

如processDesign()方法所示,@PostMapping与类级别的@RequestMapping协作,指定processDesign()方法要处理针对“/design”的POST请求。我们所需要的正是以这种方式处理taco艺术家的表单提交。

当表单提交的时候,表单中的输入域会绑定到Taco对象(这个类会在下面的程序清单中进行介绍)的属性中,该对象会以参数的形式传递给processDesign()。从这里开始,processDesign()就可以针对Taco对象采取任意操作了。

程序清单2.5 定义taco设计的领域对象

1
2
3
4
5
6
7
8
package tacos;
import java.util.List;
import lombok.Data;
@Data
public class Taco {
private String name;
private List<String> ingredients;
}

我们可以看到,Taco是一个非常简单的Java领域对象,其中包含了几项属性。与Ingredient类似,Taco类也添加了@Data注解,会在编译期自动生成必要的JavaBean方法,所以这些方法在运行期是可用的。

我们可以看到,Taco是一个非常简单的Java领域对象,其中包含了几项属性。与Ingredient类似,Taco类也添加了@Data注解,会在编译期自动生成必要的JavaBean方法,所以这些方法在运行期是可用的。

回过头来再看一下程序清单2.3中的表单,你会发现其中包含多个checkbox元素,它们的名字都是ingredients,另外还有一个名为name的文本输入元素。表单中的这些输入域直接对应Taco类的ingredients和name属性。

表单中的name输入域只需要捕获一个简单的文本值。因此,Taconame属性是String类型的。配料的复选框也有文本值,但是用户可能会选择一个或多个,所以它们所绑定的ingredients属性是一个List<String>,能够捕获选中的每种配料。

processDesign()方法对Taco对象没有执行任何操作。实际上,这个方法什么都没做。现在,这样是可以的。到第3章,我们将会添加一些持久化的逻辑,将提交的Taco保存到一个数据库中。

与showDesignForm()方法类似,processDesign()最后也返回了一个String类型的值。同样与showDesignForm()相似,返回的这个值代表了一个要展现给用户的视图。但是,区别在于processDesign()返回的值带有“redirect:”前缀,表明这是一个重定向视图。更具体地讲,它表明在processDesign()完成之后,用户的浏览器将会重定向到相对路径“/order/current”。

这里的想法是在创建完taco后,用户将会被重定向到一个订单表单页面,在这里用户可以创建一个订单,将他们所创建的taco快递过去。但是,我们现在还没有处理“/orders/current”请求的控制器。

根据已经学到的关于@Controller、@RequestMapping和@GetMapping的知识,我们可以很容易地创建这样的控制器。它应该如程序清单2.6所示。

程序清单2.6 展现taco订单表单的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package tacos.web;
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
import tacos.Order;
@Slf4j
@Controller
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/current")
public String orderForm(Model model) {
model.addAttribute("order", new Order());
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.7所示。

程序清单2.7 一个taco订单表单视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!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="${order}">
<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="*{name}"/>
<br/>
<label for="street">Street address: </label>
<input type="text" th:field="*{street}"/>
<br/>
<label for="city">City: </label>
<input type="text" th:field="*{city}"/>
<br/>
<label for="state">State: </label>
<input type="text" th:field="*{state}"/>
<br/>
<label for="zip">Zip code: </label>
<input type="text" th:field="*{zip}"/>
<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.3中的<form>标签有所不同,它指定了一个表单的action。如果不指定action,那么表单将会以HTTP POST的形式提交到与展现该表单相同的URL上。在这里,我们明确指明表单要POST提交到“/orders”上(使用Thymeleaf的@{…}操作符指定相对上下文的路径)。

因此,我们需要在OrderController中添加另外一个方法,以便于处理针对“/orders”的POST请求。我们在第3章才会对订单进行持久化,在此之前,我们让它尽可能简单,如程序清单2.8所示。

程序清单2.8 处理taco订单的提交

1
2
3
4
5
@PostMapping
public String processOrder(Order order) {
log.info("Order submitted: " + order);
return "redirect:/";
}

当调用processOrder()方法处理所提交的订单时,我们会得到一个Order对象,它的属性绑定了所提交的表单域。Order与Taco非常相似,是一个非常简单的类,其中包含了订单的信息,如程序清单2.9所示。

程序清单2.9 taco订单的领域对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package tacos;
import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import org.hibernate.validator.constraints.NotBlank;
import lombok.Data;
@Data
public class Order {
private String name;
private String street;
private String city;
private String state;
private String zip;
private String ccNumber;
private String ccExpiration;
private String ccCVV;
}

现在,我们已经开发了OrderController和订单表单的视图,接下来我们可以尝试运行一下。打开浏览器并访问http://localhost:8080/design,为taco选择一些配料,并点击“Submit Your Taco”按钮,将会看到如图2.3所示的一个表单。

epub_29101559_22

图2.3 taco订单表单

填充表单的一些输入域并点击“Submit order”按钮。请关注应用的日志来查看你的订单信息。在我尝试运行的时候,日志条目如下所示(为了适应页面的宽度,重新进行了格式化):

1
2
3
Order submitted: Order(name=Craig Walls,street1=1234 7th Street,
city=Somewhere, state=Who knows?, zip=zipzap, ccNumber=Who can guess?,
ccExpiration=Some day, ccCVV=See-vee-vee)

如果仔细查看上述测试订单的日志,就会发现尽管processOrder()方法完成了它的工作并处理了表单提交,但是它让一些错误的信息混入了进来。表单中的大多数输入域包含的可能都是不正确的信息。我们接下来添加一些校验,确保所提交的数据至少与所需的信息比较接近。