1. 概述
Spring MVC和Spring Data各自在简化应用程序开发方面做得很好。但是,如果我们把它们放在一起呢?
在本教程中,我们将了解Spring Data 的 Web 支持,以及它的解析器如何减少样板文件并使我们的控制器更具表现力。
在此过程中,我们将了解Querydsl以及它与Spring Data的集成。
2. 一点背景
Spring Data的Web支持是在标准Spring MVC平台之上实现的一组与Web相关的功能,旨在为控制器层添加额外的功能。
Spring Data Web 支持的功能是围绕几个解析器类构建的。解析器简化了控制器方法的实现,这些控制器方法与Spring 数据存储库互操作,并通过附加功能丰富了它们。
这些功能包括从存储库层获取域对象,而无需显式调用存储库实现,以及构建控制器响应,这些响应可以作为支持分页和排序的数据段发送到客户端。
此外,对采用一个或多个请求参数的控制器方法的请求可以在内部解析为Querydsl查询。
3. 一个演示 Spring 启动项目
为了了解如何使用 Spring Data Web 支持来改进控制器的功能,让我们创建一个基本的 Spring 启动项目。
我们演示项目的 Maven 依赖项是相当标准的,但有一些例外,我们将在后面讨论:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
在本例中,我们包含了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注释对其进行注释。
3.1. 域类
现在,让我们向项目添加一个简单的UserJPA 实体类,这样我们就可以有一个工作域模型来使用:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private final String name;
// standard constructor / getters / toString
}
3.2. 存储库层
为了保持代码简单,我们的演示 Spring 启动应用程序的功能将缩小到仅从 H2 内存数据库中获取一些用户实体。
Spring 引导可以轻松创建存储库实现,这些存储库实现提供开箱即用的最小 CRUD 功能。因此,让我们定义一个简单的存储库接口,该接口适用于用户JPA 实体:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User,Long> {}
UserRepository接口的定义本身并没有什么复杂的,除了它扩展了PagingAndSortingRepository。
这表示Spring MVC对数据库记录启用自动分页和排序功能。
3.3. 控制器层
现在,我们至少需要实现一个基本的 RESTful 控制器,它充当客户端和存储库层之间的中间层。
因此,让我们创建一个控制器类,该类在其构造函数中采用UserRepository实例,并添加单个方法用于按id 查找用户实体:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User findUserById(@PathVariable("id") User user) {
return user;
}
}
3.4. 运行应用程序
最后,让我们定义应用程序的主类,并使用几个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}
4.域类转换器类
目前,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 变量以及可解析的域类实例,我们自动触发了域对象的查找。
5.可分页处理程序方法参数解析器类
Spring MVC 支持在控制器和存储库中使用可分页类型。
简而言之,可分页实例是保存分页信息的对象。因此,当我们将可分页参数传递给控制器方法时,Spring MVC 使用PageableHandlerMethodArgumentResolver类将可分页实例解析为PageRequest对象,这是一个简单的可分页实现。
5.1. 使用可分页作为控制器方法参数
若要了解PageableHandlerMethodArgumentResolver类的工作原理,让我们向UserController类添加一个新方法:
@GetMapping("/users")
public Page<User> findAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
与findUserById() 方法相反,这里我们需要调用存储库实现来获取数据库中持久化的所有用户JPA 实体。
由于该方法采用可分页实例,因此它返回存储在Page<User>object 中的整个实体集的子集。
Page对象是对象列表的子列表,它公开了我们可用于检索有关分页结果的信息的几种方法,包括结果页的总数和要检索的页数。
默认情况下,Spring MVC 使用PageableHandlerMethodArgumentResolver类来构造一个PageRequest对象,具有以下请求参数:
- page:我们要检索的页面索引 – 参数索引为零,默认值为0
- 大小:我们要检索的页数 - 默认值为20
- sort:我们可以用来对结果进行排序的一个或多个属性,使用以下格式:property1,property2(,asc|desc) –例如,?sort=name&sort=email,asc
例如,对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,"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"));
}
5.2. 自定义分页参数
在许多情况下,我们需要自定义分页参数。实现此目的的最简单方法是使用@PageableDefault注释:
@GetMapping("/users")
public Page<User> findAllUsers(@PageableDefault(value = 2,page = 0) Pageable pageable) {
return userRepository.findAll(pageable);
}
或者,我们可以使用 PageRequest 的of()static factory 方法来创建自定义PageRequest 对象并将其传递给存储库方法:
@GetMapping("/users")
public Page<User> findAllUsers() {
Pageable pageable = PageRequest.of(0,5);
return userRepository.findAll(pageable);
}
第一个参数是从零开始的页面索引,而第二个参数是我们要检索的页面的大小。
在上面的示例中,我们创建了一个Userentity的PageRequest对象,从第一页 (0) 开始,该页面有5个条目。
此外,我们可以使用页面和大小请求参数构建一个 PageRequest对象:
@GetMapping("/users")
public Page<User> 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
}
6.排序处理程序方法参数解析器类
分页是有效管理大量数据库记录的实际方法。但是,就其本身而言,如果我们不能以某种特定的方式对记录进行排序,那就毫无用处了。
为此,Spring MVC 提供了SortHandlerMethodArgumentResolver类。解析程序根据请求参数或@SortDefault注释自动创建排序实例。
6.1. 使用排序控制器方法参数
为了清楚地了解SortHandlerMethodArgumentResolver类的工作原理,让我们将findAllUsersSortedByName() 方法添加到控制器类中:
@GetMapping("/sortedusers")
public Page<User> 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,{
"id": 5,"name": "Mary"
},{
"id": 3,"name": "Nataly"
},// continues with pageable metadata
}
6.2. 使用Sort.by()静态工厂方法
或者,我们可以使用Sort.by()static 工厂方法创建一个Sort对象,该方法采用一个非空、非空的String属性数组进行排序。
在本例中,我们将仅按name属性对记录进行排序:
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName() {
Pageable pageable = PageRequest.of(0,5,Sort.by("name"));
return userRepository.findAll(pageable);
}
当然,我们可以使用多个属性,只要它们在域类中声明即可。
6.3. 使用@SortDefault注释
同样,我们可以使用 @SortDefault注释并获得相同的结果:
@GetMapping("/sortedusers")
public Page<User> 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"));
}
7. 查询网页支持
正如我们在介绍中提到的,Spring Data Web 支持允许我们在控制器方法中使用请求参数来构建 Querydsl 的谓词类型并构造Querydsl 查询。
为了简单起见,我们将看到Spring MVC如何将请求参数转换为Querydsl BooleanExpression,而QuerydslBooleanExpression又传递给QuerydslPredicateExecutor。
为此,首先我们需要将querydsl-apt和querydsl-jpaMaven 依赖项添加到pom.xml文件中:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
接下来,我们需要重构我们的UserRepository接口,该接口还必须扩展QuerydslPredicateExecutor接口:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User,Long>,QuerydslPredicateExecutor<User> {
}
最后,让我们将以下方法添加到UserController类中:
@GetMapping("/filteredusers")
public Iterable<User> 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 支持是一个非常强大的功能,我们可以用来获取与给定条件匹配的数据库记录。
在所有情况下,整个过程都归结为仅调用具有不同请求参数的单个控制器方法。
8. 结论
在本教程中,我们深入了解了 Spring Web 支持的关键组件,并学习如何在演示的 Spring 启动项目中使用它。
像往常一样,本教程中显示的所有示例都可以在GitHub 上找到。
原文地址:https://blog.csdn.net/allway2
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。