Taco Cloud是一个可以在线订购taco的地方。 Taco Cloud允许客户展现其创意, 能够让他们通过丰富
的配料(ingredient) 设计自己的taco。Taco Cloud需要有一个页面为taco艺术家展现都可以选择哪
些配料。
在Spring Web应用中, 获取和处理数据是控制器的任务, 而将数据渲染到HTML中并在浏览器中展现则是视图的任务。
为了支撑taco的创建页面, 我们需要构建如下组件。
taco配料是非常简单的对象。 每种配料都有一个名称和类型, 以便于对其进行可视化的分类(蛋白质、 奶酪、 酱汁等) 。 每种配料还有一个ID, 这样的话对它的引用就能非常容易和明确。
定义Ingredient类,Ingredient类为配料类。
@Data注解就是由Lombok提供的, 它会告诉Lombok生成所有缺失的方法, 同时还会生成所有以
final属性作为参数的构造器。 通过使用Lombok, 我们能够让Ingredient的代码简洁明了。
在pom文件中添加对Lombox的依赖。
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
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
}
}
在Spring MVC框架中, 控制器是重要的参与者。 它们的主要职责是处理HTTP请求, 要么将请求传递给视图以便于渲染HTML(浏览器展现) , 要么直接将数据写入响应体(RESTful)。
对于Taco Cloud应用来说, 我们需要一个简单的控制器, 它要完成如下功能:
在程序中创建 DesignTacoController控制器,处理 “/design” 请求。
@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> ingredients, Type type) {
return ingredients.stream()
.filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}
@Slf4j,Lombok所提供的注解, 在运行时,它会在这个类中自动生成一个SLF4J(Simple Logging Facade for Java) Logger。
@Controller,这个注解会将这个类识别为控制器,并且将其作为组件扫描的候选者,Spring会发现它并自动创建一个DesignTacoController实例, 并将该实例作为Spring应用上下文中的bean。
@RequestMapping注解用到类级别的时候, 它能够指定该控制器所处理的请求类型。
@GetMapping对类级别的@RequestMapping进行了细化。@GetMapping结合类级别的@RequestMapping,指明当接收到对“/design”的HTTP GET请求时, 将会调用showDesignForm()来处理请求
Spring MVC的请求映射注解表
注解 | 描述 |
---|---|
@RequestMapping | 通用的请求处理 |
@GetMapping | 处理HTTP GET请求 |
@PostMapping | 处理HTTP POST请求 |
@DeleteMapping | 处理HTTP DELETE请求 |
@PatchMapping | 处理HTTP PATCH请求 |
推荐在类级别上使用@RequestMapping, 以便于指定基本路径。 在每个处理器方法上,使用更具体的注解。
showDesignForm()方法处理请求,构建了一个Ingredient对象的列表。showDesignForm()方法接下来的几行代码会根据配料类型过滤列表。 配料类型的列表会作为属性添加到Model对象上。Model对象负责在控制器和展现数据的视图之间传递数据。 showDesignForm()方法最后返回“design”, 这是视图的逻辑名称, 会用来将模型渲染到视图上。
在视图中将Model中的数据取出,展现在页面中。
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloudtitle>
<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="${design}">
<div class="grid">
<div class="ingredient-group" id="wraps">
<h3>Designate your wrap:h3>
<div th:each="ingredient : ${wrap}">
<input name="ingredients" type="checkbox" th:value="${ingredient.id}"/>
<span th:text="${ingredient.name}">INGREDIENTspan><br/>
div>
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}">INGREDIENTspan><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}">INGREDIENTspan><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}">INGREDIENTspan><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}">INGREDIENTspan><br/>
div>
div>
div>
<div>
<h3>Name your taco creation:h3>
<input type="text" th:field="*{name}"/>
<br/>
<button>Submit your tacobutton>
div>
form>
body>
html>
运行程序,访问http://localhost:8080/design,就可以查看到页面。
在视图中的标签,它的method属性被设置成了POST,并没有声明action属性。当表单提交的时候, 浏览器会收集表单中的所有数据,以HTTP POST请求的形式将其发送至服务器端, 发送路径与渲染表单的GET请求路径相同, 也就是“/design”。
需要在DesignTacoController控制器中,编写对 “/design” 的POST请求。
@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";
}
在提交表单的时候,数据会和Taco类中的属性对应的绑定起来,需要先定义Taco类。
import java.util.List;
import lombok.Data;
@Data
public class Taco {
private String name;
private List<String> ingredients;
}
processDesign()的方法最后也返回了一个String类型的值,同样与showDesignForm()相似, 返回的这个值代表了一个要展现给用户的视图。 区别在于processDesign()返回的值带有“redirect:”前缀, 表明这是一个重定向视图。 表明在processDesign()完成之后, 用户的浏览器将会重定向到相对路径“/order/current”
下面编写处理“/orders/current”请求的控制器。
orderForm这个方法,只是返回了一个名为orderForm的逻辑视图名。
@Slf4j
@Controller
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/current")
public String orderForm(Model model) {
model.addAttribute("order", new Order());
return "orderForm";
}
}
下面将编写orderForm.html这个文件。
在页面中将订单的信息展示出来。
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloudtitle>
<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 tacoa><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>
页面中form标签指定了一个表单的action,指定了method。明确指明表单要POST提交到“/orders”上。需要在OrderController中添加另外一个方法, 以便于处理针对“/orders”的POST请求。
@PostMapping
public String processOrder(Order order) {
log.info("Order submitted: " + order);
return "redirect:/";
}
当调用processOrder()方法处理所提交的订单时, 我们会得到一个Order对象, 它的属性绑定了所提交的表单域。
下面创建Order类。
@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;
}
现在可以运行一下程序,访问http://localhost:8080/design,选择配料,点击提交,将会来到订单页面,填写订单信息,点击提交,会在控制台中输出填写完成的订单信息。表单中的大多数输入域包含的可能都是不正确的信息。 应该添加一些校验, 确保所提交的数据至少与所需的信息比较接近。
Spring支持Java的Bean校验API(Bean ValidationAPI, 也被称为JSR-303)。
要在Spring MVC中应用校验, 我们需要。
对于Taco类来说, 我们想要确保name属性不能为空或null, 同时希望选中的配料至少要包含一项。
可以声明以下的校验规则。
name属性不为null之外,还声明了它的值在长度上至少要有5个字符。
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public class Taco {
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
@Size(min=1, message="You must choose at least 1 ingredient")
private List<String> ingredients;
}
对提交的订单进行校验时,我们必须要给Order类添加注解。
对于地址相关的属性,保证没有提交空白字段。可以使用Hibernate Validator的@NotBlank注解。
确保ccNumber属性不为空,还要保证它所包含的值是一个合法的信用卡号码。@CreditCardNumber注解。 这个注解声明该属性的值必须是合法的信用卡号。
ccExpiration属性必须符合MM/YY格式(两位的月份和年份),@Pattern注解并为其提供了一个正则
表达式,确保属性值符合预期的格式。
ccCVV属性需要是一个3位的数字,@Digits注解, 能够确保它的值包含3位数字。
所有的校验注解都包含了一个message属性,该属性定义了当输入的信息不满足声明的校验规则时要给用户展现的消息。
import javax.validation.constraints.Digits;
import javax.validation.constraints.Pattern;
import org.hibernate.validator.constraints.CreditCardNumber;
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class Order {
@NotBlank(message="Name is required")
private String name;
@NotBlank(message="Street is required")
private String street;
@NotBlank(message="City is required")
private String city;
@NotBlank(message="State is required")
private String state;
@NotBlank(message="Zip code is required")
private String zip;
@CreditCardNumber(message="Not a valid credit card number")
private String ccNumber;
@Pattern(regexp="^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$",message="Must be formatted MM/YY")
private String ccExpiration;
@Digits(integer=3, fraction=0, message="Invalid CVV")
private String ccCVV;
}
修改每个控制器, 让表单在POST提交至对应的控制器方法时执行对应的校验。
要校验提交的Taco,需要为DesignTacoController中processDesign()方法的Taco参数添加一个Java Bean Validation API的@Valid注解。
@Valid注解会告诉Spring MVC要对提交的Taco对象进行校验, 而校验时机是在它绑定完表单数据之后、 调用processDesign()之前。 如果存在校验错误, 那么这些错误的细节将会捕获到一个Errors对象中并传递给processDesign()。 processDesign()方法的前几行会查阅Errors对象,调用其hasErrors()方法判断是否有校验错误。
@PostMapping
public String processDesign(@Valid Taco design, Errors errors) {
if (errors.hasErrors()) {
return "design";
}
// Save the taco design...
// We'll do this in chapter 3
log.info("Processing design: " + design);
return "redirect:/orders/current";
}
对提交的Order对象进行校验, OrderController的processOrder()方法做类似变更。
@PostMapping
public String processOrder(@Valid Order order, Errors errors) {
if (errors.hasErrors()) {
return "orderForm";
}
log.info("Order submitted: " + order);
return "redirect:/";
}
为了在页面显示校验错误,需要对页面进行改造,如下所示:
orderForm.html
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloudtitle>
<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 tacoa><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}"/>
<span class="validationError"
th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Errorspan>
<br/>
<label for="street">Street address: label>
<input type="text" th:field="*{street}"/>
<span class="validationError"
th:if="${#fields.hasErrors('street')}"
th:errors="*{street}">Street Errorspan>
<br/>
<label for="city">City: label>
<input type="text" th:field="*{city}"/>
<span class="validationError"
th:if="${#fields.hasErrors('city')}"
th:errors="*{city}">City Errorspan>
<br/>
<label for="state">State: label>
<input type="text" th:field="*{state}"/>
<span class="validationError"
th:if="${#fields.hasErrors('state')}"
th:errors="*{state}">State Errorspan>
<br/>
<label for="zip">Zip code: label>
<input type="text" th:field="*{zip}"/>
<span class="validationError"
th:if="${#fields.hasErrors('zip')}"
th:errors="*{zip}">Zip Errorspan>
<br/>
<h3>Here's how I'll pay...h3>
<label for="ccNumber">Credit Card #: label>
<input type="text" th:field="*{ccNumber}"/>
<span class="validationError"
th:if="${#fields.hasErrors('ccNumber')}"
th:errors="*{ccNumber}">CC Num Errorspan>
<br/>
<label for="ccExpiration">Expiration: label>
<input type="text" th:field="*{ccExpiration}"/>
<span class="validationError"
th:if="${#fields.hasErrors('ccExpiration')}"
th:errors="*{ccExpiration}">CC Num Errorspan>
<br/>
<label for="ccCVV">CVV: label>
<input type="text" th:field="*{ccCVV}"/>
<span class="validationError"
th:if="${#fields.hasErrors('ccCVV')}"
th:errors="*{ccCVV}">CC Num Errorspan>
<br/>
<input type="submit" value="Submit order"/>
form>
body>
html>
design.html
<div>
<h3>Name your taco creation:h3>
<input type="text" th:field="*{name}"/>
<br/>
<span th:text="${#fields.hasErrors('name')}">XXXspan>
<span class="validationError"
th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Errorspan>
<br/>
<button>Submit your tacobutton>
div>
视图控制器是只将请求转发到视图而不做其他事情的控制器。
声明一个配置类,继承WebMvcConfigurer 接口,addViewControllers()方法会接收一个ViewControllerRegistry对象,我们可以使用它注册一个或多个视图控制器。
在这里将“/”传递了进去, 视图控制器将会针对该路径执行GET请求。 这个方法会返回ViewControllerRegistration对象, 基于该对象调用了setViewName()方法, 用它指明当请求“/”的时
候要转发到“home”视图上。
为每种配置(Web、 数据、 安全等)创建新的配置类, 这样能够保持应用的引导配置类尽可能地整洁和简单。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
Spring非常灵活, 能够支持很多常见的模板方案。
支持的模板方案
模板 | Spring Boot starter依赖 |
---|---|
FreeMarker | spring-boot-starter-freemarker |
Groovy | Templates spring-boot-starter-groovy-templates |
Java Server Pages(JSP) | 无(由Tomcat或Jetty提供) |
Mustache | spring-boot-starter-mustache |
Thymeleaf | spring-boot-starter-thymeleaf |
选择想要的视图模板库, 将其作为依赖项添加到构建文件中, 然后就可以在“/templates”目录下 编写模板了。Spring Boot会探测到你所选择的模板库, 并自动配置为Spring MVC控制器生成视图所需的各种组件。
缓存模板
默认情况下, 模板只有在第一次使用的时候解析一次, 解析的结果会被后续的请求所使用。 能防止每次请求时多余的模板解析过程, 因此有助于提升性能。在开发期, 这个特性就不太友好了。 假设我们启动完应用之后访问taco的设计页面, 然后决定对它做一些修改, 但是当我们刷新Web浏览器的时候显示的依然是原始的版本。 要想看到变更效果, 就必须重新启动应用, 这当然是非常不方便的。
可以禁用缓存。就是将相关的缓存属性设置为false。如要禁用Thymeleaf缓存, 我们只需要在application.properties中添加如下这行代码:spring.thymeleaf.cache=false
唯一需要注意的是,在应用部署到生产环境之前,一定要删除这一行代码(或者将其设置为true)。