- Spring實戰(zhàn)(第6版)
- (美)克雷格·沃斯
- 5200字
- 2022-12-20 19:14:50
2.1 展現信息
從根本上來講,Taco Cloud是一個可以在線訂購taco的地方。但是,除此之外,Taco Cloud允許客戶展現其創(chuàng)意,能夠讓他們通過豐富的配料(ingredient)設計自己的taco。
因此,Taco Cloud需要有一個頁面為taco藝術家展現可以選擇的配料??蛇x的配料可能隨時會發(fā)生變化,所以不能將它們硬編碼到HTML頁面中。我們應該從數據庫中獲取可用的配料并將其傳遞給頁面,進而展現給客戶。
在Spring Web應用中,獲取和處理數據是控制器的任務,而將數據渲染到HTML中并在瀏覽器中展現是視圖的任務。為了支撐taco的創(chuàng)建頁面,我們需要構建如下的組件:
● 用來定義taco配料屬性的領域類;
● 用來獲取配料信息并將其傳遞至視圖的Spring MVC控制器類;
● 用來在用戶的瀏覽器中渲染配料列表的視圖模板。
這些組件之間的關系如圖2.1所示。

圖2.1 典型的Spring MVC請求流
因為本章主要關注Spring的Web框架,所以我們會將數據庫相關的內容放到第3章中進行講解。現在的控制器只負責向視圖提供配料。在第3章中,我們會重新改造這個控制器,讓它能夠與存儲庫協(xié)作,從數據庫中獲取配料數據。
在編寫控制器和視圖之前,我們首先確定用來表示配料的領域類型,它會為開發(fā)Web組件奠定基礎。
2.1.1 構建領域類
應用的領域指的是它所要解決的主題范圍,也就是會影響應用理解的理念和概念[1]。在Taco Cloud應用中,領域對象包括taco設計、組成這些設計的配料、顧客以及顧客所下的taco訂單。圖2.2展示了這些實體以及它們是如何關聯(lián)到一起的。

