《spring实战》学习笔记 第二章 开发Web应用

深入研究 Spring MVC 并 会 看到 如何 展现 模型 数据 和 处理 表单 输入。

2. 1 展现信息

关于@Date注解

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
  }

}

Ingredient 类不寻常 的 一点 是它 似乎 缺少 了 常见 的 getter 和 setter 方法, 以及 equals()、 hashCode()、 toString() 等 方法。 没有 这些 方法, 部分 原因 是 节省 空间, 此外 还 因为 我们 使用 了 名为 Lombok 的 库( 这是 一个 非常 棒 的 库, 它 能够 在 运行时 动态 生成 这些 方法)。 实际上, 类 级别 的@ Data 注解 就 是由 Lombok 提供

 

可以 在 pom. xml 中手动加入Lombok库

    <dependency>
    	<groupId>org.projectlombok</groupId>
    	<artifactId>lombok</artifactId>
    	<optional>true</optional> 
    </dependency>

 Lombok 非常 有用, 但 你也 要 知道, 它是 可选 的。 在 开发 Spring 应用 的 时候, 它 并不是 必备 的, 所以 如果 你 不想 使用 它的 话, 完全可以 手动 编写 这些 缺失 的 方法。

 

2. 1. 2 创建 控制器 类

在 Spring MVC 框架 中, 控制器 是 重要的 参与者。 它们 的 主要 职责 是 处理 HTTP 请求, 要么 将 请求 传递 给 视图 以便 于 渲染 HTML( 浏览器 展现), 要么 直接 将 数据 写入 响应 体( RESTful)。

代码案例

// tag::head[]
package tacos.web;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
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 lombok.extern.slf4j.Slf4j;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;

@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {

//end::head[]

@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));
	}
}
	
//tag::showDesignForm[]
  @GetMapping
  public String showDesignForm(Model model) {
    model.addAttribute("design", new Taco());
    return "design";
  }

//end::showDesignForm[]
 

//tag::processDesignValidated[]
  @PostMapping
  public String processDesign(@Valid @ModelAttribute("design") Taco design, Errors errors, Model model) {
    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";
  }

//end::processDesignValidated[]

//tag::filterByType[]
  private List<Ingredient> filterByType(
      List<Ingredient> ingredients, Type type) {
    return ingredients
              .stream()
              .filter(x -> x.getType().equals(type))
              .collect(Collectors.toList());
  }

//end::filterByType[]
// tag::foot[]
}
// end::foot[]

 

注解说明

@Slf4j是 Lombok 所 提供 的 注解, 在 运行时, 它 会在 这个 类 中 自动 生成 一个 SLF4J( Simple Logging Facade for Java) Logger。 这个 简单 的 注解 和在 类 中 通过 如下 代码 显 式 声明 的 效果 是 一样 的:

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger( DesignTacoController.class);

@ Controller, 这个 注解 会 将 这个 类 识别 为 控制器, 并且 将其 作为 组件 扫描 的 候选 者,

@ RequestMapping 注解, 当@ RequestMapping 注解 用到 类 级别 的 时候, 它 能够 指定 该 控制器 所 处理 的 请求 类型。例如将处理 路径 以“/ design” 开头 的 请求。

 

处理 GET 请求

@GetMapping 注 解对 类 级别 的@ RequestMapping 进行 了 细化。@ GetMapping 结合 类 级别 的@ RequestMapping, 指明 当接 收到 对“/ design” 的 HTTP GET 请求 时, 将会 调用 showDesignForm() 来处 理 请求。

@GetMapping 是一 个 相对 较 新的 注解, 是在 Spring 4. 3 引入 的。 在 Spring 4. 3 之前, 你 可能 需要 使用方法 级别 的@ RequestMapping 注解 作为 替代:

@RequestMapping( method= RequestMethod. GET)

  Spring MVC 的 请求 映射 注解

  • @RequestMapping 通用 的 请求 处理
  • @GetMapping 处理 HTTP GET 请求
  • @PostMapping 处理 HTTP POST 请求
  • @PutMapping 处理 HTTP PUT 请求
  • @DeleteMapping 处理 HTTP DELETE 请求
  • @PatchMapping 处理 HTTP PATCH 请求

 

