SpringBoot2 高级教程(二)
四、Spring Boot 的网络应用
如今,web 是任何类型应用的主要渠道,从桌面到移动设备,从社交和商业应用到游戏,从简单内容到流数据。有了这个想法,Spring Boot 可以帮助您轻松开发下一代网络应用。
本章向您展示了如何轻松创建 Spring Boot web 应用。通过前面章节中的一些例子,你已经了解了你可以用网络做什么。您了解了 Spring Boot 使得用几行代码创建 web 应用变得更加容易,并且您不需要担心配置文件或寻找应用服务器来部署您的 web 应用。通过使用 Spring Boot 及其自动配置,您可以拥有一个嵌入式应用服务器,如 Tomcat、Netty、Undertow 或 Jetty,这使得您的应用非常易于分发和移植。
的实现
让我们开始谈论 Spring MVC 技术和它的一些特性。请记住,Spring 框架由大约 20 个模块或技术组成,web 技术是其中之一。对于 web 技术,Spring 框架有spring-web、spring-webmvc、spring-webflux和spring-websocket模块。
spring-web模块具有基本的 web 集成特性,比如多部分文件上传功能、Spring 容器的初始化(通过使用 servlet 监听器)以及面向 web 的应用上下文。spring-mvc模块(也称为 web 服务器模块)包含 web 应用的所有 Spring MVC(模型-视图-控制器)和 REST 服务实现。这些模块提供了许多特性,比如非常强大的 JSP 标记库、可定制的绑定和验证、灵活的模型传输、可定制的处理程序和视图解析等等。
Spring MVC 是围绕org.springframework.web.servlet.DispatcherServlet类设计的。这个 servlet 非常灵活,并且具有非常健壮的功能,这在其他任何 MVC web 框架中都是找不到的。使用DispatcherServlet,您有几个现成的解析策略,包括视图解析器、区域解析器、主题解析器和异常处理程序。换句话说,DispatcherServlet接受一个 HTTP 请求,并将其重定向到正确的处理程序(用@Controller或@RestController标记的类以及使用@RequestMapping注释的方法)和正确的视图(您的 JSP)。
Spring Boot MVC 自动配置
通过将spring-boot-starter-web依赖项添加到您的pom.xml或build.gradle文件中,可以很容易地创建 Web 应用。这个依赖项提供了所有必需的spring-webjar 和一些额外的 jar,比如tomcat-embed*和jackson(用于 JSON 和 XML)。这意味着 Spring Boot 使用 Spring MVC 模块的能力,并提供所有必要的自动配置来创建正确的 web 基础设施,例如配置DispatcherServlet,提供默认值(除非您覆盖它),设置嵌入式 Tomcat 服务器(这样您就可以在没有任何应用容器的情况下运行您的应用),等等。
自动配置为您的 web 应用添加了以下功能。
-
静态内容支持。这意味着您可以在名为
/static(默认情况下)或/public、/resources或/META-INF/resources的目录中添加静态内容,比如 HTML、JavaScript、CSS、media 等等,这些目录应该在您的类路径或当前目录中。Spring Boot 拿起它,并应要求为他们服务。您可以通过修改spring.mvc.static-path-pattern或spring.resources.static-locations属性来轻松改变这一点。Spring Boot 和 web 应用的一个很酷的特性是,如果你创建一个index.html文件,Spring Boot 会自动提供它,不需要注册任何其他 bean,也不需要额外的配置。 -
HttpMessageConverters 。如果您正在使用常规的 Spring MVC 应用,并且想要获得 JSON 响应,那么您需要为
HttpMessageConvertersbean 创建必要的配置(XML 或 JavaConfig)。Spring Boot 默认添加了这种支持,所以你不必这么做;这意味着默认情况下您会得到 JSON 格式(由于 Jackson 库,spring-boot-starter-web提供了依赖项)。如果 Spring Boot 自动配置发现您的类路径中有 Jackson XML 扩展,它会将一个 XMLHttpMessageConverter聚合到转换器,这意味着您的应用可以基于您的content-type请求,或者是application/json或者是application/xml提供服务。 -
JSON 序列化器和反序列化器。如果你想对 JSON 的序列化/反序列化有更多的控制,Spring Boot 提供了一个简单的方法来创建你自己的,从
JsonSerializer<T>和/或JsonDeserializer<T>扩展,用@JsonComponent注释你的类,这样它就可以注册使用了。Spring Boot 的另一个特点是杰克逊的支持;默认情况下,Spring Boot 将日期字段序列化为2018-05-01T23:31:38.141+0000,但是您可以通过更改spring.jackson.date-format=yyyy-MM-dd属性来更改这种默认行为(您可以应用任何日期格式模式);前一个值生成输出,如2018-05-01。 -
路径匹配和内容协商。Spring MVC 应用实践之一是能够响应任何后缀来表示内容类型响应及其内容协商。如果你有类似
/api/todo.json或者/api/todo.pdf的东西,那么content-type设置为application/json和application/pdf;所以响应分别是 JSON 格式或 PDF 文件。换句话说,Spring MVC 进行.*后缀模式匹配,比如/api/todo.*。默认情况下,Spring Boot 禁用此功能。通过使用spring.mvc.contentnegotiation.favor-parameter=true属性(默认为false,您仍然可以使用一个可以添加参数的特性;所以你可以做一些类似/api/todo?format=xml的事情。(format是默认的参数名;当然可以用spring.mvc.contentnegotiation.parameter-name=myparam来改。这将触发content-type至application/xml。 -
错误处理。Spring Boot 使用
/error映射创建一个白色标签页面来显示所有的全局错误。您可以通过创建自己的自定义页面来更改行为。您需要在src/main/resources/public/error/位置创建您的定制 HTML 页面,这样您就可以创建500.html或404.html页面。如果你正在创建一个 RESTful 应用,Spring Boot 以 JSON 格式响应。Spring Boot 还支持 Spring MVC 来处理使用@ControllerAdvice或@ExceptionHandler注释时的错误。您可以通过实现ErrorPageRegistrar并将其声明为 Spring bean 来注册自定义ErrorPages。 -
模板引擎 支持。Spring Boot 支持 FreeMarker,Groovy 模板,百里香叶和小胡子。当包含
spring-boot-starter-<template engine>依赖项时,Spring Boot 自动配置对于启用和添加所有必要的视图解析器和文件处理程序是必要的。默认情况下,Spring Boot 会查看src/main/resources/templates/路径。
还有许多其他功能,Spring Boot 网站自动配置提供。现在,我们只关注了 Servlet 技术,但是很快我们就会进入 Spring Boot 家族的最新成员: WebFlux 。
Spring Boot Web:所有应用
为了更好地理解 Spring Boot 如何使用 web 应用以及 Spring MVC 模块的强大功能,您将创建一个 ToDo 应用来公开一个 RESTful API。这些是要求:
-
创建具有以下字段和类型的 ToDo 域模型:
id(字符串)、description(字符串)、completed(布尔)、created(日期和时间)、modified(日期和时间)。 -
创建一个 RESTful API,提供基本的 CRUD(创建、读取、更新、删除)操作。使用最常见的 HTTP 方法:POST、PUT、PATCH、GET 和 DELETE。
-
创建一个存储库来处理多个待办事项的状态。目前,内存中的存储库就足够了。
-
当有错误请求或提交新的 ToDo 没有必需字段时,添加错误处理程序。唯一的必填字段是描述。
-
所有的请求和响应都应该是 JSON 格式。
所有应用
打开你的浏览器,进入 https://start.spring.io ,使用以下值创建你的待办事宜应用(另见图 4-1 )。
图 4-1
-
组:
com.apress.todo -
神器:
todo-in-memory -
名称:
todo-in-memory -
包名:
com.apress.todo -
依赖关系:
Web,Lombok
选择 Lombok 依赖项有助于轻松创建域模型类,并消除样板文件 setters、getters 和其他覆盖。
注意
如果您需要更多关于龙目岛的信息,请参见 https://projectlombok.org 的参考文件。
您可以选择 Maven 或 Gradle 作为项目类型;在本书中,我们不加区分地使用这两个词。按下 Generate Project 按钮并下载 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。一些最好的 ide 是 STS ( https://spring.io/tools/sts/all )、IntelliJ IDEA ( www.jetbrains.com/idea/ )和 VSCode ( https://code.visualstudio.com/ )。我为代码完成特性推荐这些 ide 中的一个,它可以帮助您看到要添加到代码中的方法或参数。
域模型:ToDo
根据需求,您需要创建一个ToDo域模型类(参见清单 4-1 )。
package com.apress.todo.domain;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
public class ToDo {
@NotNull
private String id;
@NotNull
@NotBlank
private String description;
private LocalDateTime created;
private LocalDateTime modified;
private boolean completed;
public ToDo(){
LocalDateTime date = LocalDateTime.now();
this.id = UUID.randomUUID().toString();
this.created = date;
this.modified = date;
}
public ToDo(String description){
this();
this.description = description;
}
}
Listing 4-1com.apress.todo.domain.ToDo.java
清单 4-1 向您展示了ToDo类,它拥有所有必需的字段。它还使用了@Data注释,这是一个 Lombok 注释,生成一个默认的构造函数(如果你没有的话)和所有的设置器、获取器和覆盖,比如toString方法,使类更干净。还要注意,该类在一些字段中有@NotNull和@NotBlank注释;这些注释用于我们稍后进行的验证。默认的构造函数有字段初始化,所以很容易创建一个ToDo实例。
流畅的 API: ToDoBuilder
接下来,让我们创建一个流畅的 API 类来帮助创建一个ToDo实例。您可以看到这个类是一个工厂,它创建一个带有描述或特定 ID 的 ToDo(参见清单 4-2 )。
package com.apress.todo.domain;
public class ToDoBuilder {
private static ToDoBuilder instance = new ToDoBuilder();
private String id = null;
private String description = "";
private ToDoBuilder(){}
public static ToDoBuilder create() {
return instance;
}
public ToDoBuilder withDescription(String description){
this.description = description;
return instance;
}
public ToDoBuilder withId(String id){
this.id = id;
return instance;
}
public ToDo build(){
ToDo result = new ToDo(this.description);
if(id != null)
result.setId(id);
return result;
}
}
Listing 4-2com.apress.todo.domain.ToDoBuilder.java
清单 4-2 是一个简单的工厂类,它创建了一个ToDo实例。您将在接下来的章节中扩展它的功能。
存储库:公共存储库
接下来,创建一个具有通用持久性操作的接口。这个接口是通用的,所以很容易使用任何其他实现,使回购成为一个可扩展的解决方案(参见清单 4-3 )。
package com.apress.todo.repository;
import java.util.Collection;
public interface CommonRepository<T> {
public T save(T domain);
public Iterable<T> save(Collection<T> domains);
public void delete(T domain);
public T findById(String id);
public Iterable<T> findAll();
}
Listing 4-3com.apress.todo.repository.CommonRepository<T>.java
清单 4-3 是一个通用接口,可以用作任何其他持久性实现的基础。当然,您可以随时更改这些签名。这只是一个如何创建可扩展的东西的例子。
存储库:ToDoRepository
让我们创建一个实现CommonRepository<T>接口的具体类。记住规范;现在,只需要在内存中保存待办事项(参见清单 4-4 )。
package com.apress.todo.repository;
import com.apress.todo.domain.ToDo;
import org.springframework.stereotype.Repository;
@Repository
public class ToDoRepository implements CommonRepository<ToDo> {
private Map<String,ToDo> toDos = new HashMap<>();
@Override
public ToDo save(ToDo domain) {
ToDo result = toDos.get(domain.getId());
if(result != null) {
result.setModified(LocalDateTime.now());
result.setDescription(domain.getDescription());
result.setCompleted(domain.isCompleted());
domain = result;
}
toDos.put(domain.getId(), domain);
return toDos.get(domain.getId());
}
@Override
public Iterable<ToDo> save(Collection<ToDo> domains) {
domains.forEach(this::save);
return findAll();
}
@Override
public void delete(ToDo domain) {
toDos.remove(domain.getId());
}
@Override
public ToDo findById(String id) {
return toDos.get(id);
}
@Override
public Iterable<ToDo> findAll() {
return toDos.entrySet().stream().sorted(entryComparator).map(Map.Entry::getValue).collect(Collectors.toList());
}
private Comparator<Map.Entry<String,ToDo>> entryComparator = (Map.Entry<String, ToDo> o1, Map.Entry<String, ToDo> o2) -> {
return o1.getValue().getCreated().compareTo(o2.getValue().getCreated());
};
}
Listing 4-4com.apress.todo.repository.ToDoRepository.java
清单 4-4 展示了CommonRepository<T>接口的实现。审查代码并进行分析。此类正在使用保存所有待办事项的哈希。由于 hash 的性质,所有的操作都得到了简化,使其易于实现。
验证:ToDoValidationError
接下来,让我们创建一个验证类,暴露应用中任何可能的错误,比如没有描述的 ToDo。记住,在ToDo类中,ID 和描述字段被标记为@NotNull。描述字段有一个额外的@NotBlank注释,以确保它从不为空(参见清单 4-5 )。
package com.apress.todo.validation;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.ArrayList;
import java.util.List;
public class ToDoValidationError {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<String> errors = new ArrayList<>();
private final String errorMessage;
public ToDoValidationError(String errorMessage) {
this.errorMessage = errorMessage;
}
public void addValidationError(String error) {
errors.add(error);
}
public List<String> getErrors() {
return errors;
}
public String getErrorMessage() {
return errorMessage;
}
}
Listing 4-5com.apress.todo.validation.ToDoValidationError.java
清单 4-5 显示了ToDoValidationError类,它保存任何请求产生的任何错误。它使用了一个额外的@JsonInclude注释,这意味着即使 errors 字段为空,也必须包含它。
验证:ToDoValidationErrorBuilder
让我们创建另一个帮助构建ToDoValidationError实例的工厂(参见清单 4-6 )。
package com.apress.todo.validation;
import org.springframework.validation.Errors;
import org.springframework.validation.ObjectError;
public class ToDoValidationErrorBuilder {
public static ToDoValidationError fromBindingErrors(Errors errors) {
ToDoValidationError error = new ToDoValidationError("Validation failed. " + errors.getErrorCount() + " error(s)");
for (ObjectError objectError : errors.getAllErrors()) {
error.addValidationError(objectError.getDefaultMessage());
}
return error;
}
}
Listing 4-6com.apress.todo.validation.ToDoValidationErrorBuilder.java
清单 4-6 是另一个Factory类,它可以用所有必要的信息轻松创建一个ToDoValidationError实例。
控制器:ToDoController
现在,是时候创建 RESTful API 并使用所有以前的类了。您创建了ToDoController类,在其中您可以看到所有的 Spring MVC 特性、注释、配置端点的方式以及如何处理错误。让我们回顾一下清单 4-7 中的代码。
package com.apress.todo.controller;
import com.apress.todo.domain.ToDo;
import com.apress.todo.domain.ToDoBuilder;
import com.apress.todo.repository.CommonRepository;
import com.apress.todo.validation.ToDoValidationError;
import com.apress.todo.validation.ToDoValidationErrorBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
@RestController
@RequestMapping("/api")
public class ToDoController {
private CommonRepository<ToDo> repository;
@Autowired
public ToDoController(CommonRepository<ToDo> repository) {
this. repository = repository;
}
@GetMapping("/todo")
public ResponseEntity<Iterable<ToDo>> getToDos(){
return ResponseEntity.ok(repository.findAll());
}
@GetMapping("/todo/{id}")
public ResponseEntity<ToDo> getToDoById(@PathVariable String id){
return ResponseEntity.ok(repository.findById(id));
}
@PatchMapping("/todo/{id}")
public ResponseEntity<ToDo> setCompleted(@PathVariable String id){
ToDo result = repository.findById(id);
result.setCompleted(true);
repository.save(result);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.buildAndExpand(result.getId()).toUri();
return ResponseEntity.ok().header("Location",location.toString()).build();
}
@RequestMapping(value="/todo", method = {RequestMethod.POST,RequestMethod.PUT})
public ResponseEntity<?> createToDo(@Valid @RequestBody ToDo toDo, Errors errors){
if (errors.hasErrors()) {
return ResponseEntity.badRequest().body(ToDoValidationErrorBuilder.fromBindingErrors(errors));
}
ToDo result = repository.save(toDo);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(result.getId()).toUri();
return ResponseEntity.created(location).build();
}
@DeleteMapping("/todo/{id}")
public ResponseEntity<ToDo> deleteToDo(@PathVariable String id){
repository.delete(ToDoBuilder.create().withId(id).build());
return ResponseEntity.noContent().build();
}
@DeleteMapping("/todo")
public ResponseEntity<ToDo> deleteToDo(@RequestBody ToDo toDo){
repository.delete(toDo);
return ResponseEntity.noContent().build();
}
@ExceptionHandler
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ToDoValidationError handleException(Exception exception) {
return new ToDoValidationError(exception.getMessage());
}
}
Listing 4-7com.apress.todo.controller.ToDoController.java
清单 4-7 是ToDoController类。我们来复习一下。
-
@RestController。Spring MVC 提供了@Controller和@RestController来表达请求映射、请求输入、异常处理等等。所有的功能都依赖于这些注释,因此不需要扩展或实现接口特定的接口。 -
@RequestMapping。这个注释将请求映射到控制器方法。有几个属性来匹配 URL、HTTP 方法(GET、PUT、DELETE 等。)、请求参数、报头和媒体类型。它可以在类级(共享映射)使用,也可以在方法级用于特定端点映射。在这种情况下,用"/api"标记,意味着所有的方法都有这个前缀。 -
@Autowired。构造函数用@Autowired进行了注释,这意味着它注入了CommonRepository<ToDo>实现。此注释可以省略;从版本 4.3 开始,Spring 会自动注入任何声明的依赖项。 -
@GetMapping。这是@RequestMapping注释的快捷变体,对HTTP GET方法有用。@GetMapping相当于@RequestMapping(value="/todo", method = {RequestMethod.GET})。 -
@PatchMapping。这是@RequestMapping注释的快捷方式变体;在这个类中,它将 ToDo 标记为已完成。 -
@DeleteMapping。这是@RequestMapping注释的快捷方式变体;它用于删除待办事项。有两个重载方法:deleteToDo,一个接受字符串,另一个接受ToDo实例。 -
@PathVariable。当您声明包含 URL 表达式模式的终结点时,此批注非常有用;在本例中是"/api/todo/{id}",其中 ID 必须与方法参数的名称相匹配。 -
@RequestBody。这个注释发送一个带有主体的请求。通常,当你提交一个表单或一个特定的内容时,这个类接收一个 JSON 格式的ToDo,然后HttpMessageConverter将 JSON 反序列化成一个ToDo实例;多亏了 Spring Boot 和它的自动配置,这是自动完成的,因为它默认注册了MappingJackson2HttpMessageConverter。 -
ResponseEntity<T>。这个类返回一个完整的响应,包括 HTTP 头,主体通过HttpMessageConverters转换并写入 HTTP 响应。ResponseEntity<T>类支持流畅的 API,因此创建响应很容易。 -
@ResponseStatus。通常,当方法具有 void 返回类型(或 null 返回值)时,使用此注释。该注释发回响应中指定的 HTTP 状态代码。 -
@Valid。该注释验证传入的数据,并用作方法的参数。要触发一个验证器,需要用@NotNull、@NotBlank和其他注释来注释您想要验证的数据。在这种情况下,ToDo类在 ID 和 description 字段中使用这些注释。如果验证器发现错误,它们被收集在Errors类中(在本例中,一个spring-webmvcjars 附带的 hibernate 验证器被注册并用作全局验证器;您可以创建自己的自定义验证并覆盖 Spring Boot 的默认值)。然后,您可以检查并添加必要的逻辑来发回错误响应。 -
@ExceptionHandler。Spring MVC 自动声明内置的异常解析器,并为该注释添加支持。在这种情况下,@ExceptionHandler在这个控制器类中声明(或者您可以在@ControllerAdvice拦截器中使用它),任何异常都被重定向到handleException方法。如果需要,您可以说得更具体一些。例如,你可以有一个DataAccessException,并通过一个方法来处理。@ExceptionHandler @ResponseStatus(value = HttpStatus.BAD_REQUEST) public ToDoValidationError handleException(DataAccessException exception) { return new ToDoValidationError(exception.getMessage()); }
在类中有一个方法接受两个 HTTP 方法:POST和PUT。@RequestMapping可以接受多个 HTTP 方法,因此很容易分配一个方法来处理它们(例如@RequestMapping(value="/todo", method = {RequestMethod.POST, RequestMethod.PUT}))。
我们已经涵盖了这个应用的所有必要需求,所以是时候运行它并查看结果了。
运行:待办事项应用
现在,您已经准备好运行 ToDo 应用并测试它了。如果您使用的是 IDE (STS 或 IntelliJ),可以右键单击主应用类(TodoInMemoryApplication.java)并选择运行操作。如果您使用的编辑器没有这些功能,您可以通过打开终端窗口并执行清单 4-8 或清单 4-9 中的命令来运行您的 Todo 应用。
./gradlew spring-boot:run
Listing 4-9If you are using Gradle project type
./mvnw spring-boot:run
Listing 4-8If you are using Maven project type
Spring Initializr ( https://start.spring.io )总是提供你选择的项目类型包装器(Maven 或者 Gradle 包装器),所以不需要预装 Maven 或者 Gradle。
Spring Boot web 应用的一个默认设置是,它配置了一个嵌入式 Tomcat 服务器,因此您可以轻松运行您的应用,而无需将其部署到应用 servlet 容器中。默认情况下,它选择端口 8080。
测试:待办事项应用
测试 ToDo 应用应该非常简单。这种测试是通过命令或特定的客户端进行的。如果你正在考虑单元测试或集成测试,我会在另一章解释。这里我们将使用 cURL 命令。默认情况下,这个命令在任何 UNIX 操作系统中都有,但是如果你是 Windows 用户,你可以从 https://curl.haxx.se/download.html 下载它。
第一次运行时,待办事项应用不应该有任何待办事项。您可以通过在另一个终端中执行以下命令来确保这一点。
curl -i http://localhost:8080/api/todo
您应该会看到类似于以下输出的内容:
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 02 May 2018 22:10:19 GMT
[]
你的目标是/api/todo端点,如果你看一下清单 4-7 ,一个getToDos方法返回ResponseEntity<Iterable<ToDo>>,这是 ToDo 的集合。默认响应是 JSON 格式(见Content-Type头)。响应是发回 HTTP 头和状态。
接下来,让我们用下面的命令添加一些 ToDo。
curl -i -X POST -H "Content-Type: application/json" -d '{ "description":"Read the Pro Spring Boot 2nd Edition Book"}' http://localhost:8080/api/todo
在命令中,post ( -X POST)和 data ( -d)是 JSON 格式。您只发送描述字段。需要添加具有正确内容类型的头(-H),并指向/api/todo端点。执行该命令后,您应该会看到如下输出:
HTTP/1.1 201
Location: http://localhost:8080/api/todo/d8d37c51-10a8-4c82-a7b1-b72b5301cdab
Content-Length: 0
Date: Wed, 02 May 2018 22:21:09 GMT
您获得了位置头,在那里 ToDo 被读取。Location显示您刚刚创建的待办事项的 ID。这个响应是由createToDo方法生成的。添加至少另外两个待办事项,以便我们可以有更多的数据。
如果您再次执行第一个命令来获得所有的 ToDo,您应该会看到类似这样的内容:
curl -i http://localhost:8080/api/todo
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 02 May 2018 22:30:16 GMT
[{"id":"d8d37c51-10a8-4c82-a7b1-b72b5301cdab","description":"Read the Pro Spring Boot 2nd Edition Book","created":"2018-05-02T22:27:26.042+0000","modified":"2018-05-02T22:27:26.042+0000","completed":false},{"id":"fbb20090-19f5-4abc-a8a9-92718c2c4759","description":"Bring Milk after work","created":"2018-05-02T22:28:23.249+0000","modified":"2018-05-02T22:28:23.249+0000","completed":false},{"id":"2d051b67-7716-4ee6-9c45-1de939fa579f","description":"Take the dog for a walk","created":"2018-05-02T22:29:28.319+0000","modified":"2018-05-02T22:29:28.319+0000","completed":false}]
当然,这打印得不是很好,但是你有完整的待办事项列表。您可以使用另一个命令行工具来格式化这个输出:jq ( https://stedolan.github.io/jq/ )。
curl -s http://localhost:8080/api/todo | jq
[
{
"id": "d8d37c51-10a8-4c82-a7b1-b72b5301cdab",
"description": "Read the Pro Spring Boot 2nd Edition Book",
"created": "2018-05-02T22:27:26.042+0000",
"modified": "2018-05-02T22:27:26.042+0000",
"completed": false
},
{
"id": "fbb20090-19f5-4abc-a8a9-92718c2c4759",
"description": "Bring Milk after work",
"created": "2018-05-02T22:28:23.249+0000",
"modified": "2018-05-02T22:28:23.249+0000",
"completed": false
},
{
"id": "2d051b67-7716-4ee6-9c45-1de939fa579f",
"description": "Take the dog for a walk",
"created": "2018-05-02T22:29:28.319+0000",
"modified": "2018-05-02T22:29:28.319+0000",
"completed": false
}
]
接下来,您可以修改其中一个待办事项;例如,
curl -i -X PUT -H "Content-Type: application/json" -d '{ "description":"Take the dog and the cat for a walk", "id":"2d051b67-7716-4ee6-9c45-1de939fa579f"}' http://localhost:8080/api/todo
HTTP/1.1 201
Location: http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
Content-Length: 0
Date: Wed, 02 May 2018 22:38:03 GMT
此处Take the dog for a walk改为Take the dog and the cat for a walk。该命令使用-X PUT,并且需要id字段(我们可以从以前帖子的 location 头或者从访问/api/todo端点获得它)。如果你检查了所有的待办事项,你有一个修改的待办事项。
接下来,让我们完成一个待办事项。您可以执行以下命令。
curl -i -X PATCH http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
HTTP/1.1 200
Location: http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
Content-Length: 0
Date: Wed, 02 May 2018 22:50:27 GMT
该命令使用的是通过setCompleted方法处理的-X PATCH。如果您查看位置链接,ToDo 应该已完成。
curl -s http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f | jq
{
"id": "2d051b67-7716-4ee6-9c45-1de939fa579f",
"description": "Take the dog and the cat for a walk",
"created": "2018-05-02T22:44:57.652+0000",
"modified": "2018-05-02T22:50:27.691+0000",
"completed": true
}
completed字段现在是true。如果此待办事项已完成,那么您可以删除它。
curl -i -X DELETE http://localhost:8080/api/todo/2d051b67-7716-4ee6-9c45-1de939fa579f
HTTP/1.1 204
Date: Wed, 02 May 2018 22:56:18 GMT
cURL 命令有-X DELETE,它由deleteToDo方法处理,从散列中删除。如果你看一下所有的待办事项,你现在应该比以前少一个。
curl -s http://localhost:8080/api/todo | jq
[
{
"id": "d8d37c51-10a8-4c82-a7b1-b72b5301cdab",
"description": "Read the Pro Spring Boot 2nd Edition Book",
"created": "2018-05-02T22:27:26.042+0000",
"modified": "2018-05-02T22:27:26.042+0000",
"completed": false
},
{
"id": "fbb20090-19f5-4abc-a8a9-92718c2c4759",
"description": "Bring Milk after work",
"created": "2018-05-02T22:28:23.249+0000",
"modified": "2018-05-02T22:28:23.249+0000",
"completed": false
}
]
现在,让我们测试验证。执行以下命令。
curl -i -X POST -H "Content-Type: application/json" -d '{"description":""}' http://localhost:8080/api/todo
该命令在描述字段为空的情况下发送数据(-d选项)(通常,当您提交 HTML 表单时会发生这种情况)。您应该会看到以下输出。
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 03 May 2018 00:01:53 GMT
Connection: close
{"errors":["must not be blank"],"errorMessage":"Validation failed. 1 error(s)"}
400 状态码(错误请求)和errors和errorMessage(由ToDoValidationErrorBuilder类构建)响应。使用以下命令。
curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/api/todo
HTTP/1.1 400
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 03 May 2018 00:07:28 GMT
Connection: close
{"errorMessage":"Required request body is missing: public org.springframework.http.ResponseEntity<?> com.apress.todo.controller.ToDoController.createToDo(com.apress.todo.domain.ToDo,org.springframework.validation.Errors)"}
此命令正在发布,但没有数据,并以错误消息作为响应。这来自于@ExceptionHandler注释和handleException方法。所有的错误(不同于空白的描述)都用这个方法处理。
您可以继续测试更多的 ToDo 或修改一些验证注释,看看它们是如何工作的。
注意
如果没有 cURL 命令或者无法安装,可以使用其他任何 REST 客户端,比如 PostMan ( https://www.getpostman.com )或者失眠( https://insomnia.rest )。如果你喜欢命令行,那么 Httpie ( https://httpie.org )是另一个不错的选择;它使用 Python。
Spring Boot Web:改写默认值
Spring Boot web 自动配置设置默认运行一个 Spring web 应用。在这一节中,我将向您展示如何覆盖其中的一些。
您可以通过创建自己的配置(XML 或 JavaConfig)和/或使用application.properties(或)来覆盖 web 默认值。yml)文件。
服务器覆盖
默认情况下,嵌入式 Tomcat 服务器在端口 8080 上启动,但是您可以通过使用以下属性轻松地更改它。
server.port=8081
Spring 的一个很酷的特性是你可以应用 SpEL (Spring Expression Language)并将其应用于这些属性。例如,当您创建一个可执行的 jar (./mvnw package或./gradlew build时,您可以在运行您的应用时传递一些参数。您可以执行以下操作
java -jar todo-in-memory-0.0.1-SNAPSHOT.jar --port=8787
而在你的application.properties中,你有这样的东西:
server.port=${port:8282}
这个表达式意味着如果你传递了--port参数,它就取那个值;如果不是,则设置为8282。这只是你能用 SpEL 做的一点点尝试,但是如果你想知道更多,去 https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions 。
您还可以更改服务器地址,这在您希望使用特定 IP 运行应用时非常有用。
server.address=10.0.0.7
您可以更改应用的上下文。
server.servlet.context-path=/my-todo-app
你可以像这样卷曲:
curl -I http://localhost:8080/my-todo-app/api/todo
您可以通过使用以下属性让 Tomcat 支持 SSL。
server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=secret
server.ssl.key-password=secret
我们将在后面的章节中重温这些属性,并让我们的应用与 SSL 一起工作。
您可以使用下列属性来管理会话。
server.servlet.session.store-dir=/tmp
server.servlet.session.persistent=true
server.servlet.session.timeout=15
server.servlet.session.cookie.name=todo-cookie.dat
server.servlet.session.cookie.path=/tmp/cookies
如果您的环境支持,您可以启用 HTTP/2 支持。
server.http2.enabled=true
JSON 日期格式
默认情况下,日期类型以长格式显示在 JSON 响应中;但是您可以通过在以下属性中提供您自己的模式来改变这一点。
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=MST7MDT
这些属性格式化日期并使用您指定的时区(如果您想了解更多关于可用 id 的信息,您可以执行java.util.TimeZone#getAvailableIDs)。如果您修改了 ToDo 应用,运行它,添加一些 ToDo,并获得列表。您应该会得到这样的回应:
curl -s http://localhost:8080/api/todo | jq
[
{
"id": "f52d1429-432d-43c5-946d-15c7fa5f50eb",
"description": "Get the Pro Spring Boot 2nd Edition Book",
"created": "2018-05-03 11:40:37",
"modified": "2018-05-03 11:40:37",
"completed": false
}
]
如果你想知道更多关于哪些属性存在,书签 https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 。
内容类型:JSON/XML
Spring MVC 使用HttpMessageConverters(客户端和服务器端)来协商 HTTP 交换中的内容转换。如果在类路径中找到了 Jackson 库,Spring Boot 会将默认值设置为 JSON。但是,如果您也想公开 XML,并请求 JSON 或 XML 内容,会发生什么呢?
Spring Boot 通过添加一个额外的依赖项和一个属性使这变得非常简单。
如果您使用的是 Maven,请参见清单 4-10 。
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
Listing 4-10Maven: pom.xml
或者,如果您正在使用 Gradle,请参见清单 4-11 。
compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml')
Listing 4-11Gradle: build.gradle
您可以将以下属性添加到 application.properties 文件中。
spring.mvc.contentnegotiation.favor-parameter=true
如果您运行带有这些更改的 ToDo 应用,您可以通过执行以下命令获得 XML 格式的响应。
curl -s http://localhost:8080/api/todo?format=xml
<ArrayList><item><id>b3281340-b1aa-4104-b3d2-77a96a0e41b8</id><description>Read the Pro Spring Boot 2nd Edition Book</description><created>2018-05-03T19:18:30.260+0000</created><modified>2018-05-03T19:18:30.260+0000</modified><completed>false</completed></item></ArrayList>
在前面的命令中,?format=xml被追加到 URL 后面;JSON 响应也是如此。
curl -s http://localhost:8080/api/todo?format=json | jq
[
{
"id": "b3281340-b1aa-4104-b3d2-77a96a0e41b8",
"description": "Read the Pro Spring Boot 2nd Edition Book",
"created": "2018-05-03T19:18:30.260+0000",
"modified": "2018-05-03T19:18:30.260+0000",
"completed": false
}
]
如果您想漂亮地打印 XML,在 UNIX 环境中有xmllint命令。
curl -s http://localhost:8080/api/todo?format=xml | xmllint --format -
Spring MVC:覆盖默认值
到目前为止,我还没有向您展示如何创建 Web 应用来公开 HTML、JavaScript 等技术的组合,这是因为现在业界倾向于在前端使用 JavaScript/TypeScript 应用。
这并不意味着你不能创建一个具有后端和前端的 Spring Web MVC。Spring Web MVC 为您提供了与模板引擎和其他技术的良好集成。
如果使用任何模板引擎,可以通过使用以下属性来选择视图前缀和后缀:
spring.mvc.view.prefix=/WEB-INF/my-views/
spring.mvc.view.suffix=.jsp
正如你所看到的,Spring Boot 可以帮助你做很多配置,如果你在做常规的 Spring Web 应用,通常你需要做很多。这种新方式有助于你加速发展。如果你需要更多关于如何使用 Spring MVC 及其所有特性的指导,你可以看看参考文档: https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/web.html#mvc
使用不同的应用容器
默认情况下,Spring Boot 使用 Tomcat(用于 web servlet 应用)作为应用容器,并设置一个嵌入式服务器。如果您想覆盖这个缺省值,可以通过修改 Maven pom.xml或 Gradle build.gradle文件来实现。
使用 Jetty 服务器
同样的变化也适用于逆流或净流(见清单 4-12 和清单 4-13 )。
configurations {
compile.exclude module: "spring-boot-starter-tomcat"
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-jetty")
// ...
}
Listing 4-13Gradle: build.gradle
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Listing 4-12Maven - pom.xml
Spring Boot 网站:客户端
创建 Spring Boot web 应用的另一个重要特性是 Spring Web MVC 附带了一个有用的RestTemplate类,可以帮助创建客户端。
任何 App 客户端
打开您的浏览器并转到 https://start.spring.io 站点,使用以下值创建您的 ToDo 客户端应用(另见图 4-2 )。
图 4-2
任何 app 客户端
-
组:
com.apress.todo -
神器:
todo-client -
名称:
todo-client -
包名:
com.apress.todo -
依赖关系:
Web,Lombok
按下 Generate Project 按钮并下载 ZIP 文件。将其解压缩并导入到您最喜欢的 IDE 中。
域模型:ToDo
创建 ToDo 域模型,该模型应该与上一个应用中的最小字段相匹配(参见清单 4-14 )。
package com.apress.todo.client.domain;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
public class ToDo {
private String id;
private String description;
private LocalDateTime created;
private LocalDateTime modified;
private boolean completed;
public ToDo(){
LocalDateTime date = LocalDateTime.now();
this.id = UUID.randomUUID().toString();
this.created = date;
this.modified = date;
}
public ToDo(String description){
this();
this.description = description;
}
}
Listing 4-14com.apress.todo.client.domain.ToDo.java
清单 4-14 向您展示了 ToDo 域模型类。包名称不同;不需要匹配类名。应用不需要知道要序列化或反序列化到 JSON 中的包是什么。
错误处理函数:两个错误处理函数
接下来,让我们创建一个错误处理程序,处理来自服务器的任何错误响应。创建ToDoErrorHandler类(参见清单 4-15 )。
package com.apress.todo.client.error;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import java.io.IOException;
import java.nio.charset.Charset;
public class ToDoErrorHandler extends DefaultResponseErrorHandler {
private Logger log = LoggerFactory.getLogger(ToDoErrorHandler.class);
@Override
public void handleError(ClientHttpResponse response)
throws IOException {
log.error(response.getStatusCode().toString());
log.error(StreamUtils.copyToString(
response.getBody(),Charset.defaultCharset()));
}
}
Listing 4-15com.apress.todo.client.error.ToDoErrorHandler.java
清单 4-15 显示了ToDoErrorHandler类,这是一个从DefaultResponseErrorHandler扩展而来的定制类。因此,如果我们得到一个 400 HTTP 状态(错误请求),我们可以捕捉错误并对其做出反应;但是在这种情况下,该类只是记录错误。
自定义属性:ToDoRestClientProperties
有必要知道 ToDo 应用在哪里运行,以及使用哪个 basePath,这就是为什么有必要保存这些信息。最佳实践之一是从外部获取这些信息。
让我们创建一个保存 URL 和基本路径信息的ToDoRestClientProperties类。这些信息可以保存在application.properties文件中(参见清单 4-16 )。
package com.apress.todo.client;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix="todo")
@Data
public class ToDoRestClientProperties {
private String url;
private String basePath;
}
Listing 4-16com.apress.todo.client.ToDoRestClientProperties.java
清单 4-16 显示了保存 URL 和基本路径信息的类。Spring Boot 允许您创建自定义类型的属性,这些属性可以从application.properties文件中访问和映射;唯一的要求是您需要用@ConfigurationProperties注释来标记这个类。这个注释可以接受像prefix这样的参数。
在application.properties文件中,添加以下内容。
todo.url=http://localhost:8080
todo.base-path=/api/todo
客户端:ToDoRestClient
让我们创建一个使用RestTemplate类的客户机,它有助于在客户机和服务器之间交换信息。创建ToDoRestClient类(参见清单 4-17 )。
package com.apress.todo.client;
import com.apress.todo.client.domain.ToDo;
import com.apress.todo.client.error.ToDoErrorHandler;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
@Service
public class ToDoRestClient {
private RestTemplate restTemplate;
private ToDoRestClientProperties properties;
public ToDoRestClient(
ToDoRestClientProperties properties){
this.restTemplate = new RestTemplate();
this.restTemplate.setErrorHandler(
new ToDoErrorHandler());
this.properties = properties;
}
public Iterable<ToDo> findAll() throws URISyntaxException {
RequestEntity<Iterable<ToDo>> requestEntity = new RequestEntity<Iterable<ToDo>>(HttpMethod.GET,new URI(properties.getUrl() + properties.getBasePath()));
ResponseEntity<Iterable<ToDo>> response =
restTemplate.exchange(requestEntity,new ParameterizedTypeReference<Iterable<ToDo>>(){});
if(response.getStatusCode() == HttpStatus.OK){
return response.getBody();
}
return null;
}
public ToDo findById(String id){
Map<String, String> params = new HashMap<String, String>();
params.put("id", id);
return restTemplate.getForObject(properties.getUrl() + properties.getBasePath() + "/{id}",ToDo.class,params);
}
public ToDo upsert(ToDo toDo) throws URISyntaxException {
RequestEntity<?> requestEntity = new RequestEntity<>(toDo,HttpMethod.POST,new URI(properties.getUrl() + properties.getBasePath()));
ResponseEntity<?> response = restTemplate.exchange(requestEntity, new ParameterizedTypeReference<ToDo>() {});
if(response.getStatusCode() == HttpStatus.CREATED){
return restTemplate.getForObject(response.getHeaders().getLocation(),ToDo.class);
}
return null;
}
public ToDo setCompleted(String id) throws URISyntaxException{
Map<String, String> params = new HashMap<String, String>();
params.put("id", id);
restTemplate.postForObject(properties.getUrl() + properties.getBasePath() + "/{id}?_method=patch",null, ResponseEntity.class, params);
return findById(id);
}
public void delete(String id){
Map<String, String> params = new HashMap<String, String>();
params.put("id", id);
restTemplate.delete(properties.getUrl() + properties.getBasePath() + "/{id}",params);
}
}
Listing 4-17com.apress.todo.client.ToDoRestClient.java
清单 4-17 显示了与 ToDo 应用交互的客户端。该类正在使用 RestTemplate 类。RestTemplate是 Spring 用于同步客户端 HTTP 访问的中心类。它简化了与 HTTP 服务器的通信,并加强了 RESTful 原则。它处理 HTTP 连接,让应用代码提供 URL(可能有模板变量)并提取结果。许多特性中的一个允许您处理自己的错误响应。所以看一下构造函数,它正在设置ToDoErrorHandler类。
复习课;它包含 ToDo 应用(后端)的所有操作。
注意
默认情况下,RestTemplate 依赖标准的 JDK 设施来建立 HTTP 连接。您可以通过 interceptinghtpaccessor . setrequestfactory(org . spring framework . HTTP . client . clienthttprequestfactory)属性切换到使用不同的 HTTP 库,如 Apache HttpComponents、Netty 和 OkHttp。
运行和测试客户端
为了运行和测试客户端应用,修改TodoClientApplication类(参见清单 4-18 )。
package com.apress.todo;
import com.apress.todo.client.ToDoRestClient;
import com.apress.todo.client.domain.ToDo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class TodoClientApplication {
public static void main(String[] args) {
SpringApplication app = new
SpringApplication(TodoClientApplication.class);
app.setWebApplicationType(WebApplicationType.NONE);
app.run(args);
}
private Logger log = LoggerFactory.getLogger(TodoClientApplication.class);
@Bean
public CommandLineRunner process(ToDoRestClient client){
return args -> {
Iterable<ToDo> toDos = client.findAll();
assert toDos != null;
toDos.forEach( toDo -> log.info(toDo.toString()));
ToDo newToDo = client.upsert(new ToDo("Drink plenty of Water daily!"));
assert newToDo != null;
log.info(newToDo.toString());
ToDo toDo = client.findById(newToDo.getId());
assert toDos != null;
log.info(toDo.toString());
ToDo completed = client.setCompleted(newToDo.getId());
assert completed.isCompleted();
log.info(completed.toString());
client.delete(newToDo.getId());
assert client.findById(newToDo.getId()) == null;
};
}
}
Listing 4-18com.apress.todo.TodoClientApplication.java
清单 4-18 向您展示了如何测试客户端。首先用WebApplicationType.NONE关闭 web 环境。然后使用CommandLineRunner(作为 bean)在 app 启动前执行代码。
在运行它之前,分析代码,看看发生了什么。您可以使用命令行或通过您的 IDE 运行。确保 ToDo 应用已启动并正在运行。
您将再次使用此客户端。
注意
请记住,所有的源代码都可以从 Apress 网站或 GitHub 的以下位置下载: https://github.com/Apress/pro-spring-boot-2 。
摘要
在这一章中,你学习了 Spring Boot 如何管理和自动配置 web 应用,以及它如何使用 Spring MVC 的力量。您还了解了可以覆盖自动配置提供的所有合理的缺省值。
通过 ToDo 应用,您学习了 web 应用的一些 Spring Boot 特性,比如 JSON 和 XML 配置,几个 MVC 注释的用法,比如@RequestMapping和@ExceptionHandler,等等。
您了解了 Spring Boot 如何使用嵌入式应用容器(在本例中,Tomcat 是默认的)来轻松部署或传输。
您在 ToDo 应用中使用的所有注释都是 Spring MVC 的一部分。在本书的剩余部分,我们实现了更多。如果想了解更多关于 Spring MVC 的知识,可以看看 Pro Spring 5 (Apress,2017)。
在下一章中,您将了解 Spring Boot 如何为持久性引擎自动配置 Spring Data 模块。您还将了解更多关于 Spring JDBC、JPA、REST 和 NoSQL 模块的知识,比如 Mongo。
五、Spring Boot 的数据访问
从尝试访问、保存和分析数据,到使用几个字节到几十亿字节的信息,数据已经成为 IT 世界中最重要的部分。已经有许多尝试来创建框架和库,以便于开发人员与数据进行交互,但是有时这变得太复杂了。
在 3.0 版本之后,Spring 框架创建了不同的团队,专门研究不同的技术。Spring Data 项目组诞生了。这个项目的目标是简化数据访问技术的使用,从关系数据库和非关系数据库,到 map-reduce 框架和基于云的数据服务。这个 Spring Data 项目是特定于给定数据库的子项目的集合。
本章介绍了使用前几章中的 ToDo 应用通过 Spring Boot 进行数据访问。您将使 ToDo 应用与 SQL 和 NoSQL 数据库一起工作。我们开始吧。
SQL 数据库
您还记得(在 Java 世界中)需要处理所有 JDBC (Java 数据库连接)任务的那些日子吗?您必须下载正确的驱动程序和连接字符串,打开和关闭连接、SQL 语句、结果集和事务,并将结果集转换为对象。在我看来,这些都是非常手工的工作。然后,许多 ORM(对象关系映射)框架开始出现来管理这些任务——比如 Castor XML、ObjectStore 和 Hibernate 等框架。它们允许您识别域类并创建与数据库表相关的 XML。在某种程度上,你也需要成为管理这类框架的专家。
Spring 框架通过遵循模板设计模式对那些框架帮助很大。它允许您创建一个抽象类,该类定义了执行方法的方式,并创建了允许您只关注业务逻辑的数据库抽象。它将所有困难留给了 Spring 框架,包括处理连接(打开、关闭和池化)、事务以及与框架交互的方式。
值得一提的是,Spring 框架依赖于几个接口和类(如javax.sql.DataSource接口)来获得关于您将要使用的数据库、如何连接到它(通过提供连接字符串)以及它的凭证的信息。现在,如果您有事务管理要做,那么DataSource接口是必不可少的。通常,DataSource接口需要驱动程序类、JDBC URL、用户名和密码来连接数据库。
Spring Data
Spring Data 团队已经为 Java 和 Spring 社区创建了一些惊人的数据驱动框架。他们的任务是为数据访问提供熟悉且一致的基于 Spring 的编程,并对您想要使用的底层数据存储技术进行全面控制。
Spring Data 项目是几个额外的库和数据框架的保护伞,这使得对关系和非关系数据库使用数据访问技术变得很容易(又名 NoSQL)。
以下是 Spring Data 的一些特性。
-
支持跨存储持久性
-
基于存储库和定制的对象映射抽象
-
基于方法名的动态查询
-
通过 JavaConfig 和 XML 实现简单的 Spring Integration
-
支持 Spring MVC 控制器
-
透明审核的事件(已创建,上次更改)
还有更多的功能——需要一整本书来介绍它们。这一章的内容足以创建强大的数据驱动的应用。请记住,Spring Data 是涵盖所有内容的主要总括项目。
Spring 的 JDBC
在这一节中,我将向您展示如何使用JdbcTemplate类。这个特定的类实现了模板设计模式,这是一个具体的类,它公开了执行其方法的已定义方式或模板。它隐藏了所有的样板算法或一组指令。在 Spring 中,您可以选择不同的方式来构成您的 JDBC 数据库访问的基础;使用JdbcTemplate类是经典的 Spring JDBC 方法,这是最低级别。
当您使用JdbcTemplate类时,您只需要实现回调接口来创建与任何数据库引擎交互的简单方法。JdbcTemplate类需要一个javax.sql.DataSource,可以在任何类中使用,通过在 JavaConfig、XML 或注释中声明它。JdbcTemplate类负责所有的SQLException并得到妥善处理。
您可以使用NamedParameterJdbcTemplate(JDBC template 包装器)来提供命名参数(:parameterName),而不是传统的 JDBC "?"占位符。这是 SQL 查询的另一个选项。
JdbcTemplate类公开了不同的方法。
-
查询(选择)。您通常使用
query、queryForObject方法调用。 -
更新(插入/更新/删除)。你使用
update方法调用。 -
操作(数据库/表/函数)。您使用
execute和update方法调用。
使用 Spring JDBC,您可以通过使用SimpleJdbcCall类调用存储过程,并使用特定的RowMapper接口处理结果。JdbcTemplate类使用RowMapper<T>来逐行映射ResultSet的行。
Spring JDBC 支持嵌入式数据库引擎,比如 HSQL、H2 和 Derby。它易于配置,并提供快速启动时间和可测试性。
另一个特性是用脚本初始化数据库的能力;您可以使用嵌入式支持,也可以不使用。您可以添加自己的 SQL 格式的模式和数据。
JDBC 和 Spring Boot
Spring 框架支持使用 JDBC 或 ORMs 处理 SQL 数据库(我将在下面的章节中介绍)。Spring Boot 给数据应用带来了更多。
当 Spring Boot 发现您的应用有一个 JDBC JARs 时,它使用自动配置来设置合理的默认值。Spring Boot 根据类路径中的 SQL 驱动程序自动配置数据源。如果它发现您有任何嵌入式数据库引擎(H2、HSQL 或 Derby),那么它是默认配置的;换句话说,你可以有两个驱动依赖项(例如,MySQL 和 H2),如果 Spring Boot 没有找到任何声明的数据源 bean,它会基于类路径中的嵌入式数据库引擎 JAR 创建它(例如,H2)。默认情况下,Spring Boot 还将 HikariCP 配置为连接池管理器。当然,您可以覆盖这些默认值。
如果您想覆盖缺省值,那么您需要提供自己的 datasource 声明,或者是 JavaConfig、XML,或者是在application.properties文件中。
# Custom DataSource
spring.datasource.username=springboot
spring.datasource.password=rocks!
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?autoReconnect=true&useSSL=false
src/main/resources/application.properties
如果您在应用容器中部署应用,Spring Boot 支持 JNDI 连接。您可以在application.properties文件中设置 JNDI 名称。
spring.datasource.jndi-name=java:jboss/ds/todos
Spring Boot 为数据应用带来的另一个功能是,如果你有一个名为 schema.sql 、 data.sql 、 schema- < platform >的文件。sql ,或数据- <平台>。sql 在类路径中,它通过执行这些脚本文件来初始化您的数据库。
因此,如果您想在您的 Spring Boot 应用中使用 JDBC,您需要添加spring-boot-starter-jdbc依赖项和您的 SQL 驱动程序。
ToDo 应用
是时候像我们在上一章所做的那样使用 ToDo 应用了。你可以从头开始,也可以跟着做。如果您是从零开始,那么您可以转到 Spring Initializr ( https://start.spring.io )并将以下值添加到字段中。
-
组:
com.apress.todo -
神器:
todo-jdbc -
名称:
todo-jdbc -
包名:
com.apress.todo -
依赖关系:
Web, Lombok, JDBC, H2, MySQL
您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-1 )。
图 5-1
spring initializehttps://start.spring.io
你可以复制上一章的所有类,除了ToDoRepository类;这是唯一的新类。还要确保在pom.xml或build.gradle文件中有两个驱动程序:H2 和 MySQL。根据我在上一节中讨论的内容,如果我不指定任何数据源(在 JavaConfig、XML 或application.properties中),Spring Boot 自动配置会做什么呢?正确!Spring Boot 默认自动配置H2 嵌入式数据库。
存储库:ToDoRepository
创建一个实现CommonRepository接口的ToDoRepository类(参见清单 5-1 )。
package com.apress.todo.repository;
import com.apress.todo.domain.ToDo;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.time.LocalDateTime;
import java.util.*;
@Repository
public class ToDoRepository implements CommonRepository<ToDo> {
private static final String SQL_INSERT = "insert into todo (id, description, created, modified, completed) values (:id,:description,:created,:modified,:completed)";
private static final String SQL_QUERY_FIND_ALL = "select id, description, created, modified, completed from todo";
private static final String SQL_QUERY_FIND_BY_ID = SQL_QUERY_FIND_ALL + " where id = :id";
private static final String SQL_UPDATE = "update todo set description = :description, modified = :modified, completed = :completed where id = :id";
private static final String SQL_DELETE = "delete from todo where id = :id";
private final NamedParameterJdbcTemplate jdbcTemplate;
public ToDoRepository(NamedParameterJdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
private RowMapper<ToDo> toDoRowMapper = (ResultSet rs, int rowNum) -> {
ToDo toDo = new ToDo();
toDo.setId(rs.getString("id"));
toDo.setDescription(rs.getString("description"));
toDo.setModified(rs.getTimestamp("modified").toLocalDateTime());
toDo.setCreated(rs.getTimestamp("created").toLocalDateTime());
toDo.setCompleted(rs.getBoolean("completed"));
return toDo;
};
@Override
public ToDo save(final ToDo domain) {
ToDo result = findById(domain.getId());
if(result != null){
result.setDescription(domain.getDescription());
result.setCompleted(domain.isCompleted());
result.setModified(LocalDateTime.now());
return upsert(result, SQL_UPDATE);
}
return upsert(domain,SQL_INSERT);
}
private ToDo upsert(final ToDo toDo, final String sql){
Map<String, Object> namedParameters = new HashMap<>();
namedParameters.put("id",toDo.getId());
namedParameters.put("description",toDo.getDescription());
namedParameters.put("created",java.sql.Timestamp.valueOf(toDo.getCreated()));
namedParameters.put("modified",java.sql.Timestamp.valueOf(toDo.getModified()));
namedParameters.put("completed",toDo.isCompleted());
this.jdbcTemplate.update(sql,namedParameters);
return findById(toDo.getId());
}
@Override
public Iterable<ToDo> save(Collection<ToDo> domains) {
domains.forEach( this::save);
return findAll();
}
@Override
public void delete(final ToDo domain) {
Map<String, String> namedParameters = Collections.singletonMap("id", domain.getId());
this.jdbcTemplate.update(SQL_DELETE,namedParameters);
}
@Override
public ToDo findById(String id) {
try {
Map<String, String> namedParameters = Collections.singletonMap("id", id);
return this.jdbcTemplate.queryForObject(SQL_QUERY_FIND_BY_ID, namedParameters, toDoRowMapper);
} catch (EmptyResultDataAccessException ex) {
return null;
}
}
@Override
public Iterable<ToDo> findAll() {
return this.jdbcTemplate.query(SQL_QUERY_FIND_ALL, toDoRowMapper);
}
}
Listing 5-1com.apress.todo.respository.ToDoRepository.java
列表 5-1 显示了使用JdbcTemplate的ToDoRepository类,不是直接强韧的。这个类使用的是帮助所有命名参数的NamedParameterJdbcTemplate(一个JdbcTemplate包装器),这意味着在 SQL 语句中不使用?,而是使用类似:id的名称。
此类还声明了一个行映射器;请记住,JdbcTemplate使用行映射器在每行的基础上映射ResultSet的行。
分析代码并检查每个方法中使用普通 SQL 的每个方法实现。
数据库初始化:schema.sql
请记住,Spring 框架允许您初始化数据库——创建或修改任何表,或者在应用启动时插入/更新数据。要初始化一个 Spring app(不是 Spring Boot),需要添加配置(XML 或者 Java config);但是 ToDo 应用是 Spring Boot。如果 Spring Boot 找到了schema.sql和/或data.sql文件,它会自动执行它们。让我们创建schema.sql(见清单 5-2 )。
DROP TABLE IF EXISTS todo;
CREATE TABLE todo
(
id varchar(36) not null primary key,
description varchar(255) not null,
created timestamp,
modified timestamp,
completed boolean
);
Listing 5-2src/main/resources/schema.sql
清单 5-2 显示了应用启动时执行的schema.sql,因为 H2 是配置的默认数据源,所以这个脚本是针对 H2 引擎执行的。
运行和测试:ToDo 应用
现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它,或者如果您使用的是 Maven,请执行
./mvnw spring-boot:run
如果您使用的是 Gradle,请执行
./gradlew bootRun
要测试 ToDo 应用,您可以运行 ToDoClient 应用。它应该没有任何问题。
H2 控制台
既然您已经运行了 ToDo 应用,那么您如何确保应用将数据保存在 H2 引擎中呢?Spring Boot 有一个属性可以启用 H2 控制台,这样你就可以与它进行交互。它对于开发非常有用,但对于生产环境却不太有用。
修改application.properties文件并添加以下属性。
spring.h2.console.enabled=true
src/main/resources/application.properties
重启 ToDo 应用,用 cURL 命令添加值,进入浏览器并点击http://localhost:8080/h2-console。(见图 5-2 )。
图 5-2
http://localhost:8080/h2-console
图 5-2 显示了 H2 控制台,您可以在/h2-console端点到达该控制台。(您也可以覆盖这个端点)。JDBC URL 必须是jdbc:h2:mem:testdb(有时这是不同的,所以把它改成那个值)。默认情况下,数据库名称是testdb(但是您也可以覆盖它)。如果您点击连接按钮,您将获得不同的视图,在该视图中您可以看到正在创建的表格和数据(参见图 5-3 )。
图 5-3
图 5-3 显示了您可以执行任何 SQL 查询并取回数据。如果您想查看 ToDo 应用中正在执行哪些 SQL 查询,您可以将以下属性添加到application.properties文件中。
logging.level.org.springframework.data=INFO
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
src/main/resources/application.properties
正如您所看到的,JdbcTemplate类为您提供了许多与任何数据库引擎交互的可能性,但是这个类是“最底层”的方法。
在撰写本文时,有了一种以更统一的方式使用JdbcTemplate类的新方法 Spring Data 方法(我将在下面的章节中描述)。Spring Data 团队已经创建了新的 Spring Data JDBC 项目,它遵循了 Eric Evans 在《?? 领域驱动设计》一书中描述的聚合根概念。它有很多特性,比如 CRUD 操作、支持@Query注释、支持 MyBatis 查询、事件等等,所以请关注这个项目。这是一种新的 JDBC 方式。
Spring Data
JPA (Java 持久性 API)为对象关系映射提供了一个 POJO 持久性模型。Spring Data JPA 促进了这种模型的持久性。
实现数据访问可能会很麻烦,因为我们需要处理连接、会话、异常处理等等,即使对于简单的 CRUD 操作也是如此。这就是为什么 Spring Data JPA 提供了额外级别的功能:直接从接口创建存储库实现,并使用约定从方法名生成查询。
以下是 Spring Data JPA 的一些特性。
-
不同提供者对 JPA 规范的支持,比如 Hibernate、Eclipse Link、Open JPA 等等。
-
对存储库的支持(来自领域驱动设计的概念)。
-
域类的审核。
-
支持 Quesydsl (
http://www.querydsl.com/)谓词和类型安全的 JPA 查询。 -
分页、排序、动态查询执行支持。
-
支持
@Query标注。 -
支持基于 XML 的实体映射。
-
使用
@EnableJpaRepositories注释进行基于 JavaConfig 的存储库配置。
Spring Boot Spring Data JPA
Spring Data JPA 最重要的一个好处是,我们不需要担心实现基本的 CRUD 功能,因为这就是它要做的。我们只需要创建一个从Repository<T,ID>、CrudRepository<T,ID>或JpaRepository<T,ID>扩展而来的接口。JpaRepository接口不仅提供了CrudRepository所做的,还从提供额外功能的PagingAndSortingRepository接口扩展而来。如果您查看CrudRepository<T,ID>接口(在您的 ToDo 应用中使用),您可以看到所有的签名方法,如清单 5-3 所示。
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
Listing 5-3org.springframework.data.repository.CrudRepository.java
清单 5-3 展示了CrudRepository<T,ID>接口,其中T表示实体(你的域模型类)和 ID,需要实现Serializable的主键。
在一个简单的 Spring 应用中,需要使用@EnableJpaRepositories注释来触发额外的配置,该配置应用于应用中定义的存储库的生命周期中。好的一面是当你使用 Spring Boot 的时候你不需要这个,因为 Spring Boot 会照顾它。Spring Data JPA 的另一个特性是查询方法,这是用域实体的字段创建 SQL 语句的一种非常强大的方法。
因此,要在 Spring Boot 中使用 Spring Data JPA,您需要spring-boot-starter-data-jpa和 SQL 驱动程序。
当 Spring Boot 执行其自动配置并发现您有 Spring Data JPA JAR 时,它默认配置数据源(如果没有定义的话)。它配置 JPA 提供程序(默认情况下,它使用 Hibernate)。它启用了存储库(通过使用@EnableJpaRepositories配置)。它检查您是否定义了任何查询方法。还有更多。
带有 Spring Data 的 ToDo 应用 JPA
您可以从头开始创建您的 ToDo 应用,或者查看您需要的类,以及您的pom.xml或build.gradle文件中必要的依赖项。
从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。
-
组:
com.apress.todo -
神器:
todo-jpa -
名称:
todo-jpa -
包名:
com.apress.todo -
依赖关系:
Web, Lombok, JPA, H2, MySQL
您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-4 )。
图 5-4
spring initializehttps://start.spring.io
您可以复制前一章中的所有类,除了ToDoRepository类,它是唯一新的类;你修改其他人。
存储库:ToDoRepository
创建一个从CrudRepository<T,ID>扩展而来的ToDoRepository接口。T是ToDo级,ID 是String(见清单 5-4 )。
package com.apress.todo.repository;
import com.apress.todo.domain.ToDo;
import org.springframework.data.repository.CrudRepository;
public interface ToDoRepository extends
CrudRepository<ToDo,String> {}
Listing 5-4com.apress.todo.repository.ToDoRepository.java
清单 5-4 显示了扩展一个CrudRepository的ToDoRepository接口。没有必要创建一个具体的类或实现任何东西;Spring Data JPA 为我们完成了实现。所有的 CRUD 操作都处理我们需要持久化数据的任何事情。就是这样——在需要的地方使用ToDoRepository,我们不需要做任何其他事情。
域模型:ToDo
要使用 JPA 并符合规范,需要从域模型中声明实体(@Entity)和主键(@Id)。让我们通过添加以下注释和方法来修改ToDo类(参见清单 5-5 )。
package com.apress.todo.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Entity
@Data
@NoArgsConstructor
public class ToDo {
@NotNull
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String id;
@NotNull
@NotBlank
private String description;
@Column(insertable = true, updatable = false)
private LocalDateTime created;
private LocalDateTime modified;
private boolean completed;
public ToDo(String description){
this.description = description;
}
@PrePersist
void onCreate() {
this.setCreated(LocalDateTime.now());
this.setModified(LocalDateTime.now());
}
@PreUpdate
void onUpdate() {
this.setModified(LocalDateTime.now());
}
}
Listing 5-5com.apress.todo.domain.Todo.java
清单 5-5 显示了ToDo域模型的修改版本。这个类现在有了额外的元素。
-
@NoArgsConstructor。该注释属于 Lombok 库。它创建一个没有参数的类构造函数。JPA 需要一个不带参数的构造函数。 -
@Entity。此批注指定该类是一个实体,并保存在所选的数据库引擎中。 -
@Id。该注释指定了实体的主键。被注释的字段应该是任何 Java 原语类型和任何原语包装类型。 -
@GeneratedValue。该注释提供了主键值的生成策略(仅限简单键)。通常,它与@Id注释一起使用。有不同的策略(标识、自动、顺序和表格)和一个密钥生成器。在这种情况下,该类定义了"system-uuid"(这将生成一个惟一的 36 字符 ID)。 -
@GenericGenerator。这是 Hibernate 的一部分,它允许您使用策略从前面的注释中生成一个惟一的 ID。 -
@Column。此批注指定持久属性的映射列;如果字段中没有列注释,则它是数据库中该列的默认名称。该类将创建的字段标记为仅用于插入,而不用于更新。 -
@PrePersist。这个注释是一个回调,在采取任何持久动作之前被触发。在将记录插入数据库之前,它为创建和修改的字段设置新的时间戳。 -
@PreUpdate。这个注释是在执行任何更新操作之前触发的另一个回调。它在修改的字段被更新到数据库之前为其设置新的时间戳。
最后两个注释(@PrePersist和@PreUpdate)是处理日期/时间戳的非常好的方式,使开发人员更容易理解。
在我们继续之前,分析一下代码,看看与以前版本的ToDo域模型类有什么不同。
控制器:ToDoController
现在,是时候修改ToDoController类了(参见清单 5-6 )。
package com.apress.todo.controller;
import com.apress.todo.domain.ToDo;
import com.apress.todo.domain.ToDoBuilder;
import com.apress.todo.repository.ToDoRepository;
import com.apress.todo.validation.ToDoValidationError;
import com.apress.todo.validation.ToDoValidationErrorBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder
;
import javax.validation.Valid;
import java.net.URI;
import java.util.Optional;
@RestController
@RequestMapping("/api")
public class ToDoController {
private ToDoRepository toDoRepository;
@Autowired
public ToDoController(ToDoRepository toDoRepository) {
this.toDoRepository = toDoRepository;
}
@GetMapping("/todo")
public ResponseEntity<Iterable<ToDo>> getToDos(){
return ResponseEntity.ok(toDoRepository.findAll());
}
@GetMapping("/todo/{id}")
public ResponseEntity<ToDo> getToDoById(@PathVariable String id){
Optional<ToDo> toDo = toDoRepository.findById(id);
if(toDo.isPresent())
return ResponseEntity.ok(toDo.get());
return ResponseEntity.notFound().build();
}
@PatchMapping("/todo/{id}")
public ResponseEntity<ToDo> setCompleted(@PathVariable String id){
Optional<ToDo> toDo = toDoRepository.findById(id);
if(!toDo.isPresent())
return ResponseEntity.notFound().build();
ToDo result = toDo.get();
result.setCompleted(true);
toDoRepository.save(result);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.buildAndExpand(result.getId()).toUri();
return ResponseEntity.ok().header("Location",location.toString()).build();
}
@RequestMapping(value="/todo", method = {RequestMethod.POST,RequestMethod.PUT})
public ResponseEntity<?> createToDo(@Valid @RequestBody ToDo toDo, Errors errors){
if (errors.hasErrors()) {
return ResponseEntity.badRequest().body(ToDoValidationErrorBuilder.fromBindingErrors(errors));
}
ToDo result = toDoRepository.save(toDo);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(result.getId()).toUri();
return ResponseEntity.created(location).build();
}
@DeleteMapping("/todo/{id}")
public ResponseEntity<ToDo> deleteToDo(@PathVariable String id){
toDoRepository.delete(ToDoBuilder.create().withId(id).build());
return ResponseEntity.noContent().build();
}
@DeleteMapping("/todo")
public ResponseEntity<ToDo> deleteToDo(@RequestBody ToDo toDo){
toDoRepository.delete(toDo);
return ResponseEntity.noContent().build();
}
@ExceptionHandler
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ToDoValidationError handleException(Exception exception) {
return new ToDoValidationError(exception.getMessage());
}
}
Listing 5-6com.apress.todo.controller.ToDoController.java
清单 5-6 显示了修改后的ToDoController级。它现在直接使用ToDoRepository接口,一些方法,比如findById,返回一个 Java 8 Optional类型。
在我们继续之前,分析一下这个类,看看它与以前的版本有什么不同。大部分代码保持不变。
Spring Boot JPA 房地产公司
Spring Boot 提供了允许您在使用 Spring Data JPA 时覆盖默认值的属性。其中之一是创建 DDL(数据定义语言)的能力,默认情况下它是关闭的,但是您可以启用它从您的领域模型进行逆向工程。换句话说,该属性从您的域模型类中生成表和任何其他关系。
您还可以告诉您的 JPA 提供者创建、删除、更新或验证您现有的 DDL/data,这是一种有用的迁移机制。此外,您可以设置一个属性来显示针对数据库引擎执行的 SQL 语句。
向application.properties文件添加必要的属性,如清单 5-7 所示。
# JPA
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
# H2
spring.h2.console.enabled=true
Listing 5-7src/main/resources/application.properties
清单 5-7 显示了application.properties和 JPA 属性。它基于域模型类生成表,并在每次应用启动时创建表。以下是spring.jpa.hibernate.ddl-auto属性的可能值。
-
create(创建模式并销毁以前的数据)。 -
create-drop(在会话结束时创建并销毁模式)。 -
update(如有必要,更新模式)。 -
validate(验证模式,不更改数据库)。 -
none(禁用 DDL 处理)。
运行和测试:ToDo 应用
现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它。如果您使用的是 Maven,请执行
./mvnw spring-boot:run
如果您使用的是 Gradle,请执行
./gradlew bootRun
要测试 ToDo 应用,您可以运行 ToDoClient 应用。它应该没有任何问题。你也可以用 cURL 命令发送 ToDo 并查看 H2 控制台(http://localhost:8080/h2-console)。
Spring Data 架
Spring Data REST 项目构建在 Spring Data 存储库之上。它分析你的领域模型类,它使用 HATEOAS(超媒体作为应用状态的引擎,HAL +JSON)公开超媒体驱动的 HTTP 资源。以下是其中的一些功能。
-
使用 HAL 作为媒体类型,从您的域模型类中公开一个可发现的 RESTful API。
-
支持分页并将您的域类公开为集合。
-
展示存储库中定义的查询方法的专用搜索资源。
-
如果您想扩展默认设置,支持您自己的控制器的高度定制。
-
允许通过处理 Spring
ApplicationEvents来处理 REST 请求。 -
带来了一个 HAL 浏览器来公开所有的元数据;对于开发非常有用。
-
支持 Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j、Spring Data Solr、Spring Data Cassandra、Spring Data Gemfire。
Spring Data 由 Spring Boot 提供
如果你想在一个常规的 Spring MVC app 中使用 Spring Data REST,你需要通过在你的 JavaConfig 类中包含带有@Import注释的RepositoryRestMvcConfiguration类来触发它的配置(在那里你有你的@Configuration注释);但是如果你直接使用 Spring Boot,你不需要做任何事情。多亏了@EnableAutoConfiguration注释,Spring Boot 处理了这个问题。
如果您想在 Spring Boot 应用中使用 Spring Data REST,您需要包含spring-boot-starter-data-rest和spring-boot-starter-data-*技术依赖项,和/或 SQL 驱动程序(如果您打算使用 SQL 数据库引擎)。
带有 Spring Data JPA 和 Spring Data REST 的 ToDo 应用
您可以从头开始创建您的 ToDo 应用,或者在您的pom.xml或build.gradle文件中查看您需要哪些类以及必要的依赖项。
从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。
-
组:
com.apress.todo -
神器:
todo-rest -
名称:
todo-rest -
包名:
com.apress.todo -
依赖关系:
Web, Lombok, JPA, REST Repositories, H2, MySQL
您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-5 )。
图 5-5
您只能复制域模型ToDo、ToDoRepository类和application.properties文件;是的,只有两个类和一个属性文件。
运行:待办事项应用
现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它。如果您使用的是 Maven,请执行
./mvnw spring-boot:run
如果您使用的是 Gradle,请执行
./gradlew bootRun
运行 ToDo 应用时,要看到的一个重要内容是输出。
Mapped "{[/{repository}/search],methods=[HEAD],produces ...
Mapped "{[/{repository}/search],methods=[GET],produces= ...
Mapped "{[/{repository}/search],methods=[OPTIONS],produ ...
Mapped "{[/{repository}/search/{search}],methods=[GET], ...
Mapped "{[/{repository}/search/{search}],methods=[GET], ...
Mapped "{[/{repository}/search/{search}],methods=[OPTIO ...
Mapped "{[/{repository}/search/{search}],methods=[HEAD] ...
Mapped "{[/{repository}/{id}/{property}],methods=[GET], ...
Mapped "{[/{repository}/{id}/{property}/{propertyId}],m ...
Mapped "{[/{repository}/{id}/{property}],methods=[DELET ...
Mapped "{[/{repository}/{id}/{property}],methods=[GET], ...
Mapped "{[/{repository}/{id}/{property}],methods=[PATCH ...
Mapped "{[/{repository}/{id}/{property}/{propertyId}],m ...
Mapped "{[/ || ],methods=[OPTIONS],produces=[applicatio ...
Mapped "{[/ || ],methods=[HEAD],produces=[application/h ...
Mapped "{[/ || ],methods=[GET],produces=[application/ha ...
Mapped "{[/{repository}],methods=[OPTIONS],produces=[ap ...
Mapped "{[/{repository}],methods=[HEAD],produces=[appli ...
Mapped "{[/{repository}],methods=[GET],produces=[applic ...
Mapped "{[/{repository}],methods=[GET],produces=[applic ...
Mapped "{[/{repository}],methods=[POST],produces=[appli ...
Mapped "{[/{repository}/{id}],methods=[OPTIONS],produce ...
Mapped "{[/{repository}/{id}],methods=[HEAD],produces=[ ...
Mapped "{[/{repository}/{id}],methods=[GET],produces=[a ...
Mapped "{[/{repository}/{id}],methods=[PUT],produces=[a ...
Mapped "{[/{repository}/{id}],methods=[PATCH],produces= ...
Mapped "{[/{repository}/{id}],methods=[DELETE],produces ...
Mapped "{[/profile/{repository}],methods=[GET],produces ...
Mapped "{[/profile/{repository}],methods=[OPTIONS],prod ...
Mapped "{[/profile/{repository}],methods=[GET],produces ...
它定义了存储库的所有映射端点(在这个应用中只有一个),以及您可以使用的所有 HTTP 方法。
测试:待办事项应用
为了测试 ToDo 应用,我们将使用 cURL 命令和浏览器。需要修改 ToDoClient app 接受媒体类型,HAL+JSON;所以在这一节,我们就不用了。首先看看你的浏览器。转到http://localhost:8080你应该会看到类似于图 5-6 的东西。
首先,看看你的浏览器。转到http://localhost:8080。你应该会看到类似于图 5-6 的东西。
图 5-6
如果您看到相同的信息,但是是 RAW 格式,请尝试为您的浏览器安装 JSON Viewer 插件并重新加载页面。它将http://localhost:8080/toDos URL 公开为端点,这意味着您可以访问并执行这个 URL 的所有 HTTP 方法(来自日志)。
让我们用 cURL 命令添加一些 ToDo(是一行)。
curl -i -X POST -H "Content-Type: application/json" -d '{ "description":"Read the Pro Spring Boot 2nd Edition Book"}' http://localhost:8080/toDos
HTTP/1.1 201
Location: http://localhost:8080/toDos/8a8080876338ae4e016338b2e2ee0000
Content-Type: application/hal+json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 07 May 2018 03:43:57 GMT
{
"description" : "Read the Pro Spring Boot 2nd Edition Book",
"created" : "2018-05-06T21:43:57.676",
"modified" : "2018-05-06T21:43:57.677",
"completed" : false,
"_links" : {
"self" : {
"href" : "http://localhost:8080/toDos/8a8080876338ae4e016338b2e2ee0000"
},
"toDo" : {
"href" : "http://localhost:8080/toDos/8a8080876338ae4e016338b2e2ee0000"
}
}
}
您将得到一个 HAL+JSON 结果。再添加几个之后,你可以回到你的浏览器,点击http://localhost:8080/toDos链接。你会看到类似图 5-7 的东西。
图 5-7
http://localhost:8080/toDos
图 5-7 显示了访问/toDos端点时 HAL+JSON 的响应。
使用 HAL 浏览器进行测试:ToDo 应用
Spring Data REST 项目有一个工具 HAL 浏览器。它是一个 web 应用,帮助开发人员以交互方式可视化所有端点。因此,如果您不想直接使用端点和/或 cURL 命令,您可以使用 HAL 浏览器。
要使用 HAL 浏览器,请添加以下依赖项。如果您使用的是 Maven,将以下内容添加到您的pom.xml文件中。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
Maven pom.xml
如果您使用的是 Gradle,将以下内容添加到您的build.gradle文件中。
compile 'org.springframework.data:spring-data-rest-hal-browser'
Gradle build.gradle
现在,您可以重新启动您的 ToDo 应用,并在浏览器中直接进入http://localhost:8080。你同图 5-8 所示。
图 5-8
您可以单击 GET 和 NON-GET 列来与每个端点和 HTTP 方法进行交互。这对开发人员来说是一个很好的选择。
我向您展示的是您可以使用 Spring Data REST 实现的许多特性中的一部分。你真的不需要任何控制器了。此外,由于 Spring Boot 和 Spring Data REST 中的简单覆盖,您可以公开任何域类。
没有 SQL 数据库
NoSQL 数据库是持久化数据的另一种方式,但与关系数据库的表格关系不同。这些新兴的 NoSQL 数据库已经有了一个分类系统。你可以根据它的数据模型找到它。
-
专栏(Cassandra、HBase 等。)
-
文件(CouchDB、mongodb 等)。)
-
关键值(Redis、Riak 等)。)
-
Graph (Neo4J,Virtuoso 等。)
-
多模型(OrientDB、arangodb 等)。)
如你所见,你有很多选择。我认为最重要的一种特性是找到一个可伸缩的、可以轻松处理数百万条记录的数据库。
Spring Data MongoDB
Spring Data MongoDB 项目为您提供了与 MongoDB 文档数据库的必要交互。其中一个重要的特性是,您仍然可以使用使用@Document注释的域模型类,并声明使用CrudRepository<T,ID>的接口。这创建了 MongoDB 用于持久性的必要集合。
以下是该项目的一些特点。
-
Spring Data MongoDB 提供了一个
MongoTemplate助手类(与JdbcTemplate非常相似),它处理与 MongoDB 文档数据库交互的所有样板文件。 -
持久性和映射生命周期事件。
-
MongoTemplate助手类。它还提供了使用MongoReader/MongoWriter抽象的低级映射。 -
基于 Java 的查询、标准和更新 DSL。
-
地理空间和 MapReduce 集成和 GridFS 支持。
-
对 JPA 实体的跨存储持久性支持。这意味着您可以使用标有
@Entity和其他注释的类,并使用它们通过 MongoDB 文档数据库来持久化/检索数据。
Spring Boot 的 Spring Data MongoDB
要将 MongoDB 与 Spring Boot 一起使用,您需要添加spring-boot-starter-data-mongodb依赖项并访问 MongoDB 服务器实例。
Spring Boot 使用自动配置特性来设置与 MongoDB 服务器实例通信的一切。默认情况下,Spring Boot 尝试连接到本地主机并使用端口 27017(MongoDB 标准端口)。如果您有一个 MongoDB 远程服务器,您可以通过覆盖缺省值来连接它。您需要使用application.properties文件中的spring.mongodb.*属性(最简单的方法),或者您可以使用 XML 或在 JavaConfig 类中进行 bean 声明。
Spring Boot 还自动配置了MongoTemplate类(该类与JdbcTemplate非常相似),因此它可以与 MongoDB 服务器进行任何交互。另一个很棒的特性是您可以使用库,这意味着您可以重用用于 JPA 的相同接口。
mongodb 安装
在开始之前,您需要确保您的计算机上安装了 MongoDB 服务器。
如果您使用带有brew命令( http://brew.sh/ )的 Mac/Linux,请执行以下命令。
brew install mongodb
您可以用这个命令运行它。
mongod
或者你可以从 https://www.mongodb.org/downloads#production 的网站下载 MongoDB 并按照说明进行安装。
MongoDB 嵌入式
使用 MongoDB 还有另一种方法,至少作为开发环境。可以使用 MongoDB Embedded。通常,您在测试环境中使用它,但是您可以在开发模式中用运行时范围轻松地运行它。
要使用 MongoDB Embedded,您需要添加以下依赖项。如果使用的是 Maven,可以添加到pom.xml文件中。
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>runtime</scope>
</dependency>
Maven pom.xml
如果您使用的是 Gradle:
runtime('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
Gradle build.gradle
接下来,您需要配置 Mongo 客户端来使用 MongoDB 嵌入式服务器(参见清单 5-8 )。
package com.apress.todo.config;
import com.mongodb.MongoClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
@Configuration
public class ToDoConfig {
private Environment environment;
public ToDoConfig(Environment environment){
this.environment = environment;
}
@Bean
@DependsOn("embeddedMongoServer")
public MongoClient mongoClient() {
int port =
this.environment.getProperty("local.mongo.port",
Integer.class);
return new MongoClient("localhost",port);
}
}
Listing 5-8com.apress.todo.config.ToDoConfig.java
清单 5-8 显示了MongoClient bean 的配置。MongoDB Embedded 在应用启动时使用一个随机端口,这就是为什么还需要使用Environment bean 的原因。
如果采用这种方法来使用 MongoDB 服务器,则不需要设置任何其他属性。
带有 Spring Data 的 ToDo 应用 MongoDB
您可以从头开始创建您的 ToDo 应用,或者在您的pom.xml或build.gradle文件中查看您需要哪些类以及必要的依赖项。
从头开始,打开你的浏览器,打开 Spring Initializr ( https://start.spring.io )。将以下值添加到字段中。
-
组:
com.apress.todo -
神器:
todo-mongo -
名称:
todo-mongo -
包名:
com.apress.todo -
依赖关系:
Web, Lombok, MongoDB, Embedded MongoDB
您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩,并在您喜欢的 IDE 中导入项目(参见图 5-9 )。
图 5-9
您可以从 todo-jpa 项目中复制所有的类。在下一节中,您将看到哪些类需要修改。
域模型:ToDo
打开ToDo域模型类并相应地修改它(参见清单 5-9 )。
package com.apress.todo.domain;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;
@Document
@Data
public class ToDo {
@NotNull
@Id
private String id;
@NotNull
@NotBlank
private String description;
private LocalDateTime created;
private LocalDateTime modified;
private boolean completed;
public ToDo(){
LocalDateTime date = LocalDateTime.now()
;
this.id = UUID.randomUUID().toString();
this.created = date;
this.modified = date;
}
public ToDo(String description){
this();
this.description = description;
}
}
Listing 5-9com.apress.todo.domain.ToDo.java
清单 5-9 显示了修改后的ToDo类。该类正在使用@Document批注来标记为持久的;它还使用@Id来声明一个惟一键。
如果您有一个远程 MongoDB 服务器,您可以覆盖指向本地主机的缺省值。您可以转到您的application.properties文件并添加以下属性。
## MongoDB
spring.data.mongodb.host=my-remote-server
spring.data.mongodb.port=27017
spring.data.mongodb.username=myuser
spring.data.mongodb.password=secretpassword
接下来,你可以复习你的ToDoRepository和ToDoController类,它们应该根本不会改变。这就是使用 Spring Data 的美妙之处:您可以为交叉存储和所有以前的类重用您的模型,使开发更容易、更快。
运行和测试:ToDo 应用
现在是运行和测试 ToDo 应用的时候了。您可以在您的 IDE 中运行它。或者,如果您使用的是 Maven,请执行
./mvnw spring-boot:run
如果您使用的是 Gradle,请执行
./gradlew bootRun
要测试 ToDo 应用,您可以运行您的 ToDoClient 应用—就这样。切换持久性引擎非常容易,没有太多的麻烦。也许你想知道如果你想使用 map-reduce 或者更低级的操作会发生什么。嗯,你可以通过使用MongoTemplate类。
带有 Spring Data 的 ToDo 应用 MongoDB REST
如何创建一个 MongoDB REST 应用?您需要将spring-boot-starter-data-rest依赖项添加到您的pom.xml或build.gradle文件中——就这样!!当然,您需要删除控制器和验证包以及ToDoBuilder类;你只需要两节课。
请记住,Spring Data REST 公开了存储库端点,并使用 HATEOAS 作为媒体类型(HAL+JSON)。
注意
你可以在 Apress 网站或 GitHub 的 https://github.com/Apress/pro-spring-boot-2 找到这一部分的解决方案。该项目的名称是 todo-mongo-rest。
Spring Data Redis(Spring Data 重定向器)
Spring Data Redis 提供了一种配置和访问 Redis 服务器的简单方法。它提供了从低级到高级的抽象来与之交互,并遵循相同的 Spring Data 标准,提供了一个RedisTemplate类和基于存储库的持久性。
以下是 Spring Data Redis 的一些特性。
-
RedisTemplate给出了所有 Redis 操作的高级抽象。 -
通过发布/订阅发送消息。
-
Redis Sentinel 和 Redis 群集支持。
-
跨多个驱动程序使用连接包作为底层,如 Jedis 和莴苣。
-
储存库,通过使用
@EnableRedisRepositories进行排序和分页。 -
Redis 实现了 Spring 缓存,所以您可以使用 Redis 作为您的 web 缓存机制。
与 Spring Boot 的 Spring Data Redis
如果您想使用 Spring Data Redis,您只需要添加spring-boot-starter-data-redis依赖项来访问 Redis 服务器。
Spring Boot 使用自动配置来设置使用 Redis 服务器的默认值。如果您正在使用存储库特性,它会自动使用@EnableRedisRepositories(您不需要添加它)。
默认情况下,使用本地主机和端口 6379。当然,您可以通过更改application.properties文件中的spring.redis.*属性来覆盖默认设置。
任何带有 Spring Data Redis 的应用
您可以从头开始创建您的 ToDo 应用,或者在您的pom.xml或build.gradle文件中查看您需要哪些类以及必要的依赖项。
从头开始,进入你的浏览器并打开 Spring Initializr。将以下值添加到字段中。
-
组:
com.apress.todo -
神器:
todo-redis -
名称:
todo-redis -
包名:
com.apress.todo -
依赖关系:
Web, Lombok, Redis
您可以选择 Maven 或 Gradle 作为项目类型。然后,您可以按下 Generate Project 按钮,这将下载一个 ZIP 文件。将其解压缩并在您喜欢的 IDE 中导入项目(参见图 5-10 )。
图 5-10
您可以从todo-mongo项目中复制所有的类。在下一节中,我们将看到哪些类需要修改。
域模型:ToDo
打开ToDo域模型类并做相应修改(见清单 5-10 )。
package com.apress.todo.domain;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@RedisHash
public class ToDo {
@NotNull
@Id
private String id;
@NotNull
@NotBlank
private String description;
private LocalDateTime created;
private LocalDateTime modified
;
private boolean completed;
public ToDo(){
LocalDateTime date = LocalDateTime.now();
this.id = UUID.randomUUID().toString();
this.created = date;
this.modified = date;
}
public ToDo(String description){
this();
this.description = description;
}
}
Listing 5-10com.apress.todo.ToDo.java
清单 5-10 显示了您修改的唯一一个类。该类使用了将该类标记为持久的@RedisHash注释,并且还使用了@Id注释作为组合键的一部分。当插入一个 ToDo 时,有一个散列包含一个格式为class:id的键。对于这个应用,组合键类似于"com.apress.todo.domain.ToDo: bbee6e32-37f3-4d5a-8f29-e2c79a28c301"。
如果有远程 Redis 服务器,可以覆盖指向本地主机的默认值。您可以转到您的application.properties文件并添加以下属性。
## Redis - Remote
spring.redis.host=my-remote-server
spring.redis.port=6379
spring.redis.password=my-secret
你可以复习一下你的ToDoRepository和ToDoController类,应该根本不会改变;和以前一样。
运行和测试:ToDo 应用
现在是运行和测试这个 ToDo 应用的时候了。您可以在您的 IDE 中运行它。或者,如果您使用的是 Maven,请执行
./mvnw spring-boot:run
如果您使用的是 Gradle,请执行
./gradlew bootRun
要测试 ToDo 应用,您可以运行您的 ToDoClient 应用;仅此而已。如果你想使用一个不同的结构(比如集合、列表、字符串、ZSET)一个低级操作,你可以使用已经由 Spring Boot 设置和配置的RedisTemplate类。
注意
记住你可以从 Apress 网站或者 GitHub 的 https://github.com/Apress/pro-spring-boot-2 获得这本书的源代码。
Spring Boot 的更多数据功能
还有更多用于操作数据的特性和支持的引擎,从使用带有 jOOQ(Java Object-Oriented query atwww.jooq.org)的 DSL,jOOQ 从您的数据库生成 Java 代码,并允许您通过它自己的 DSL 以类型安全的方式构建 SQL 查询。
有很多方法可以进行数据库迁移,可以使用 Flyway ( https://flywaydb.org/ )或 Liquibase ( http://www.liquibase.org/ )在启动时运行。
多个数据源
Spring Boot 的一个重要特性(我认为这是一个必须具备的特性)是操纵多个DataSource实例,不管使用的是哪种持久技术。
正如您已经知道的,Spring Boot 提供了一个基于应用类路径的默认自动配置,您可以覆盖它,没有任何问题。要使用多个DataSource实例,它们可能指向不同的数据库和/或不同的引擎,您必须覆盖缺省值。如果你还记得第四章和的话,我们创建了一个完整而简单的 web 应用,需要设置:DataSource、EntityManager、TransactionManager、JpaVendor等等。如果我们想要使用多个数据源,我们需要添加相同的配置。换句话说,我们需要添加多个EntityManager、TransactionManager等等。
我们如何将此应用到 ToDo 应用中?回顾你在第四章所做的内容。仔细看看每个配置,就可以想象需要做什么了。
你可以在书中的源代码中找到解决方法。项目名称为todo-rest-2ds。这个项目包含所有的User和ToDo域类,它们将数据保存到自己的数据库中。
摘要
在本章中,您学习了不同的数据技术以及它们如何与 Spring Boot 配合使用。您还了解了 Spring Boot 使用其自动配置特性来基于类路径应用默认值。
您了解到,如果您有两个或更多的 SQL 驱动程序,其中一个是 H2、HSQL 或 Derby,如果您还没有定义一个DataSource实例,Spring Boot 将配置嵌入式数据库引擎。您看到了如何配置和覆盖一些数据默认值。您了解了 Spring Data 实现了一个模板模式来隐藏没有 Spring 框架通常会完成的所有复杂任务。
在下一章中,我们用反应式编程将 web 和数据提升一个层次,并探索 WebFlux 和反应式数据。