REST Paginierung im Frühjahr

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

Dieses Tutorial konzentriert sich auf die Implementierung der Paginierung in einer REST-API unter Verwendung von Spring MVC und Spring Data.

2. Seite als Ressource vs Seite als Darstellung

Die erste Frage beim Entwerfen der Paginierung im Kontext einer RESTful-Architektur ist, ob die Seite als tatsächliche Ressource oder nur als Darstellung von Ressourcen betrachtet werden soll .

Das Behandeln der Seite selbst als Ressource führt zu einer Reihe von Problemen, z. B. dazu, dass Ressourcen zwischen Aufrufen nicht mehr eindeutig identifiziert werden können. Dies, zusammen mit der Tatsache, dass die Seite in der Persistenzschicht keine richtige Entität ist, sondern ein Halter, der bei Bedarf erstellt wird, macht die Auswahl einfach: Die Seite ist Teil der Darstellung .

Die nächste Frage im Paginierungsdesign im Kontext von REST ist, wo die Paging-Informationen enthalten sein sollen :

  • im URI-Pfad: / foo / page / 1
  • die URI-Abfrage: / foo? page = 1

Wenn Sie bedenken, dass eine Seite keine Ressource ist , ist das Codieren der Seiteninformationen im URI keine Option mehr.

Wir werden die Standardmethode zur Lösung dieses Problems verwenden, indem wir die Paging-Informationen in einer URI-Abfrage codieren.

3. Der Controller

Nun zur Implementierung - der Spring MVC Controller für die Paginierung ist unkompliziert :

@GetMapping(params = { "page", "size" }) public List findPaginated(@RequestParam("page") int page, @RequestParam("size") int size, UriComponentsBuilder uriBuilder, HttpServletResponse response) { Page resultPage = service.findPaginated(page, size); if (page > resultPage.getTotalPages()) { throw new MyResourceNotFoundException(); } eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent( Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size)); return resultPage.getContent(); }

In diesem Beispiel fügen wir die beiden Abfrageparameter Größe und Seite über @RequestParam in die Controller-Methode ein .

Alternativ hätten wir auch ein Pageable- Objekt verwenden können, das die Parameter für Seite , Größe und Sortierung automatisch zuordnet . Darüber hinaus bietet die Entität PagingAndSortingRepository sofort einsatzbereite Methoden, die auch die Verwendung von Pageable als Parameter unterstützen.

Wir injizieren auch sowohl die HTTP- Antwort als auch den UriComponentsBuilder , um die Erkennbarkeit zu verbessern - die wir über ein benutzerdefiniertes Ereignis entkoppeln. Wenn dies kein Ziel der API ist, können Sie das benutzerdefinierte Ereignis einfach entfernen.

Beachten Sie schließlich, dass der Schwerpunkt dieses Artikels nur auf dem REST und der Webebene liegt. Um einen tieferen Einblick in den Datenzugriffsteil der Paginierung zu erhalten, lesen Sie diesen Artikel über die Paginierung mit Spring Data.

4. Auffindbarkeit für die REST-Paginierung

Im Rahmen der Paginierung bedeutet das Erfüllen der HATEOAS-Einschränkung von REST , dass der Client der API die nächsten und vorherigen Seiten basierend auf der aktuellen Seite in der Navigation erkennen kann. Zu diesem Zweck verwenden wir den Link- HTTP-Header in Verbindung mit den Link-Beziehungstypen " next ", " prev ", " first " und " last " .

In REST ist die Erkennbarkeit ein Querschnittsthema , das nicht nur für bestimmte Vorgänge, sondern auch für Arten von Vorgängen gilt. Beispielsweise sollte der URI dieser Ressource jedes Mal, wenn eine Ressource erstellt wird, vom Client erkannt werden können. Da diese Anforderung für die Erstellung einer beliebigen Ressource relevant ist, werden wir sie separat behandeln.

Wir werden diese Bedenken mithilfe von Ereignissen entkoppeln, wie wir im vorherigen Artikel über die Erkennbarkeit eines REST-Service erörtert haben. Im Falle einer Paginierung wird das Ereignis - PaginnedResultsRetrievedEvent - in der Controller-Schicht ausgelöst. Anschließend implementieren wir die Erkennbarkeit mit einem benutzerdefinierten Listener für dieses Ereignis.

Kurz gesagt, der Listener prüft, ob die Navigation eine nächste , vorherige , erste und letzte Seite zulässt . Wenn dies der Fall ist, werden der Antwort die relevanten URIs als 'Link'-HTTP-Header hinzugefügt .

Gehen wir jetzt Schritt für Schritt. Der vom Controller übergebene UriComponentsBuilder enthält nur die Basis-URL (den Host, den Port und den Kontextpfad). Daher müssen wir die verbleibenden Abschnitte hinzufügen:

void addLinkHeaderOnPagedResourceRetrieval( UriComponentsBuilder uriBuilder, HttpServletResponse response, Class clazz, int page, int totalPages, int size ){ String resourceName = clazz.getSimpleName().toString().toLowerCase(); uriBuilder.path( "/admin/" + resourceName ); // ... }

Als nächstes verwenden wir einen StringJoiner, um jeden Link zu verketten. Wir werden den uriBuilder verwenden , um die URIs zu generieren. Mal sehen, wie wir mit dem Link zur nächsten Seite vorgehen würden :

StringJoiner linkHeader = new StringJoiner(", "); if (hasNextPage(page, totalPages)){ String uriForNextPage = constructNextPageUri(uriBuilder, page, size); linkHeader.add(createLinkHeader(uriForNextPage, "next")); }

Werfen wir einen Blick auf die Logik der Methode constructNextPageUri :

String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) { return uriBuilder.replaceQueryParam(PAGE, page + 1) .replaceQueryParam("size", size) .build() .encode() .toUriString(); }

