知識庫 / Spring / Spring MVC RSS 訂閱

Spring Web 支持

REST,Spring Data,Spring MVC
HongKong
10
03:50 AM · Dec 06 ,2025

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
}

如我們所見,響應包括 firstpageSizetotalElementstotalPages 這四個 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);
}

或者,我們也可以使用 PageRequestof()靜態工廠方法來創建自定義的 PageRequest 對象並將其傳遞給存儲方法:

@GetMapping("/users")
public Page<User> findAllUsers() {
    Pageable pageable = PageRequest.of(0, 5);
    return userRepository.findAll(pageable);
}

第一個參數是基於零的頁面索引,第二個參數是我們要檢索的頁面大小。

在上面的示例中,我們創建了一個 PageRequest 對象,該對象使用 User 實體,從第一頁 (0) 開始,該頁面包含 5 條記錄。

此外,我們還可以使用 pagesize 請求參數構建一個 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-aptquerydsl-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 項目中使用它們。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.