圖2.2 Taco Cloud的領域類
作為開始,我們首先關注taco的配料。在我們的領域中,taco配料是非常簡單的對象。每種配料都有一個名稱和類型,以便于對其進行可視化的分類(蛋白質、奶酪、醬汁等)。每種配料還有一個ID,這樣的話對它的引用就能非常容易和明確。程序清單2.1所示的Ingredient類定義了我們所需的領域對象。
程序清單2.1 定義taco配料
package tacos;
import lombok.Data;
@Data
public class Ingredient {
private final String id;
private final String name;
private final Type type;
public enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
我們可以看到,這是一個非常普通的Java領域類,它定義了描述配料所需的3個屬性。在程序清單2.1中,Ingredient類最不尋常的一點就是它似乎缺少了常見的getter和setter方法,以及像equals()、 hashCode()、toString()等這些有用的方法。
在程序清單中沒有這些方法,除了節(jié)省篇幅的目的外,還因為我們使用了名為Lombok的庫。這是一個非常棒的庫,它能夠在編譯期自動生成這些方法,這樣一來,在運行期就能使用它們了。實際上,類級別的@Data注解就是由Lombok提供的,它會告訴Lombok生成所有缺失的方法,同時還會生成所有以final屬性為參數的構造器。使用Lombok能夠讓Ingredient的代碼簡潔明了。
Lombok并不是Spring庫,但是它非常有用,如果沒有它,開發(fā)工作將很難開展。當我需要在書中將代碼示例編寫得短小簡潔時,它簡直成了我的救星。
要使用Lombok,首先要將其作為依賴添加到項目中。如果你使用Spring Tool Suite,只需要右鍵點擊pom.xml,并從Spring上下文菜單選項中選擇“Add Starters”。在第1章中看到的選擇依賴的對話框將會再次出現(參見圖1.4),這樣,我們就有機會添加依賴或修改已選擇的依賴。在Developer Tools下找到Lombok選項,并確保它處于已選中的狀態(tài),然后選擇“OK”,Spring Tool Suite會自動將其添加到構建規(guī)范中。
另外,你也可以在pom.xml中通過如下的條目進行手動添加:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
如果想要手動添加Lombok到構建之中,還需要在pom.xml文件的<build>部分將其從Spring Boot Maven插件中排除:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
Lombok的魔力是在編譯期發(fā)揮作用的,所以在運行期沒有必要用到它們。像這樣將其排除出去,在最終形成的JAR或WAR文件中就不會包含它了。
Lombok依賴將會在開發(fā)階段為你提供Lombok注解(例如@Data),并且會在編譯期進行自動化的方法生成。但是,我們還需要將Lombok作為擴展添加到IDE上,否則IDE將會報錯,提示缺少方法和final屬性沒有賦值。請訪問Project Lombok網站以查閱如何在你所選擇的IDE上安裝Lombok。
為什么我的代碼中有那么多的錯誤?
需要重申的是,在使用Lombok的時候,你必須在IDE中安裝Lombok插件。否則,IDE將無從得知Lombok提供了getter、setter和其他方法,并且會因為缺失這些方法而報錯。
許多流行的IDE都支持Lombok,包括Eclipse、Spring Tool Suite、IntelliJ IDEA和Visual Studio Code。請訪問Project Lombok網站以了解如何在你的IDE中安裝Lombok插件的更詳細信息。
我相信你會發(fā)現Lombok非常有用,但你也需要知道,它是可選的。在開發(fā)Spring應用時,它并不是強制要使用的,所以你如果不想使用它,完全可以手動編寫這些缺失的方法。你盡可以合上本書去這樣做……我會在這里等你。
配料是taco的基本構成要素。為了解這些配料是如何組合在一起的,我們要定義Taco領域類,如程序清單2.2所示。
程序清單2.2 定義taco設計的領域對象
package tacos;
import java.util.List;
import lombok.Data;
@Data
public class Taco {
private String name;
private List<Ingredient> ingredients;
}
我們可以看到,Taco是一個很簡單的Java領域對象,它包含兩個屬性。與Ingredient一樣,Taco類使用了@Data注解,以便Lombok在編譯期自動生成基本的JavaBean方法。
現在已經定義了Ingredient和Taco,我們還需要一個領域類來定義客戶如何指定他們想要訂購的taco并明確支付信息和投遞信息(配送地址)。這就是TacoOrder類的職責了,如程序清單2.3所示。
程序清單2.3 taco訂單的領域對象
package tacos;
import java.util.List;
import java.util.ArrayList;
import lombok.Data;
@Data
public class TacoOrder {
private String deliveryName;
private String deliveryStreet;
private String deliveryCity;
private String deliveryState;
private String deliveryZip;
private String ccNumber;
private String ccExpiration;
private String ccCVV;
private List<Taco> tacos = new ArrayList<>();
public void addTaco(Taco taco) {
this.tacos.add(taco);
}
}
除了比Ingredient或Taco具有更多的屬性外,TacoOrder并沒有什么特殊的新內容可以討論。它是一個很簡單的領域類,具有9個屬性,其中5個是投遞相關的信息,3個是支付相關的信息,還有一個是組成訂單的Taco對象的列表。它有一個addTaco()方法,是為了方便向訂單中添加taco而增加的。
現在領域類型已經定義完畢,我們可以讓它們運行起來了。接下來,我們會在應用中添加一些控制器,讓它們來處理應用的Web請求。
2.1.2 創(chuàng)建控制器類
在Spring MVC框架中,控制器是重要的參與者。它們的主要職責是處理HTTP請求,要么將請求傳遞給視圖以便于渲染HTML(瀏覽器展現),要么直接將數據寫入響應體(RESTful)。在本章中,我們將會關注使用視圖來為Web瀏覽器生成內容的控制器。在第7章,我們將會看到如何以REST API的形式編寫控制器來處理請求。
對于Taco Cloud應用來說,我們需要一個簡單的控制器,它要完成如下的功能:
● 處理路徑為“/design”的HTTP GET請求;
● 構建配料的列表;
● 處理請求,并將配料數據傳遞給要渲染為HTML的視圖模板,然后發(fā)送給發(fā)起請求的Web瀏覽器。
程序清單2.4中的DesignTacoController類解決了這些需求。
程序清單2.4 初始的Spring控制器類
package tacos.web;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import lombok.extern.slf4j.Slf4j;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;
@Slf4j
@Controller
@RequestMapping("/design")
@SessionAttributes("tacoOrder")
public class DesignTacoController {
@ModelAttribute
public void addIngredientsToModel(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));
}
}
@ModelAttribute(name = "tacoOrder")
public TacoOrder order() {
return new TacoOrder();
}
@ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
@GetMapping
public String showDesignForm() {
return "design";
}
private Iterable<Ingredient> filterByType(
List<Ingredient> ingredients, Type type) {
return ingredients
.stream()
.filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}
對于DesignTacoController,我們先要注意在類級別所應用的注解。首先是@Slf4j,這是Lombok所提供的注解,在編譯期,它會在這個類中自動生成一個SLF4J Logger(SLF4J即simple logging facade for Java,請訪問slf4j網站以了解更多)靜態(tài)屬性。這個簡單的注解和在類中通過如下代碼顯式聲明的效果是一樣的:
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(DesignTacoController.class);
隨后,我們將會用到這個Logger。
DesignTacoController用到的下一個注解是@Controller。這個注解會將這個類識別為控制器,并且將其作為組件掃描的候選者,所以Spring會發(fā)現它并自動創(chuàng)建一個DesignTacoController實例,并將該實例作為Spring應用上下文中的bean。
DesignTacoController還帶有@RequestMapping注解。當@RequestMapping注解用到類級別的時候,它能夠指定該控制器所處理的請求類型。在本例中,它規(guī)定DesignTacoController將會處理路徑以“/design”開頭的請求。
最后,我們可以看到DesignTacoController還帶有@SessionAttributes("tacoOrder")注解,這表明在這個類中稍后放到模型里面的TacoOrder對象應該在會話中一直保持。這一點非常重要,因為創(chuàng)建taco也是創(chuàng)建訂單的第一步,而我們創(chuàng)建的訂單需要在會話中保存,這樣能夠使其跨多個請求。
處理GET請求
修飾showDesignForm()方法的@GetMapping注解對類級別的@RequestMapping進行了細化。@GetMapping結合類級別的@RequestMapping,指明當接收到對“/design”的HTTP GET請求時,Spring MVC將會調用showDesignForm()來處理請求。
@GetMapping只是諸多請求映射注解中的一個。表2.1列出了Spring MVC中所有可用的請求映射注解。
表2.1 Spring MVC的請求映射注解