Wir werden für den Rest der URIs, die wir einschließen möchten, ähnlich vorgehen.

Schließlich fügen wir die Ausgabe als Antwortheader hinzu:

response.addHeader("Link", linkHeader.toString());

Beachten Sie, dass ich der Kürze halber hier nur ein Teilcodebeispiel und den vollständigen Code eingefügt habe.

5. Testen Sie die Fahrpaginierung

Sowohl die Hauptlogik der Paginierung als auch die Auffindbarkeit werden durch kleine, fokussierte Integrationstests abgedeckt. Wie im vorherigen Artikel verwenden wir die REST-gesicherte Bibliothek, um den REST-Service zu nutzen und die Ergebnisse zu überprüfen.

Dies sind einige Beispiele für Paginierungsintegrationstests. Eine vollständige Testsuite finden Sie im GitHub-Projekt (Link am Ende des Artikels):

@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()); }

6. Auffindbarkeit der Fahrpaginierung testen

Das Testen, ob die Paginierung für einen Kunden erkennbar ist, ist relativ einfach, obwohl es viel zu tun gibt.

Die Tests konzentrieren sich auf die Position der aktuellen Seite in der Navigation und die verschiedenen URIs, die von jeder Position aus erkennbar sein sollten:

@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); }

Beachten Sie, dass sich hier der vollständige Low-Level-Code für extractURIByRel befindet, der für das Extrahieren der URIs nach rel- Beziehung verantwortlich ist.

7. Alle Ressourcen abrufen

Zum gleichen Thema wie Paginierung und Auffindbarkeit muss die Wahl getroffen werden, ob ein Client alle Ressourcen im System auf einmal abrufen darf oder ob der Client sie paginiert anfordern muss .

Wenn die Auswahl getroffen wird, dass der Client nicht alle Ressourcen mit einer einzigen Anforderung abrufen kann und die Paginierung nicht optional, sondern erforderlich ist, stehen mehrere Optionen für die Antwort auf eine Anforderung zum Abrufen aller Ressourcen zur Verfügung. Eine Möglichkeit besteht darin, einen 404 ( nicht gefunden ) zurückzugeben und den Link- Header zu verwenden, um die erste Seite erkennbar zu machen:

Link =; rel = "first"; rel = "last"

Eine andere Möglichkeit besteht darin, die Umleitung - 303 (siehe Andere ) - zur ersten Seite zurückzukehren. Eine konservativere Route wäre, einfach eine 405 ( Methode nicht zulässig) für die GET-Anforderung an den Client zurückzugeben .

8. REST-Paging mit Bereich- HTTP-Headern

A relatively different way of implementing pagination is to work with the HTTP Range headersRange, Content-Range, If-Range, Accept-Ranges – and HTTP status codes – 206 (Partial Content), 413 (Request Entity Too Large), 416 (Requested Range Not Satisfiable).

One view on this approach is that the HTTP Range extensions were not intended for pagination and that they should be managed by the Server, not by the Application. Implementing pagination based on the HTTP Range header extensions is nevertheless technically possible, although not nearly as common as the implementation discussed in this article.

9. Spring Data REST Pagination

In Spring Data, if we need to return a few results from the complete data set, we can use any Pageable repository method, as it will always return a Page. The results will be returned based on the page number, page size, and sorting direction.

Spring Data REST automatically recognizes URL parameters like page, size, sort etc.

To use paging methods of any repository we need to extend PagingAndSortingRepository:

public interface SubjectRepository extends PagingAndSortingRepository{}

If we call //localhost:8080/subjects Spring automatically adds the page, size, sort parameters suggestions with the API:

"_links" : { "self" : { "href" : "//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/subjects?page=10.

If we want to implement paging into our own custom repository API we need to pass an additional Pageable parameter and make sure that API returns a Page:

@RestResource(path = "nameContains") public Page findByNameContaining(@Param("name") String name, Pageable p);

Whenever we add a custom API a /search endpoint gets added to the generated links. So if we call //localhost:8080/subjects/search we will see a pagination capable endpoint:

"findByNameContaining" : { "href" : "//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 the list of the results from the Page, the getContent() API of Page provides the list of records fetched as a result of the Spring Data REST API.

The code in this section is available in the spring-data-rest project.

10. Convert a List into a Page

Let's suppose that we have a Pageable object as input, but the information that we need to retrieve is contained in a list instead of a PagingAndSortingRepository. In these cases, we may need to convert a List into a Page.

For example, imagine that we have a list of results from a SOAP service:

List list = getListOfFooFromSoapService();

We need to access the list in the specific positions specified by the Pageable object sent to us. So, let's define the start index:

int start = (int) pageable.getOffset();

And the end index:

int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size() : (start + pageable.getPageSize()));

Having these two in place, we can create a Page to obtain the list of elements between them:

Page page = new PageImpl(fooList.subList(start, end), pageable, fooList.size());

That's it! We can return now page as a valid result.

And note that if we also want to give support for sorting, we need to sort the list before sub-listing it.

11. Conclusion

This article illustrated how to implement Pagination in a REST API using Spring, and discussed how to set up and test Discoverability.

Wenn Sie sich eingehend mit der Paginierung in der Persistenzstufe befassen möchten, lesen Sie meine JPA- oder Hibernate-Paginierungs-Tutorials.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie im GitHub-Projekt - dies ist ein Maven-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.

REST unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs