官术网_书友最值得收藏!

仔細看一下視圖中的<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

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

主站蜘蛛池模板: 奉化市| 西丰县| 海晏县| 博乐市| 六安市| 灵丘县| 丽江市| 永靖县| 于都县| 镶黄旗| 赞皇县| 城固县| 长宁区| 闽侯县| 汶川县| 繁峙县| 鄂州市| 内江市| 江口县| 丰顺县| 西平县| 射阳县| 丹东市| 渝中区| 霍州市| 米泉市| 新闻| 二连浩特市| 凤阳县| 望江县| 东丰县| 拜泉县| 阜康市| 柘城县| 车致| 星座| 红原县| 新乡县| 许昌县| 丁青县| 砚山县|