當showDesignForm()處理針對“/design”的GET請求時,其實并沒有做太多的事情。它只不過返回了一個值為“design”的String,這是視圖的邏輯名稱,用來向瀏覽器渲染模型。
似乎針對“/design”的GET請求并沒有做太多的事情,但事實恰恰相反,除了在showDesignForm()方法中看到的,它還有很多其他的事情做。你可能注意到,程序清單2.4中有一個名為addIngredientsToModel()的方法,它帶有@ModelAttribute注解。這個方法也會在請求處理的時候被調用,構建一個包含Ingredient的配料列表并將其放到模型中?,F在,這個列表是硬編碼的。在第3章,我們會從數據庫中獲取可用的列表。
配料列表準備就緒之后,addIngredientsToModel()方法接下來的幾行代碼會根據配料類型過濾列表,這是通過名為filterByType()的輔助方法實現的。配料類型的列表會以屬性的形式添加到Model對象上,并傳遞給showDesignForm()方法。Model對象負責在控制器和展現數據的視圖之間傳遞數據。實際上,放到Model屬性中的數據將會復制到Servlet Request的屬性中,這樣視圖就能找到它們,并使用它們在用戶的瀏覽器中渲染頁面。
addIngredientsToModel()之后是另外兩個帶有@ModelAttribute注解的方法。這些方法要簡單得多,只創(chuàng)建了一個新的TacoOrder和Taco對象來放置到模型中。TacoOrder對象在前面闡述@SessionAttributes注解的時候曾經提到過,當用戶在多個請求之間創(chuàng)建taco時,它會持有正在建立的訂單的狀態(tài)。除此之外,Taco對象也被放置到了模型中,這樣一來,為響應“/design”的GET請求而呈現的視圖就能展示一個非空的對象了。
我們的DesignTacoController已經具備雛形了。如果現在運行應用并在瀏覽器上訪問“/design”路徑,DesignTacoController的showDesignForm()和addIngredientsToModel()方法將會被調用,它們在將請求傳遞給視圖之前,會將配料和一個空的Taco放到模型中。但是,我們現在還沒有定義視圖,請求將會遇到很糟糕的問題,也就是HTTP 500 (Internal Server Error)錯誤。為了解決這個問題,我們將注意力切換到視圖上,在這里數據將會使用HTML進行裝飾,以便于在用戶的Web瀏覽器中展現。
2.1.3 設計視圖
在控制器完成它的工作之后,現在就該視圖登場了。Spring提供了多種定義視圖的方式,包括JavaServer Pages(JSP)、Thymeleaf、FreeMarker、Mustache和基于Groovy的模板。就現在來講,我們會使用Thymeleaf,這也是我們在第1章開啟這個項目時的選擇。我們會在2.5節(jié)考慮其他的可選方案。
在第1章,我們已經將Thymeleaf作為依賴添加了進來。在運行時,Spring Boot的自動配置功能會發(fā)現Thymeleaf在類路徑中,因此會為Spring MVC自動創(chuàng)建支撐Thymeleaf視圖的bean。
像Thymeleaf這樣的視圖庫在設計時是與特定的Web框架解耦的。這樣一來,它們無法感知Spring的模型抽象,因此,無法與控制器放到Model中的數據協(xié)同工作。但是,它們可以與Servlet的request屬性協(xié)作。所以,在Spring將請求轉移到視圖之前,它會把模型數據復制到request屬性中,Thymeleaf和其他的視圖模板方案就能訪問到它們了。
Thymeleaf模板就是增加一些額外元素屬性的HTML,這些屬性能夠指導模板如何渲染request數據。舉例來說,如果有個請求屬性的key為“message”,我們想要使用Thymeleaf將其渲染到一個HTML <p>標簽中,那么在Thymeleaf模板中,可以這樣寫:
<p th:text = "${message}">placeholder message</p>
模板渲染成HTML時,<p>元素體將會被替換為Servlet request中key為“message”的屬性值。“th:text”是Thymeleaf命名空間中的屬性,它會執(zhí)行這個替換過程。${}操作符會告訴它要使用某個request屬性(在本例中,也就是“message”)中的值。
Thymeleaf還提供了另外一個屬性:th:each,它會迭代一個元素集合,為集合中的每個條目渲染HTML。在我們設計視圖展現模型中的配料列表時,這就非常便利了。舉例來說,如果只想渲染“wrap”配料的列表,可以使用如下的HTML片段:
<h3>Designate your wrap:</h3>
<div th:each = "ingredient : ${wrap}">
<input th:field = "*{ingredients}" type = "checkbox"
th:value = "${ingredient.id}"/>
<span th:text = "${ingredient.name}">INGREDIENT</span><br/>
</div>
在這里,我們在<div>標簽中使用th:each屬性,從而針對wrap request屬性所對應集合中的每個元素重復渲染<div>標簽。每次迭代時,配料元素都會綁定到一個名為ingredient的Thymeleaf變量上。
在<div>元素中,有一個<input>復選框元素,還有一個為復選框提供標簽的<span>元素。復選框使用Thymeleaf的th:value來為渲染出的<input>元素設置value屬性,這里會將其設置為所找到的ingredient的id屬性。而th:field屬性最終會用來設置<input>元素的name屬性,用來記住復選框是否被選中。稍后添加校驗功能時,這能夠確保在出現校驗錯誤的時候,復選框依然能夠保持表單重新渲染前的狀態(tài)。<span>元素使用th:text將“INGREDIENT”占位符文本替換為ingredient的name屬性。
用實際的模型數據進行渲染時,其中一個<div>迭代的渲染結果可能會如下所示:
<div>
<input name = "ingredients" type = "checkbox" value = "FLTO" />
<span>Flour Tortilla</span><br/>
</div>
最終,上述的Thymeleaf片段會成為一大段HTML表單的一部分,我們的taco藝術家用戶會通過這個表單來提交其美味的作品。完整的Thymeleaf模板會包括所有的配料類型,這個表單如程序清單2.5所示:
程序清單2.5 設計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>
<h1>Design your taco!</h1>
<img th:src = "@{/images/TacoCloud.png}"/>
<form method = "POST" th:object = "${taco}">
<div class = "grid">
<div class = "ingredient-group" id = "wraps">
<h3>Designate your wrap:</h3>
<div th:each = "ingredient : ${wrap}">
<input th:field = "*{ingredients}" type = "checkbox"
th:value = "${ingredient.id}"/>
<span th:text = "${ingredient.name}">INGREDIENT</span><br/>
</div>
</div>
<div class = "ingredient-group" id = "proteins">
<h3>Pick your protein:</h3>
<div th:each = "ingredient : ${protein}">
<input th:field = "*{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 th:field = "*{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 th:field = "*{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 th:field = "*{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}"/>
<br/>
<button>Submit Your Taco</button>
</div>
</form>
</body>
</html>
可以看到,我們會為各種類型的配料重復定義<div>片段。另外,我們還包含了Submit按鈕和用戶用來定義其作品名稱的輸入域。
還值得注意的是,完整的模板包含了一個Taco Cloud的商標圖片以及對樣式表的<link>引用[2]。在這兩個場景中,都使用了Thymeleaf的@{}操作符,用來生成一個相對于上下文的路徑,以便于引用我們需要的靜態(tài)制品(artifact)。正如我們在第1章中所學到的,在Spring Boot應用中,靜態(tài)內容要放到根類路徑的“/static”目錄下。
我們的控制器和視圖已經完成了,現在我們可以將應用啟動起來,看一下我們的勞動成果。運行Spring Boot應用有很多種方式。在第1章中,我為你展示了如何通過在Spring Boot Dashboard中點擊Start按鈕來運行應用。不管采用哪種方式啟動Taco Cloud應用,在啟動之后,都可以通過http://localhost:8080/design來進行訪問。你將會看到類似于圖2.3的頁面。

圖2.3 渲染之后的taco設計頁面
這看上去非常不錯!訪問你站點的taco藝術家可以看到一個包含了各種taco配料的表單,他們可以使用這些配料創(chuàng)建自己的杰作。但是當他們點擊Submit your taco按鈕的時候會發(fā)生什么呢?
我們的DesignTacoController還沒有為接收創(chuàng)建taco的請求做好準備。此時提交設計表單會遇到一個錯誤(具體來講,是一個HTTP 405錯誤:Request Method “POST” Not Supported)。接下來,我們通過編寫一些處理表單提交的控制器代碼來修正這個錯誤。
- 隨手查:電腦維護與故障排查
- 技術創(chuàng)新方法
- 大數據環(huán)境下基于知識整合的語義計算技術與應用
- 電腦上網技巧
- Dreamweaver 8中文版完全自學手冊
- 2009年全國高等學校電子信息科學與工程類專業(yè):教學協(xié)作會議論文集
- 云平臺構建與管理
- SolidWorks2018基礎教程 機械實例版
- Axure RP9原型設計實戰(zhàn)案例教材
- 中老年零基礎學智能手機:手機操作+微信應用+網上購物+娛樂與安全(大字大圖版)
- 云數據中心網絡架構與技術(第2版)
- Virtual SAN最佳實踐:部署、管理、監(jiān)控、排錯與企業(yè)應用方案設計
- 網址收藏夾(精華版·2007)
- 區(qū)塊鏈技術指南
- 建筑、室內設計、景觀設計的BIM應用