前面一章我們已經(jīng)創(chuàng)建了基本的顯示主頁的Web應(yīng)用,實現(xiàn)了簡單的控制器。這里我們進一步增加用戶選擇配料的功能,來看SpringMVC如何處理表單輸入。
SpringMVC,特別是它的控制器,能夠?qū)τ脩舻妮斎胱龀鲰憫?yīng),這個響應(yīng)可以是轉(zhuǎn)到其他的頁面,也可以是對請求數(shù)據(jù)進行處理,還可以是觸發(fā)數(shù)據(jù)庫操作。
我們在前面的一章已經(jīng)用一個簡單的控制器,實現(xiàn)對根目錄"/"請求的響應(yīng)。現(xiàn)在來用控制器實現(xiàn)更復(fù)雜一些的操作,對用戶的更多樣化的請求做出響應(yīng)。
一、 展示配料
我們現(xiàn)在需要實現(xiàn)一個頁面,能夠讓用戶在頁面上對玉米卷的配料進行選擇。這樣我們需要做下面幾個事情。
定義一個配料的領(lǐng)域類。(關(guān)于領(lǐng)域類的解釋放在下面)
獲取用戶選擇的配料信息,并能夠?qū)⑿畔鬟f到視圖,這需要一個控制器。
有一個選擇配料信息的視圖。
二、構(gòu)建配料領(lǐng)域類
領(lǐng)域這個概念,是Eric Evans在《領(lǐng)域驅(qū)動設(shè)計》中提出的概念,這本書也是軟件設(shè)計領(lǐng)域的經(jīng)典書籍。領(lǐng)域是體現(xiàn)業(yè)務(wù)價值,實現(xiàn)業(yè)務(wù)邏輯的概念范圍,是與程序控制邏輯相對應(yīng)的概念。領(lǐng)域設(shè)計的思路,能夠?qū)I(yè)務(wù)規(guī)則與程序控制解耦,使得代碼結(jié)構(gòu)清晰,業(yè)務(wù)邏輯易懂,層級間實現(xiàn)解耦。領(lǐng)域驅(qū)動設(shè)計近來在微服務(wù)設(shè)計中得到的廣泛的應(yīng)用,值得感興趣的同學(xué)深入研究。
在這個應(yīng)用中,我們的領(lǐng)域?qū)ο缶桶ㄅ淞稀㈩櫩?、以及顧客下的訂單等,這個就是業(yè)務(wù)邏輯和業(yè)務(wù)對象。我們現(xiàn)在就針對配料這個對象建立對象類。代碼如下。
import lombok.Data;
import lombok.RequiredArgsConstructor;
?
@Data
@RequiredArgsConstructor
public class Ingredient {
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
把這個類放在src根目錄下就行。這里我們使用了Lombok庫。這個庫能夠能夠自動補全我們類中變量的get、set方法,使得代碼變得簡潔明了。使用maven依賴就可以加入這個庫。
三、創(chuàng)建控制器類
控制器的作用我們前面已經(jīng)講了很多,這里我們要實現(xiàn)對"/"的GET請求,并將配料數(shù)據(jù)傳遞給顯示配料的視圖。代碼如下。
import com.example.demo.Ingredient;
import com.example.demo.Ingredient.Type;
import com.example.demo.Taco;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
?
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
?
@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {
@GetMapping
public String showDesignForm(Model model){
List<Ingredient> ingredients = Arrays.asList(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
new Ingredient("COTO", "Corn Tortilla", Type.WRAP),
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
new Ingredient("CARN", "Carnitas", Type.PROTEIN),
new Ingredient("TMTO", "Diced Tomatoes", Type.VEGGIES),
new Ingredient("LETC", "Lettuce", Type.VEGGIES),
new Ingredient("CHED", "Cheddar", Type.CHEESE),
new Ingredient("JACK", "Monterrey Jack", Type.CHEESE),
new Ingredient("SLSA", "Salsa", Type.SAUCE),
new Ingredient("SRCR", "Sour Cream", Type.SAUCE)
);
Type[] types = Ingredient.Type.values();
for(Type type: types){
model.addAttribute(type.toString().toLowerCase(),
filterByType(ingredients,type));
}
?
model.addAttribute("design", new Taco());
?
return "design";
?
}
?
private List<Ingredient> filterByType(List<Ingredient> ingregients, Type type){
return ingregients
.stream()
.filter(x ->x.getType().equals(type))
.collect(Collectors.toList());
}
?
?
}
@Slf4j是由Lombok提供的注解,它會在程序運行時,在這個類中自動生成一個SLF4J Logger,不用你再去手工聲明一個日志類了,例如:
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
以上聲明將由注解自動完成,你只要在需要日志的地方直接用就行了。
SLF4J是這么一個日志框架。它的全稱是 Simple Logging Facade for Java,它提供了一個針對大多數(shù)日志框架的通用調(diào)用方法,例如對java.util.logging, logback, log4j 等,你可以用它來隨時切換日志框架。
接下來我們使用了@Controller注解。它聲明了下面這個類是控制器。而且由于我們使用了Springboot,由于它的自動掃描和裝配功能,這個類在應(yīng)用啟動的時候,會自動實例化,自動創(chuàng)建一個DesignTacoController實例。
@RequestMapping("/design")注解則聲明這個控制器會處理路徑以"/design"開頭的請求。
@GetMapping注解則在方法級別對上面的注解進行了細(xì)化,指明當(dāng)接收到對"/design"的HPPT GET請求時,將會調(diào)用showDesignForm( ) 方法來處理。
具體的showDesignForm( ) 方法則會處理請求。它創(chuàng)建了一個Ingredient對象的列表。filterByType( )方法則會根據(jù)配料類型過濾列表,這里使用了Java的流特性。
流是Java 8的增加的特性,它可以極大提高Java程序員的生產(chǎn)力,讓程序員寫出高效率、干凈、簡潔的代碼。它對Java的集合能夠采用一個類似SQL語言功能,來對集合=進行排序、過濾、聚合等操作。
過濾好的配料類型的列表會作為屬性添加到Model對象上。Model對象負(fù)責(zé)在控制器和展現(xiàn)數(shù)據(jù)的視圖之間傳遞數(shù)據(jù),它實際上是會被復(fù)制到Servlet 的Response屬性上,這樣視圖就能在其中找到傳遞的數(shù)據(jù)了。
最后,showDesignForm( ) 方法返回“design”,這是視圖的邏輯命名,用來找到對應(yīng)的視圖。
我們目前還沒有設(shè)計能夠響應(yīng)“design”的這個視圖,現(xiàn)在開始吧。
三、設(shè)計視圖
視圖部分仍然采用Thymeleaf模板。具體代碼如下:
<!-- tag::head[] -->
<!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>
<h1>Design your taco!</h1>
<img th:src="@{/images/TacoCloud.png}"/>
?
<!-- tag::formTag[] -->
<form method="POST" th:object="${design}">
<!-- end::all[] -->
?
<span class="validationError"
th:if="${#fields.hasErrors('ingredients')}"
th:errors="*{ingredients}">Ingredient Error</span>
?
<!-- tag::all[] -->
<div class="grid">
<!-- end::formTag[] -->
<!-- end::head[] -->
<div class="ingredient-group" id="wraps">
<!-- tag::designateWrap[] -->
<h3>Designate your wrap:</h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
<!-- end::designateWrap[] -->
</div>
?
<div class="ingredient-group" id="proteins">
<h3>Pick your protein:</h3>
<div th:each="ingredient : ${protein}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
?
<div class="ingredient-group" id="cheeses">
<h3>Choose your cheese:</h3>
<div th:each="ingredient : ${cheese}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
?
<div class="ingredient-group" id="veggies">
<h3>Determine your veggies:</h3>
<div th:each="ingredient : ${veggies}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
?
<div class="ingredient-group" id="sauces">
<h3>Select your sauce:</h3>
<div th:each="ingredient : ${sauce}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
<span th:text="${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
</div>
?
<div>
?
?
<h3>Name your taco creation:</h3>
<input type="text" th:field="*{name}"/>
<!-- end::all[] -->
<span th:text="${#fields.hasErrors('name')}">XXX</span>
<span class="validationError"
th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Error</span>
<!-- tag::all[] -->
<br/>
?
<button>Submit your taco</button>
</div>
<!-- tag::closeFormTag[] -->
</form>
<!-- end::closeFormTag[] -->
</body>
</html>
<!-- end::all[] -->
我們繼續(xù)使用Thymeleaf視圖模板。Thymeleaf是無法直接接受SpringMVC框架的Model中的數(shù)據(jù)的,但是可以接收到Servlet的request屬性。因此SpringMVC會把模型數(shù)據(jù)復(fù)制到request屬性中,Thymeleaf就能訪問到數(shù)據(jù)了。
Thymeleaf模板采用占位符的形式來告訴框架應(yīng)該采用哪個變量的值。例如
<span th:text="${ingredient.name}">INGREDIENT</span></pre>
當(dāng)模板被渲染成HTML時,${ingredient.name}里面的內(nèi)容會被Servlet Request中key為“name”的屬性值替換。
四、運行程序
下面我們來運行程序,可以看到下面的運行結(jié)果。


我們可以看到,配料頁中,顯示了可以選擇的配料,另外還有一個提交按鈕。目前,這個提交按鈕還沒有編碼,我們接下來實現(xiàn)。
五、處理表單提交
在頁面中的代碼是這樣的:
<form method="POST" th:object="${design}">
它表示視圖的method屬性為“POST”。這樣,表單在提交的時候,瀏覽器會搜集表單中的所有數(shù)據(jù),并以HTTP POST請求的形式將其發(fā)送到服務(wù)器,發(fā)送路徑為與視圖路徑一致的“design”。
這樣,我們就需要一個能夠處理“/design”的POST請求的方法,而目前,我們只有一個“GET”的控制器,需要繼續(xù)修改“DesignTacoController”控制器類,在其中增加下面的方法。
@PostMapping
public String processDesign(Taco design){
?
log.info("Processing design: " + design);
?
return "redirect:/orders/current";
}
這里,當(dāng)用戶在前臺頁面選擇了配料,并輸入了名稱后,輸入的信息將會綁定到Taco對象中,將這個對象以參數(shù)的形式傳遞給“ processDesign”方法,在這個方法里面,我們輸出了design這個方法的具體數(shù)值,這就是SpringMvc的功能。

在前臺頁面中,用戶使用多選框選擇的多項配料,都被綁定到Ingredients列表類中,這是因為我們把每個復(fù)選框都命名成了"ingredients",這樣在提交表單的時候,就能夠?qū)⒂脩暨x擇的所有配料,都?xì)w入Ingredients列表,這樣就與Taco類的屬性有了對應(yīng)關(guān)系。
<input name="ingredients" type="checkbox" th:value="${ingredient.id}" />
我們的Taco類如下:
@Data
public class Taco {
?
private String name;
?
private List<String> ingredients;
?
}
包含了name和ingredients列表兩個屬性,能夠與頁面的選擇項對應(yīng)起來。
六、總結(jié)
這一章我們構(gòu)建了一個配料類,設(shè)計了視圖,讓用戶能夠在頁面上對配料進行選擇。后臺的控制器能夠接收到用戶的選擇數(shù)據(jù),然后根據(jù)用戶的選擇,跳轉(zhuǎn)到訂單頁面。
目前這個訂單頁面和它的控制器我們還沒開發(fā)實現(xiàn),將在下一章做這個事情。