“春天里的瑞士军刀”,《Spring实战》(二)

大家好,我是方圆
拿上这把军刀,也不要乱砍呐!


一、实现“做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

要满足如下条件:

  1. 处理“/design”的 HTTP GET请求
  2. 构建配料的列表
  3. 处理请求,传递数据,渲染页面给浏览器
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");
    }
}

这篇儿博客儿写的可读性差了一点儿,我觉得书里的写法也不如第一章清晰,加油儿吧!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方圆想当图灵

嘿嘿,小赏就行,不赏俺也不争你

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值