Spring MVC和Spring Data各自在简化应用程序开发方面做得很好。但是,如果我们把它们放在一起呢?
在本教程中,我们将了解Spring Data 的 Web 支持,以及它的解析器如何减少样板文件并使我们的控制器更具表现力。
在此过程中,我们将了解Querydsl以及它与Spring Data的集成。
Spring Data的Web支持是在标准Spring MVC平台之上实现的一组与Web相关的功能,旨在为控制器层添加额外的功能。
Spring Data Web 支持的功能是围绕几个解析器类构建的。解析器简化了控制器方法的实现,这些控制器方法与Spring 数据存储库互操作,并通过附加功能丰富了它们。
这些功能包括从存储库层获取域对象,而无需显式调用存储库实现,以及构建控制器响应,这些响应可以作为支持分页和排序的数据段发送到客户端。
此外,对采用一个或多个请求参数的控制器方法的请求可以在内部解析为Querydsl查询。
为了了解如何使用 Spring Data Web 支持来改进控制器的功能,让我们创建一个基本的 Spring 启动项目。
我们演示项目的 Maven 依赖项是相当标准的,但有一些例外,我们将在后面讨论:
-
org.springframework.boot -
spring-boot-starter-data-jpa -
org.springframework.boot -
spring-boot-starter-web -
com.h2database -
h2 -
runtime -
org.springframework.boot -
spring-boot-starter-test -
test
在本例中,我们包含了spring-boot-starter-web,因为我们将使用它来创建 RESTful 控制器,spring-boot-starter-jpa 用于实现持久性层,以及 spring-boot-starter-test用于测试控制器 API。
由于我们将使用H2作为底层数据库,因此我们也包括了com.h2database。
让我们记住,spring-boot-starter-web默认启用 Spring Data Web 支持。因此,我们不需要创建任何额外的@Configuration类来让它在我们的应用程序中工作。
相反,对于非 Spring Boot 项目,我们需要定义一个@Configuration类,并用@EnableWebMvc和@EnableSpringDataWebSupport注释对其进行注释。
现在,让我们向项目添加一个简单的UserJPA 实体类,这样我们就可以有一个工作域模型来使用:
- @Entity
- @Table(name = "users")
- public class User {
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- private long id;
- private final String name;
-
- // standard constructor / getters / toString
- }
为了保持代码简单,我们的演示 Spring 启动应用程序的功能将缩小到仅从 H2 内存数据库中获取一些用户实体。
Spring 引导可以轻松创建存储库实现,这些存储库实现提供开箱即用的最小 CRUD 功能。因此,让我们定义一个简单的存储库接口,该接口适用于用户JPA 实体:
- @Repository
- public interface UserRepository extends PagingAndSortingRepository
{}
UserRepository接口的定义本身并没有什么复杂的,除了它扩展了PagingAndSortingRepository。
这表示Spring MVC对数据库记录启用自动分页和排序功能。
现在,我们至少需要实现一个基本的 RESTful 控制器,它充当客户端和存储库层之间的中间层。
因此,让我们创建一个控制器类,该类在其构造函数中采用UserRepository实例,并添加单个方法用于按id 查找用户实体:
- @RestController
- public class UserController {
- @GetMapping("/users/{id}")
- public User findUserById(@PathVariable("id") User user) {
- return user;
- }
- }
最后,让我们定义应用程序的主类,并使用几个User实体填充 H2 数据库:
- @SpringBootApplication
- public class Application {
- public static void main(String[] args) {
- SpringApplication.run(Application.class, args);
- }
- @Bean
- CommandLineRunner initialize(UserRepository userRepository) {
- return args -> {
- Stream.of("John", "Robert", "Nataly", "Helen", "Mary").forEach(name -> {
- User user = new User(name);
- userRepository.save(user);
- });
- userRepository.findAll().forEach(System.out::println);
- };
- }
- }
现在,让我们运行该应用程序。正如预期的那样,我们看到在启动时打印到控制台的持久用户实体列表:
- User{id=1, name=John}
- User{id=2, name=Robert}
- User{id=3, name=Nataly}
- User{id=4, name=Helen}
- User{id=5, name=Mary}
目前,UserController类只实现findUserById() 方法。
乍一看,方法实现看起来相当简单。但它实际上在幕后封装了许多Spring Data Web支持功能。
由于该方法将User实例作为参数,我们最终可能会认为我们需要在请求中显式传递域对象。但是,我们没有。
Spring MVC 使用DomainClassConverter类将 id路径变量转换为域类的id类型,并使用它来从存储库层获取匹配的域对象。无需进一步查找。
例如,对http://localhost:8080/users/1终结点的 GET HTTP 请求将返回以下结果:
- {
- "id":1,
- "name":"John"
- }
因此,我们可以创建一个集成测试并检查findUserById() 方法的行为:
- @Test
- public void whenGetRequestToUsersEndPointWithIdPathVariable_thenCorrectResponse() throws Exception {
- mockMvc.perform(MockMvcRequestBuilders.get("/users/{id}", "1")
- .contentType(MediaType.APPLICATION_JSON_UTF8))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"));
- }
- }
或者,我们可以使用 REST API 测试工具(例如Postman)来测试该方法。
DomainClassConverter的好处是,我们不需要在控制器方法中显式调用存储库实现。
通过简单地指定idpath 变量以及可解析的域类实例,我们自动触发了域对象的查找。
Spring MVC 支持在控制器和存储库中使用可分页类型。
简而言之,可分页实例是保存分页信息的对象。因此,当我们将可分页参数传递给控制器方法时,Spring MVC 使用PageableHandlerMethodArgumentResolver类将可分页实例解析为PageRequest对象,这是一个简单的可分页实现。
若要了解PageableHandlerMethodArgumentResolver类的工作原理,让我们向UserController类添加一个新方法:
- @GetMapping("/users")
- public Page
findAllUsers(Pageable pageable) { - return userRepository.findAll(pageable);
- }
与findUserById() 方法相反,这里我们需要调用存储库实现来获取数据库中持久化的所有用户JPA 实体。
由于该方法采用可分页实例,因此它返回存储在Page
Page对象是对象列表的子列表,它公开了我们可用于检索有关分页结果的信息的几种方法,包括结果页的总数和要检索的页数。
默认情况下,Spring MVC 使用PageableHandlerMethodArgumentResolver类来构造一个PageRequest对象,具有以下请求参数:
例如,对http://localhost:8080/user终结点的 GET 请求将返回以下输出:
- {
- "content":[
- {
- "id":1,
- "name":"John"
- },
- {
- "id":2,
- "name":"Robert"
- },
- {
- "id":3,
- "name":"Nataly"
- },
- {
- "id":4,
- "name":"Helen"
- },
- {
- "id":5,
- "name":"Mary"
- }],
- "pageable":{
- "sort":{
- "sorted":false,
- "unsorted":true,
- "empty":true
- },
- "pageSize":5,
- "pageNumber":0,
- "offset":0,
- "unpaged":false,
- "paged":true
- },
- "last":true,
- "totalElements":5,
- "totalPages":1,
- "numberOfElements":5,
- "first":true,
- "size":5,
- "number":0,
- "sort":{
- "sorted":false,
- "unsorted":true,
- "empty":true
- },
- "empty":false
- }
如我们所见,响应包括第一个,pageSize,totalElements和totalPagesJSON元素。这非常有用,因为前端可以使用这些元素轻松创建分页机制。
此外,我们可以使用集成测试来检查findAllUsers() 方法:
- @Test
- public void whenGetRequestToUsersEndPoint_thenCorrectResponse() throws Exception {
- mockMvc.perform(MockMvcRequestBuilders.get("/users")
- .contentType(MediaType.APPLICATION_JSON_UTF8))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.jsonPath("$['pageable']['paged']").value("true"));
- }
在许多情况下,我们需要自定义分页参数。实现此目的的最简单方法是使用@PageableDefault注释:
- @GetMapping("/users")
- public Page
findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) { - return userRepository.findAll(pageable);
- }
或者,我们可以使用 PageRequest 的of()static factory 方法来创建自定义PageRequest 对象并将其传递给存储库方法:
- @GetMapping("/users")
- public Page
findAllUsers() { - Pageable pageable = PageRequest.of(0, 5);
- return userRepository.findAll(pageable);
- }
第一个参数是从零开始的页面索引,而第二个参数是我们要检索的页面的大小。
在上面的示例中,我们创建了一个Userentity的PageRequest对象,从第一页 (0) 开始,该页面有5个条目。
此外,我们可以使用页面和大小请求参数构建一个 PageRequest对象:
- @GetMapping("/users")
- public Page
findAllUsers(@RequestParam("page") int page, - @RequestParam("size") int size, Pageable pageable) {
- return userRepository.findAll(pageable);
- }
使用此实现,对http://localhost:8080/users?page=0&size=2终结点的 GET 请求将返回User对象的第一页,结果页的大小将为 2:
- {
- "content": [
- {
- "id": 1,
- "name": "John"
- },
- {
- "id": 2,
- "name": "Robert"
- }
- ],
-
- // continues with pageable metadata
-
- }
分页是有效管理大量数据库记录的实际方法。但是,就其本身而言,如果我们不能以某种特定的方式对记录进行排序,那就毫无用处了。
为此,Spring MVC 提供了SortHandlerMethodArgumentResolver类。解析程序根据请求参数或@SortDefault注释自动创建排序实例。
为了清楚地了解SortHandlerMethodArgumentResolver类的工作原理,让我们将findAllUsersSortedByName() 方法添加到控制器类中:
- @GetMapping("/sortedusers")
- public Page
findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) { - return userRepository.findAll(pageable);
- }
在这种情况下,类将使用排序请求参数创建一个排序对象。
因此,对http://localhost:8080/sortedusers?sort=name终结点的 GET 请求将返回一个 JSON 数组,其中User对象列表按name属性排序:
- {
- "content": [
- {
- "id": 4,
- "name": "Helen"
- },
- {
- "id": 1,
- "name": "John"
- },
- {
- "id": 5,
- "name": "Mary"
- },
- {
- "id": 3,
- "name": "Nataly"
- },
- {
- "id": 2,
- "name": "Robert"
- }
- ],
-
- // continues with pageable metadata
-
- }
或者,我们可以使用Sort.by()static 工厂方法创建一个Sort对象,该方法采用一个非空、非空的String属性数组进行排序。
在本例中,我们将仅按name属性对记录进行排序:
- @GetMapping("/sortedusers")
- public Page
findAllUsersSortedByName() { - Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
- return userRepository.findAll(pageable);
- }
当然,我们可以使用多个属性,只要它们在域类中声明即可。
同样,我们可以使用 @SortDefault注释并获得相同的结果:
- @GetMapping("/sortedusers")
- public Page
findAllUsersSortedByName(@SortDefault(sort = "name", - direction = Sort.Direction.ASC) Pageable pageable) {
- return userRepository.findAll(pageable);
- }
最后,让我们创建一个集成测试来检查方法的行为:
- @Test
- public void whenGetRequestToSorteredUsersEndPoint_thenCorrectResponse() throws Exception {
- mockMvc.perform(MockMvcRequestBuilders.get("/sortedusers")
- .contentType(MediaType.APPLICATION_JSON_UTF8))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.jsonPath("$['sort']['sorted']").value("true"));
- }
正如我们在介绍中提到的,Spring Data Web 支持允许我们在控制器方法中使用请求参数来构建 Querydsl 的谓词类型并构造Querydsl 查询。
为了简单起见,我们将看到Spring MVC如何将请求参数转换为Querydsl BooleanExpression,而QuerydslBooleanExpression又传递给QuerydslPredicateExecutor。
为此,首先我们需要将querydsl-apt和querydsl-jpaMaven 依赖项添加到pom.xml文件中:
-
com.querydsl -
querydsl-apt -
com.querydsl -
querydsl-jpa
接下来,我们需要重构我们的UserRepository接口,该接口还必须扩展QuerydslPredicateExecutor接口:
- @Repository
- public interface UserRepository extends PagingAndSortingRepository
, - QuerydslPredicateExecutor
{ - }
最后,让我们将以下方法添加到UserController类中:
- @GetMapping("/filteredusers")
- public Iterable
getUsersByQuerydslPredicate(@QuerydslPredicate(root = User.class) - Predicate predicate) {
- return userRepository.findAll(predicate);
- }
尽管方法实现看起来相当简单,但它实际上在表面之下公开了许多功能。
假设我们要从数据库中获取与给定名称匹配的所有用户实体。我们可以通过调用该方法并在 URL 中指定名称请求参数来实现这一点:
http://localhost:8080/filteredusers?name=John
正如预期的那样,请求将返回以下结果:
- [
- {
- "id": 1,
- "name": "John"
- }
- ]
和以前一样,我们可以使用集成测试来检查getUsersByQuerydslPredicate()方法:
- @Test
- public void whenGetRequestToFilteredUsersEndPoint_thenCorrectResponse() throws Exception {
- mockMvc.perform(MockMvcRequestBuilders.get("/filteredusers")
- .param("name", "John")
- .contentType(MediaType.APPLICATION_JSON_UTF8))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("John"));
- }
这只是 Querydsl Web 支持工作原理的一个基本示例。但它实际上并没有揭示它的所有力量。
现在,假设我们要获取与给定id 匹配的用户实体。在这种情况下,我们只需要在 URL 中传递一个 id请求参数:
http://localhost:8080/filteredusers?id=2
在这种情况下,我们将得到以下结果:
- [
- {
- "id": 2,
- "name": "Robert"
- }
- ]
很明显,Querydsl Web 支持是一个非常强大的功能,我们可以用来获取与给定条件匹配的数据库记录。
在所有情况下,整个过程都归结为仅调用具有不同请求参数的单个控制器方法。
在本教程中,我们深入了解了 Spring Web 支持的关键组件,并学习如何在演示的 Spring 启动项目中使用它。
像往常一样,本教程中显示的所有示例都可以在GitHub 上找到。