本文將重點介紹如何使用 Spring MVC 和 Spring Data 在 RESTful API 中實現分頁。java
在分頁範圍內,知足 REST 的 HATEOAS 約束,意味着使 API 的客戶端可以基於導航中的當前頁面發現下一頁和上一頁。 爲此,咱們將使用Link HTTP 響應頭,以及 「next」,「prev」,「first」 和 「last」 連接關係類型。
添加一個偵聽器,監聽器將檢查導航是否容許下一頁,上一頁,第一頁和最後一頁。它將相關的 URI 做爲 「連接」 添加到 HTTP 響應頭中。api
void addLinkHeaderOnPagedResourceRetrieval( UriComponentsBuilder uriBuilder, HttpServletResponse response, Class clazz, int page, int totalPages, int size ){ String resourceName = clazz.getSimpleName().toString().toLowerCase(); uriBuilder.path( "/admin/" + resourceName ); // ... }
接下來,咱們將使用 StringJoiner 鏈接每一個連接。咱們將使用 uriBuilder 生成 uri。讓咱們看看咱們如何繼續連接到下一頁:dom
StringJoiner linkHeader = new StringJoiner(", "); if (hasNextPage(page, totalPages)){ String uriForNextPage = constructNextPageUri(uriBuilder, page, size); linkHeader.add(createLinkHeader(uriForNextPage, "next")); }
讓咱們來看看 constructNextPageUri 方法的邏輯:學習
String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) { return uriBuilder.replaceQueryParam(PAGE, page + 1) .replaceQueryParam("size", size) .build() .encode() .toUriString(); }
咱們將對但願包含的其他 uri 進行相似的處理。
最後,咱們將輸出添加爲響應頭:測試
response.addHeader("Link", linkHeader.toString());
代碼以下:ui
@Test public void whenResourcesAreRetrievedPaged_then200IsReceived(){ Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2"); assertThat(response.getStatusCode(), is(200)); } @Test public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){ String url = getFooURL() + "?page=" + randomNumeric(5) + "&size=2"; Response response = RestAssured.get.get(url); assertThat(response.getStatusCode(), is(404)); } @Test public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){ createResource(); Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2"); assertFalse(response.body().as(List.class).isEmpty()); }
測試的重點是當前頁面在導航中的位置,以及應該從每一個位置發現的不一樣 uri:url
@Test public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){ Response response = RestAssured.get(getFooURL()+"?page=0&size=2"); String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next"); assertEquals(getFooURL()+"?page=1&size=2", uriToNextPage); } @Test public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){ Response response = RestAssured.get(getFooURL()+"?page=0&size=2"); String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev"); assertNull(uriToPrevPage ); } @Test public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){ Response response = RestAssured.get(getFooURL()+"?page=1&size=2"); String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev"); assertEquals(getFooURL()+"?page=0&size=2", uriToPrevPage); } @Test public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){ Response first = RestAssured.get(getFooURL()+"?page=0&size=2"); String uriToLastPage = extractURIByRel(first.getHeader("Link"), "last"); Response response = RestAssured.get(uriToLastPage); String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next"); assertNull(uriToNextPage); }
在 Spring Data 中,若是咱們須要從完整的結果集中返回一些結果,則可使用任何 Pageable 存儲庫方法,由於它將始終返回 Page。 將根據頁碼,頁面大小和排序方向返回結果。
Spring Data REST 自動識別 URL 參數,例如頁碼,頁面大小,排序等。
要使用任何存儲庫的分頁方法,咱們須要擴展 PagingAndSortingRepository:code
public interface SubjectRepository extends PagingAndSortingRepository<Subject, Long>{}
若是咱們調用 localhost:8080/subjects Spring 會自動添加頁碼,頁面大小,排序參數:對象
"_links" : { "self" : { "href" : "http://localhost:8080/subjects{?page,size,sort}", "templated" : true } }
默認狀況下,頁面大小是20,可是咱們能夠經過調用相似於 localhost:8080/subject?page=10 這樣的東西來更改它。
若是咱們想實現分頁到咱們本身的自定義庫 API,咱們須要傳遞一個額外的可分頁參數,並確保 API 返回一個 Page:blog
@RestResource(path = "nameContains") public Page<Subject> findByNameContaining(@Param("name") String name, Pageable p);
每當咱們添加自定義API時,就會將 /search 端點添加到生成的連接中。所以,若是咱們調用 localhost:8080/subjects/search,咱們將看到一個分頁功能的端點:
"findByNameContaining" : { "href" : "http://localhost:8080/subjects/search/nameContains{?name,page,size,sort}", "templated" : true }
全部實現 PagingAndSortingRepository 的 api 都將返回一個頁面。若是咱們須要返回來自頁面的結果列表,頁面的 getContent() API 提供了做爲 Spring Data REST API 的結果而獲取的記錄列表。
假設咱們有一個可分頁的對象做爲輸入,可是咱們須要檢索的信息包含在一個 List 中,而不是一個 PagingAndSortingRepository。在這些狀況下,咱們可能須要將 List 轉換爲 Page。
例如,假設咱們有一個 SOAP 服務的結果列表:
List<Foo> list = getListOfFooFromSoapService();
咱們須要在發送給咱們的 Pageable 對象指定的特定位置訪問列表。那麼,讓咱們定義開始索引:
int start = (int) pageable.getOffset();
結束索引:
int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size() : (start + pageable.getPageSize()));
有了這兩個地方,咱們能夠建立一個 Page 來獲取它們之間的元素列表:
Page<Foo> page = new PageImpl<Foo>(fooList.subList(start, end), pageable, fooList.size());
這樣咱們就能夠返回 Page 做爲一個有效的結果。
注意,若是咱們還但願支持排序,咱們須要在將 List 的子列表以前對其進行排序。
本文演示瞭如何使用 Spring 在 REST API 中實現分頁,並討論瞭如何設置和測試可發現性。
歡迎關注個人公衆號:曲翎風,得到獨家整理的學習資源和平常乾貨推送。
若是您對個人專題內容感興趣,也能夠關注個人博客: sagowiec.com