设计视图

showDesignForm() 这个案例方法 最后 返回“ design”, 这是 视图 的 逻辑 名称, 会 用来 将 模型 渲染 到 视图 上。

在 控制器 完成 它的 工作 之后, 现在 就 该 视图 登场 了。 Spring 提供 了 多种 定义 视图 的 方式, 包括 JavaServer Pages( JSP)、 Thymeleaf、 FreeMarker、 Mustache 和 基于 Groovy 的 模板。

本案例中 使用 Thymeleaf, 我们 需要 添加 另外 一个 依赖 到 项目 构建 中。

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

在 运行时, Spring Boot 的 自动 配置 功能 会 发现 Thymeleaf 在 类 路径 中, 因此 会为 Spring MVC 创建 支撑 Thymeleaf 视图 的 bean。

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 命名 空间 中的 属性, 它 会 执行 这个 替换 过程。${} 会 告诉 它要 使用 某个 请求 属性 

Thymeleaf 还 提供 了 一个 属性“ th: each”, 它 会 迭代 一个 元素 集合, 为 集合 中的 每个 条目 渲染 HTML。

< input> 复选框 元素, 还有 一个 为 复选框 提供 标签 的< span> 元素。 复选框 使用 Thymeleaf 的 th: value 来 为 渲染 出 的< input> 元素 设置 value 属性 。类似的还有th: text 。

值得注意 的 是,在 Spring Boot 应用 中, 静态 内容 要 放到 根 路径 的“/ static” 目录 下。然后使用Thymeleaf 的@{} 操作 符, 用来 生成 一个 相对 上下文 的 路径, 以便 于 引用 我们 需要 的 静态 制件( artifact)。

 

2. 2 处理 表单 提交

案例代码

   <form method="POST" th:object="${design}">

    <span class="validationError"
          th:if="${#fields.hasErrors('ingredients')}"
          th:errors="*{ingredients}">Ingredient Error</span>

    <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}">INGREDIENT</span><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}">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}"/>
      <span th:text="${#fields.hasErrors('name')}">XXX</span>
      <span class="validationError"
            th:if="${#fields.hasErrors('name')}"
            th:errors="*{name}">Name Error</span>
      <br/>

      <button>Submit your taco</button>
      </div>
    </form>

仔细 看一下 视图 中的< form> 标签, 你将 会 发现 它的 method 属性 被 设置 成了 POST。 除此之外,< form> 并没有 声明 action 属性。 这 意味着 当 表单 提交 的 时候, 浏览器 会 收集 表单 中的 所有 数据, 并以 HTTP POST 请求 的 形式 将其 发送 至 服务器 端, 发送 路径 与 渲染 表单 的 GET 请求 路径 相同, 也就是“/ design”。

因此, 在 该 POST 请求 的 接收 端, 我们 需要 有一个 控制器 处理 方法。 在 DesignTacoController 中, 我们 会 编写 一个 新的 处理器 方法 来处 理 针对“/ design” 的 POST 请求。

  使用@ PostMapping 来 处理 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 对象 的 属性 中, 该 对象 会 以 参数 的 形式 传递 给 processDesign()。 从这 里 开始, processDesign() 就可以 针对 Taco 对象 采取 任意 操作 了。

 

2. 3 校验 表单 输入

有种 校验 方法 就是 在 processDesign() 和 processOrder() 方法 中 添加 大量 乱七八糟 的 if/ then 代码 块, 逐个 检查, 确保 每个 输入 域 都 满足 对应 的 校验 规则。 但是, 这样 会 非常 烦琐, 并且 难以 阅读 和 调试。

幸运 的 是, Spring 支持 Java 的 Bean 校验 API( Bean Validation API, 也 被称为 JSR- 303)。 这样 的 话, 我们 能够 更容易 地 声明 检验 规则, 而 不必 在 应用 程序 代码 中 显 式 编写 声明 逻辑。

