1. 概述
Spring MVC 和 Spring Data 都能很好地簡化各自的應用開發。但是,如果我們將它們結合起來呢?
在本教程中,我們將探討 Spring Data 的 Web 支持 以及 它的 解析器 能夠減少樣板代碼並使我們的控制器更具表現力。
在學習過程中,我們還會窺探一下 Querydsl 以及它與 Spring Data 的集成。
2. 背景介紹
Spring Data 的 Web 支持是一個構建在標準 Spring MVC 平台之上的,旨在為控制器層添加額外功能的 Web 相關特性集。
Spring Data Web 支持的功能建立在多個 解析器 類之上。解析器簡化了控制器方法與 Spring Data 存儲庫的交互實現,並同時為它們添加了額外的功能。
這些功能包括:從存儲庫層檢索領域對象,而無需顯式調用存儲庫實現,以及 構建可以作為數據片段發送給客户端,並支持分頁和排序的控制器響應。
此外,針對接受一個或多個請求參數的控制器方法請求可以內部解析為 Querydsl 查詢。
3. 一個 Spring Boot 項目演示
為了理解如何利用 Spring Data Web 支持來改進我們的控制器功能,我們創建一個基本的 Spring Boot 項目。
我們的演示項目的 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. 領域模型
現在,讓我們為項目添加一個簡單的 User JPA實體類,以便我們擁有一個可供測試的運行中的領域模型:
@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 Boot 應用程序的功能將被限制為僅從 H2 內存數據庫中檢索一些 User 實體。
Spring Boot 使得創建提供最小 CRUD 功能的存儲層實現變得容易。因此,讓我們定義一個與 User JPA 實體一起工作的簡單存儲層接口:
@Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {}UserRepository 接口本身並沒有特別複雜的定義,它只是繼承了 PagingAndSortingRepository。
這表明 Spring MVC 能夠自動啓用數據庫記錄的分頁和排序功能。
3.3. 控制器層
現在,我們需要實現至少一個基本的 RESTful 控制器,作為客户端和存儲層之間的中間層。
因此,讓我們創建一個控制器類,在構造函數中注入一個 UserRepository 實例,並添加一個用於通過 id 查找 User 實體的方法:
@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 實體列表:
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. DomainClassConverter 類
目前,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 的優點在於,我們不需要在控制器方法中顯式地調用存儲庫實現。
只需指定 路徑變量,並附帶一個可解析的領域類實例,我們就能自動觸發領域對象的查找。
5. PageableHandlerMethodArgumentResolver 類
Spring MVC 支持在控制器和存儲庫中使用 Pageable 類型。簡單來説,一個 Pageable 實例是一個包含分頁信息的對象。因此,當我們向控制器方法傳遞一個 Pageable 參數時,Spring MVC 使用 PageableHandlerMethodArgumentResolver 類 來將 Pageable 實例解析為 PageRequest 對象,,這是一個簡單的 Pageable 實現。
5.1. 使用 Pageable 作為控制器方法參數
為了理解 PageableHandlerMethodArgumentResolver 類的工作原理,我們向 UserController 類添加一個新方法:
@GetMapping("/users")
public Page<User> findAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
與 findUserById() 方法不同,這裏我們需要調用 repository 實現來獲取數據庫中持久化的所有 User JPA 實體。
由於該方法接受一個 Pageable 實例,它將返回整個實體集合的一個子集,存儲在一個 Page<User> 對象中。
一個 Page 對象是一個對象列表的子列表,它暴露了我們可以用於檢索分頁結果的多個方法,包括總結果頁數和我們正在檢索的頁碼,
默認情況下,Spring MVC 使用 PageableHandlerMethodArgumentResolver 類來構造一個 PageRequest 對象,具有以下請求參數:
- page: 我們想要檢索的頁碼索引 – 該參數是零索引且默認值為 0
- size: 我們想要檢索的頁數 – 默認值為 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,
"unsorted":true,
"empty":true
},
"empty":false
}
如我們所見,響應包括 first、pageSize、totalElements 和 totalPages 這四個 JSON 元素。這對於前端來説非常有用,因為它們可以輕鬆地使用這些元素來創建分頁機制。
此外,我們還可以使用集成測試來檢查 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. 自定義分頁參數
在許多情況下,我們需要自定義分頁參數。最簡單的方法是使用 <a href="https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/web/PageableDefault.html" rel="noopener noreferrer">@PageableDefault</a> 註解:
@GetMapping("/users")
public Page<User> findAllUsers(@PageableDefault(value = 2, page = 0) Pageable pageable) {
return userRepository.findAll(pageable);
}或者,我們也可以使用 PageRequest 的 of()靜態工廠方法來創建自定義的 PageRequest 對象並將其傳遞給存儲方法:
@GetMapping("/users")
public Page<User> findAllUsers() {
Pageable pageable = PageRequest.of(0, 5);
return userRepository.findAll(pageable);
}第一個參數是基於零的頁面索引,第二個參數是我們要檢索的頁面大小。
在上面的示例中,我們創建了一個 PageRequest 對象,該對象使用 User 實體,從第一頁 (0) 開始,該頁面包含 5 條記錄。
此外,我們還可以使用 page 和 size 請求參數構建一個 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. SortHandlerMethodArgumentResolver 類
分頁是高效管理大量數據庫記錄的常用方法。但單獨使用,如果不能以某種特定方式對記錄進行排序,則毫無用處。
為此,Spring MVC 提供了 SortHandlerMethodArgumentResolver 類。該解析器自動從請求參數或從 Sort 標註中創建 @SortDefault 實例。
6.1. 使用 sort 控制器方法參數
為了更清楚地瞭解 SortHandlerMethodArgumentResolver 類的工作原理,讓我們為控制器類添加 findAllUsersSortedByName() 方法:
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName(@RequestParam("sort") String sort, Pageable pageable) {
return userRepository.findAll(pageable);
}在這種情況下,SortHandlerMethodArgumentResolver 類將通過使用 sort 請求參數創建 Sort 對象。
因此,對 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
}6.2. 使用 <em Sort.by()</em> 靜態工廠方法
或者,我們可以使用 <em Sort.by()</em> 靜態工廠方法創建 <em Sort</em> 對象,該方法接受一個非空、非空的 <em array</em>,其中包含要排序的 <em String</em> 屬性。
在這種情況下,我們將僅按 <em name</em> 屬性對記錄進行排序。
@GetMapping("/sortedusers")
public Page<User> findAllUsersSortedByName() {
Pageable pageable = PageRequest.of(0, 5, Sort.by("name"));
return userRepository.findAll(pageable);
}當然,只要在領域類中聲明,我們也可以使用多個屬性。
6.3. 使用 <em @SortDefault> 註解
同樣,我們可以使用 <em @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. Querydsl Web 支持
正如我們在引言中所提到的,Spring Data 的 Web 支持允許我們使用控制器方法中的請求參數來構建 Querydsl 的 Predicate 類型,並構建 Querydsl 查詢。
為了保持簡潔,我們將僅查看 Spring MVC 如何將請求參數轉換為 Querydsl 的 BooleanExpression, 隨後該表達式會被傳遞給 QuerydslPredicateExecutor。
要實現這一點,首先我們需要在 pom.xml 文件中添加 querydsl-apt 和 querydsl-jpa Maven 依賴項:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.1.0</version>
<classifier>jakarta</classifier>
</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);
}雖然該方法的實現看起來相當簡單,但實際上它在表面之下暴露了大量的功能。
假設我們要從數據庫中檢索所有名稱與給定名稱匹配的 User 實體。我們可以通過簡單地調用該方法並指定 URL 中的 name 請求參數來實現:
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 匹配的 User 實體。在這種情況下,我們只需要在 URL 中傳遞一個 id 請求參數:
http://localhost:8080/filteredusers?id=2
在這種情況下,我們會得到以下結果:
[
{
"id": 2,
"name": "Robert"
}
]很明顯,Querydsl 的 Web 支持是一個非常強大的功能,我們可以利用它來檢索滿足特定條件的數據庫記錄。
在所有情況下,整個過程都簡化為只需調用一個控制器方法,並傳入不同的請求參數。
8. 結論
在本教程中,我們深入研究了 Spring Web 支持的關鍵組件,並學習瞭如何在演示 Spring Boot 項目中使用它們。