大家好,我是
方圆
拿上这把军刀,也不要乱砍呐!
目录
一、实现“做Taco”的页面
1. Ingredient类和Cato类
package tacos;
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
}
}
@RequiredArgsConstructor
:这个时Lombok中的注解,并不常用,它生成的是包含常量
和被标记为NotNull变量
的构造函数,且为私有构造(private
)
package tacos;
import lombok.Data;
import java.util.List;
@Data
public class Taco {
private String name;
private List<String> ingredients;
}
2. 创建“做Taco”的Controller
要满足如下条件:
- 处理“/design”的 HTTP GET请求
- 构建配料的列表
- 处理请求,传递数据,渲染页面给浏览器
package tacos;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
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;
import static tacos.Ingredient.*;
@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 = Type.values();
for(Type type : types){
//attributeName and attributeValue
model.addAttribute(type.toString().toLowerCase(),filterByType(ingredients,type));
}
model.addAttribute("design",new Taco());
return "design";
}
//根据配料类型过滤列表
private List<Ingredient> filterByType(List<Ingredient> ingredients,Type type) {
//对参数进行过滤,将其中与传入的type相同的拿出来后,再变成List
return ingredients.stream().filter(x -> x.getType().equals(type)).collect(Collectors.toList());
}
}
Tips: 实际上,存储到Model中的数据将会复制到Servlet Response的属性
中,这样才能在视图中找到它们。
-
return ingredients.stream().filter(x -> x.getType().equals(type)).collect(Collectors.toList());
最后一行代码值得我们关注一下,其中将ingredients转换为流,对其中的内容进行过滤,筛选出与type一致的内容,再转换为List,实现了挑选的功能。 -
@Slf4j
:该注解是Simple Logging Facade for Java的缩写,来自Lombok包,用代码显示如下,即创建一个简单的日志
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(DesignTacoController.class);
3. @RequestMapping和@GetMapping
- @RequestMapping是
通用请求注解
,可以通过如下方式将其转为@GetMapping
@RequestMapping(method = RequestMethod.GET)
虽然可以实现转化,但是GetMapping更加简洁
,并且直接表明
了它要处理GET请求。
其他请求如下
注解 | 请求 |
---|---|
PostMapping | 处理Post请求 |
PutMapping | 处理Put请求 |
DeleteMapping | 处理Delete请求 |
PatchMapping | 处理Patch请求 |
4. 创建design.html跳转页面
<!-- tag::all[] -->
<!-- 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[] -->
Tips: 实际上,Thymeleaf在设计时是与Web框架解耦
的,在Spring将请求发到视图之前,会先将属性复制到Servlet request的属性
中,这样Thymeleaf就能访问到传过来的数据了。
<div th:each="ingredient : ${veggies}">
,我们需要注意其中th:each标签
,它会对${}
中的元素集合(veggies)进行遍历,将每个元素都渲染<div>
,每次迭代的时候,都会绑定到名为ingerdient的变量上。<span th:text="${ingredient.name}">INGREDIENT</span>
,其中th:text
需要我们注意一下,它会将ingredient.name
中的值提取并显示出来。<link>引用
,它是对图片和样式的引用,放在static
目录,其中styles.css代码如下
div.ingredient-group:nth-child(odd) {
float: left;
padding-right: 20px;
}
div.ingredient-group:nth-child(even) {
float: left;
padding-right: 0;
}
div.ingredient-group {
width: 50%;
}
.grid:after {
content: "";
display: table;
clear: both;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
span.validationError {
color: red;
}
5. 初步访问结果页面
二、实现表单提交响应
6. 对提交表单的响应
- 当我们对表单进行提交时,需要关注design.html中
<form method="POST" th:object="${design}">
代码,其中请求为POST请求,对象类型为Taco,我们要完成这个响应,就需要在DesignTacoController类
中,编写如下代码
@PostMapping
public String processDesign(Taco design){
//用日志保存提交的表单数据
log.info("Processing design: " + design);
return "redirect:/orders/current";
}
redirect:
实现重定向
,到/orders/current页面
6.1 实现对/orders/current的响应
编写OrdersController
package tacos;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Slf4j
@Controller
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/current")
public String orderForm(Model model){
model.addAttribute("order",new Order());
return "orderForm";
}
}
创建Order实体类
package tacos;
import lombok.Data;
@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;
}
编写orderForm.html
<!-- tag::allButValidation[] -->
<!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="${order}">
<h1>Order your taco creations!</h1>
<img th:src="@{/images/TacoCloud.png}"/>
<a th:href="@{/design}" id="another">Design another taco</a><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}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('name')}"
th:errors="*{name}">Name Error</span>
<!-- tag::allButValidation[] -->
<br/>
<label for="street">Street address: </label>
<input type="text" th:field="*{street}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('street')}"
th:errors="*{street}">Street Error</span>
<!-- tag::allButValidation[] -->
<br/>
<label for="city">City: </label>
<input type="text" th:field="*{city}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('city')}"
th:errors="*{city}">City Error</span>
<!-- tag::allButValidation[] -->
<br/>
<label for="state">State: </label>
<input type="text" th:field="*{state}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('state')}"
th:errors="*{state}">State Error</span>
<!-- tag::allButValidation[] -->
<br/>
<label for="zip">Zip code: </label>
<input type="text" th:field="*{zip}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('zip')}"
th:errors="*{zip}">Zip Error</span>
<!-- tag::allButValidation[] -->
<br/>
<h3>Here's how I'll pay...</h3>
<!-- tag::validatedField[] -->
<label for="ccNumber">Credit Card #: </label>
<input type="text" th:field="*{ccNumber}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('ccNumber')}"
th:errors="*{ccNumber}">CC Num Error</span>
<!-- tag::allButValidation[] -->
<!-- end::validatedField[] -->
<br/>
<label for="ccExpiration">Expiration: </label>
<input type="text" th:field="*{ccExpiration}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('ccExpiration')}"
th:errors="*{ccExpiration}">CC Num Error</span>
<!-- tag::allButValidation[] -->
<br/>
<label for="ccCVV">CVV: </label>
<input type="text" th:field="*{ccCVV}"/>
<!-- end::allButValidation[] -->
<span class="validationError"
th:if="${#fields.hasErrors('ccCVV')}"
th:errors="*{ccCVV}">CC Num Error</span>
<!-- tag::allButValidation[] -->
<br/>
<input type="submit" value="Submit order"/>
</form>
</body>
</html>
<!-- end::allButValidation[] -->
- 该表单中我们需要关注的命令如下,
<form method="POST" th:action="@{/orders}" th:object="${order}">,
对该表单提交后,发起POST请求
,它特殊的地方在于,指定了action
,若不指定action的情况
下,则会返回与该表单相同的URL
上,而当前指定了action
,他则会跳转到“/orders”
上。
而现在我们的OrderController中没有方法实现对POST请求的处理
,我们需要编写处理方法,如下
@PostMapping
public String processOrder(Order order){
//用日志进行保存
log.info("Order submitted: " + order);
return "redirect:/";
}
7. 结果图示
三、数据提交安全验证
为了避免表单数据在提交时被乱输,我们可以在如下三方面进行验证
- 在要被校验的
类
上声明校验规则 - 在
控制器方法
中声明校验 - 修改
表单
,以表示校验错误
8. 在类上进行声明
@Data
public class Taco {
@NotNull
@Size(min = 5,message = "Name Must Be At Least 5 characters long")
private String name;
@NotNull
@Size(min = 1,message = "You must choose at least 1 ingredient")
private List<String> ingredients;
}
@NotNull
:不能让字段为空@Size
:规定字段的长度要求
@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 is required")
private String zip;
@CreditCardNumber(message = "Not a valid credit card number")
private String ccNumber;
private String ccExpiration;
//前者为整数位数,后者为小数位数
@Digits(integer = 3,fraction = 0,message = "Invalid CVV")
private String ccCVV;
}
@NotBlank
:不能为空@CreditCardNumber
:验证它是否为银行卡号@Digits
:规定数字的规范,整数3位,小数0位
9. 在方法上进行声明
在DesignTacoController中
@PostMapping
public String processDesign(@Valid Taco design, Errors errors){
//有错误的情况下,回到当前表单,并在页面上显示错误信息
if(errors.hasErrors()){
return "design";
}
//保存Taco信息
log.info("Processing design: " + design);
//重定向
return "redirect:/orders/current";
}
在OrderController中
@PostMapping
public String processOrder(@Valid Order order, Errors errors){
//有错误的情况下,回到当前表单,并在页面上显示错误信息
if(errors.hasErrors()){
return "orderForm";
}
log.info("Order submitted : " + order);
return "redirect:/";
}
@Valid
:该注解会告诉SpringMVC要对提交的Order对象进行检查,检查的时机
在它绑定完表单数据之后,调用prodessOrder()方法
之前。如果存在错误,则会捕捉到一个Errors
对象,传递给prodessOrder()方法
,通过if(errors.hasErrors())的判断,重新返回”design“,刷新页面,显示错误信息。
10. 结果图示
四、使用视图控制器
11. 使用视图控制器来代替HomeController
我们先将HomeController删除
随后创建如下类
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
- 该控制器实现
只转发视图而不做其他事情
- 实现
WebMvcConfigurer接口
,重写addViewControllers()方法
- 我们利用
ViewControllerRegistry对象
,调用addViewController()方法指定URL
,调用setViewName()方法指定跳转的视图
同样我们可以利用最厉害的TacoCloudApplication来实现这个功能
@SpringBootApplication
public class TacoCloudApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(TacoCloudApplication.class, args);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
这篇儿博客儿写的可读性差了一点儿,我觉得书里的写法也不如第一章清晰,加油儿吧!