- 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()完成了它的任務,通過日志記錄訂單詳情來完成表單提交的處理。但是,如果仔細查看上述測試訂單的日志,會發現它讓一些“壞信息”混了進來。表單中的大多數輸入域包含的可能都是不正確的數據。我們接下來添加一些校驗,確保所提交的數據至少與所需的信息比較相似。