要在 Spring MVC 中 应用 校验, 我们 需要。

  • 在要 被 校验 的 类 上声 明 校验 规则: 具体 到 我们 的 场景 中, 也就是 Taco 类。
  • 在 控制器 方法 中 声明 要 进行 校验: 具体 来讲, 也就是 DesignTacoController 的 processDesign() 方法 和 OrderController 的 processOrder() 方法。
  • 修改 表单 视图 以 展现 校验 错误。

 

2. 3. 1   声明 校验 规则

  • @NotBlank 注解:用户 没有 提交 空白 字段。
  • @ CreditCardNumber 注解。 这个 注解 声明 该 属 性的 值 必须 是 合法 的 信用卡 号, 它要 能 通过 Luhn 算法 的 检查。 这 能 防止 用户 有意 或 无意 地 输入 错误 的 数据, 但是 该 检查 并不能 确保 这个 信用卡 号 真的 分配 给了 某个 账户, 也不能 保证 这个 账号 能够 用来 进行 支付。
  • @Pattern 注解:正则表达式
  • @ Digits 注解, 能够 确保 它的 值 包含 3 位 数字。

 

所有 的 校验 注解 都 包含 了 一个 message 属性, 该 属性 定义 了 当 输入 的 信息 不满足 声明 的 校验 规则 时 要给 用户 展现 的 消息。

 

2. 3. 2 在 表单 绑 定的 时候 执行 校验

 

在Controller控制器中对应方法中的参数加入@Valid注解,就可以启动校验器,

案例代码:

  @PostMapping
  public String processDesign(@Valid @ModelAttribute("design") Taco design, Errors errors, Model model) {
    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";
  }

@Valid 注解 会 告诉 Spring MVC 要对 提交 的 Taco 对象 进行 校验, 而 校验 时机 是在 它 绑 定 完 表单 数据 之后、 调用 processDesign() 之前。 如果 存在 校验 错误, 那么 这些 错误 的 细节 将会 捕获 到 一个 Errors 对象 中 并 传递 给 processDesign()。 processDesign() 方法 的 前 几 行会 查阅 Errors 对象, 调用 其 hasErrors() 方法 判断 是否 有 校验 错误。 如果 存在 校验 错误, 那么 这个 方法 将不 会 处理 Taco 对象 并 返回“ design” 视图 名, 表单 会 重新 展现。

为了 对 提交 的 Order 对象 进行 校验, OrderController 的 processOrder() 方法 也需 要 进行 类似 的 变更,

 

  @PostMapping
  public String processOrder(@Valid Order order, Errors errors) {
    if (errors.hasErrors()) {
      return "orderForm";
    }
    
    log.info("Order submitted: " + order);
    return "redirect:/";
  }

2. 3. 3 展现 校验 错误

Thymeleaf 提供 了 便捷 访问 Errors 对象 的 方法, 这就 是 借助 fields 及其 th: errors 属性。 举例 来说, 为了 展现 信用卡 字段 的 校验 错误, 我们 可以 添加 一个< span> 元素, 该 元素 会 将对 错误 的 引用 用到 订单 的 表单 模板 上,

代码如下

      <span th:text="${#fields.hasErrors('name')}">XXX</span>
      <span class="validationError"
            th:if="${#fields.hasErrors('name')}"
            th:errors="*{name}">Name Error</span>
      <br/>

< span> 元素 使用 class 属性 来 为 错误 添加 样式, 以 引起 用户 的 注意。

th: if 属性 来 决定 是否 要 显示 该 元素。

fields 属性 的 hasErrors() 方法 会 检查 ccNumber 域 是否 存在 错误, 如果 存在,就 将会 渲染< span>。

th: errors 属性 引用 了 ccNumber 输入 域, 如果 该 输入 域 存在 错误, 那么 它 会 将< span> 元素 的 占位符 内容 替换 为 校验 信息。

 

2. 4 使用视图控制器

