introduce
This article focuses on how to use Spring MVC and Spring Data to implement paging in the RESTful API.
Discoverability of REST pages
Within the paging scope, satisfying the REST HATEOAS constraint means that the API client can discover the next page and the previous page based on the current page in the navigation To do this, we will use the Link HTTP response header, as well as the "next", "prev", "first" and "last" link relationship types.
Add a listener that will check whether navigation allows the next, previous, first, and last pages. It adds the relevant URI as a "link" to the HTTP response header.
void addLinkHeaderOnPagedResourceRetrieval( UriComponentsBuilder uriBuilder, HttpServletResponse response, Class clazz, int page, int totalPages, int size ){ String resourceName = clazz.getSimpleName().toString().toLowerCase(); uriBuilder.path( "/admin/" + resourceName ); // ... }
Next, we'll use StringJoiner to connect to each link. We will use the uri builder to build the uri. Let's see how we can continue to link to the next page:
StringJoiner linkHeader = new StringJoiner(", "); if (hasNextPage(page, totalPages)){ String uriForNextPage = constructNextPageUri(uriBuilder, page, size); linkHeader.add(createLinkHeader(uriForNextPage, "next")); }
Let's look at the logic of the constructNextPageUri method:
String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) { return uriBuilder.replaceQueryParam(PAGE, page + 1) .replaceQueryParam("size", size) .build() .encode() .toUriString(); }
We'll do a similar thing with the rest of the URIs we want to include.
Finally, we add the output as a response header:
response.addHeader("Link", linkHeader.toString());
Test paging
The code is as follows:
@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()); }
Test page discoverability
The test focuses on the location of the current page in the navigation and the different URIs that should be found from each location:
@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); }
Using Spring Data to implement REST paging
In Spring Data, if we need to return some results from the complete result set, we can use any Pageable repository method because it will always return Page Results are returned based on Page number, Page size, and sort direction.
Spring Data REST automatically recognizes URL parameters, such as page number, page size, sorting, etc.
To use the paging method of any repository, we need to extend pagingandsortingreposition:
public interface SubjectRepository extends PagingAndSortingRepository<Subject, Long>{}
If we call localhost:8080/subjects Spring, the page number, page size and sorting parameters will be added automatically:
"_links" : { "self" : { "href" : "http://localhost:8080/subjects{?page,size,sort}", "templated" : true } }
By default, the page size is 20, but we can change it by calling something like localhost:8080/subject?page=10.
If we want to implement paging to our own custom library API, we need to pass an additional pageable parameter and ensure that the API returns a Page:
@RestResource(path = "nameContains") public Page<Subject> findByNameContaining(@Param("name") String name, Pageable p);
Whenever we add a custom API, the / search endpoint is added to the generated link. Therefore, if we call localhost:8080/subjects/search, we will see an endpoint of paging function:
"findByNameContaining" : { "href" : "http://localhost:8080/subjects/search/nameContains{?name,page,size,sort}", "templated" : true }
All APIs that implement PagingAndSortingRepository will return a page. If we need to return a list of results from the page, the page's getContent() API provides a list of records obtained as a result of the Spring Data REST API.
Convert List to Page
Suppose we have a pageable object as input, but the information we need to retrieve is contained in a List, rather than a pagingandsortingreposition. In these cases, we may need to convert the List to Page.
For example, suppose we have a list of the results of a SOAP service:
List<Foo> list = getListOfFooFromSoapService();
We need to access the list at a specific location specified by the Pageable object that was sent to us. So let's define the start index:
int start = (int) pageable.getOffset();
End index:
int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size() : (start + pageable.getPageSize()));
With these two places, we can create a Page to get the list of elements between them:
Page<Foo> page = new PageImpl<Foo>(fooList.subList(start, end), pageable, fooList.size());
So we can return Page as a valid result.
Note that if we also want to support sorting, we need to sort the List's child lists before they can be sorted.
summary
This article demonstrated how to use Spring to implement paging in the REST API, and discussed how to set up and test discoverability.
Welcome to my public address: Qu Ling Feng, get exclusive exclusive learning resources and daily dry goods push.
If you are interested in my topic content, you can also follow my blog: sagowiec.com