视图 控制器: 也就是 只 将 请求 转发 到 视图 而 不做 其他 事情 的 控制器。

案例代码:

package tacos.web;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("home");
  }

}

 

WebMvcConfigurer 接口: WebMvcConfigurer 定义 了 多个 方法 来 配置 Spring MVC。 尽管 只是 一个 接口, 但是 它 提供 了 所有 方法 的 默认 实现, 只需 要 覆盖 所需 的 方法 即可。

addViewControllers() 方法 会 接收 一个 ViewControllerRegistry 对象, 我们 可以 使用 它 注册 一个 或 多个 视图 控制器。 在这里, 我们 调用 registry 的 addViewController() 方法, 将“/” 传递 了 进去, 视图 控制器 将会 针对 该 路径 执行 GET 请求。 这个 方法 会 返回 ViewControllerRegistration 对象, 我们 马上 基于 该 对象 调用 了 setViewName() 方法, 用 它 指明 当 请求“/” 的 时候 要 转发 到“ home” 视图 上。

 

2. 5 选择 视图 模板 库

Spring 非常 灵活, 能够 支持 很多 常见 的 模板 方案。通常需要配置。

 

JSP 并不 需要 在 构建 文件 中 添加 任何 特殊 的 依赖。 这是 因为 Servlet 容器 本身( 默认 是 Tomcat) 会 实现 JSP, 因此 不需要 额外 的 依赖。 但是, 如果 你 选择 使用 JSP, 会有 另外 一个 问题。 事实上, Java Servlet 容器 包括 嵌入 式 的Tomcat 和 Jetty 容器, 通常 会在“/ WEB- INF” 目录 下 寻找 JSP。 如果 我们将 应用 构建 成 一个 可执行 的 JAR 文件, 就 无法 满足 这种 需求 了。 因此, 只有 在 将 应用 构建 为 WAR 文件 并 部署 到 传统 的 Servlet 容器 中 时, 才能 选择 JSP 方案。 如果 你想 要 构建 可执行 的 JAR 文件, 那么 必须 选择 Thymeleaf、 FreeMarker 或 其他 方案。

缓存模板

启用/ 禁用 模板 缓存 的 属性

  • FreeMarker 属性: spring. freemarker. cache
  • Groovy 属性: Templates spring. groovy. template. cache
  • Mustache 属性: spring. mustache. cache
  • Thymeleaf 属性: spring. thymeleaf. cache

默认 情况下, 这些 属性 都 设置 成了 true, 以便 于 启用 缓存。 我们 可以 将 缓存 属性 设置 为 false, 从而 禁用 所选 模板 引擎 的 缓存。 例如, 要 禁用 Thymeleaf 缓存, 我们 只需 要在 application. properties 中 添加 如下 这 行 代码:

spring.thymeleaf.cache = false

唯一 需要 注意 的 是, 在 将 应用 部署 到 生产 环境 之前, 一定 要 删除 这一 行 代码( 或者 将其 设置 为 true)。

 

2. 6   小结

  • Spring 提供 了 一个 强大 的 Web 框架, 名为 Spring MVC, 能够 用来 为 Spring 应用 开发 Web 前端。
  • Spring MVC 是 基于 注解 的, 通过 像@ RequestMapping、@ GetMapping 和@ PostMapping 这样 的 注解 来 启用 请求 处理 方法 的 声明。
  • 大多数 的 请求 处理 方法 最终 会 返回 一个 视图 的 逻辑 名称, 比如 Thymeleaf 模板, 请求 会 转发 到 这样 的 视图 上( 同时 会 带有 任意 的 模型 数据)。
  • Spring MVC 支持 校验, 这是 通过 Java Bean Validation API 和 Validation API 的 实现( 如 Hibernate Validator) 完成 的。
  • 对于 没有 模型 数据 和 逻辑 处理 的 HTTP GET 请求, 可以 使用 视图 控制器。
  • 除了 Thymeleaf 之外, Spring 支持 各种 视图 方案, 包括 FreeMarker、 Groovy Templates 和 Mustache。

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值