REST Pagination in Spring – Spring中的REST分页

最后修改: 2012年 1月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

This tutorial will focus on the implementation of pagination in a REST API using Spring MVC and Spring Data.

本教程将重点介绍使用Spring MVC和Spring Data在REST API中实现分页。

2. Page as Resource vs Page as Representation

2.作为资源的网页与作为代表的网页

The first question when designing pagination in the context of a RESTful architecture is whether to consider the page an actual Resource or just a Representation of Resources.

在RESTful架构的背景下设计分页时,第一个问题是是否将页面视为实际的资源,或者只是资源的代表

Treating the page itself as a resource introduces a host of problems, such as no longer being able to uniquely identify resources between calls. This, coupled with the fact that, in the persistence layer, the page isn’t a proper entity but a holder that’s constructed when necessary, makes the choice straightforward; the page is part of the representation.

将页面本身视为资源会带来一系列的问题,例如在调用之间不再能够唯一地识别资源。再加上在持久化层中,页面并不是一个合适的实体,而是一个在必要时被构建的持有者,这使得选择变得简单明了;页面是表示法的一部分

The next question in the pagination design in the context of REST is where to include the paging information:

在REST的背景下,分页设计的下一个问题是在哪里包含分页信息

  • in the URI path: /foo/page/1
  • the URI query: /foo?page=1

Keeping in mind that a page isn’t a Resource, encoding the page information in the URI isn’t an option.

考虑到页面不是资源,在URI中对页面信息进行编码并不是一种选择。

We’ll use the standard way of solving this problem by encoding the paging information in a URI query.

我们将使用标准的方式来解决这个问题,将分页信息编码在URI查询中。

3. The Controller

3.控制器

Now for the implementation. The Spring MVC Controller for pagination is straightforward:

现在说说实现。用于分页的Spring MVC控制器是直接的

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

    return resultPage.getContent();
}

In this example, we’re injecting the two query parameters, size and page, in the Controller method via @RequestParam.

在这个例子中,我们通过@RequestParam.将两个查询参数sizepage注入控制器方法中。

Alternatively, we could have used a Pageable object, which maps the pagesize, and sort parameters automatically. In addition, the PagingAndSortingRepository entity provides out-of-the-box methods that support using Pageable as a parameter.

另外,我们可以使用一个Pageable对象,它可以自动映射pagesizesort参数。此外,PagingAndSortingRepository实体提供了支持使用Pageable作为参数的开箱方法。

We’re also injecting the Http Response and the UriComponentsBuilder to help with Discoverability, which we’re decoupling via a custom event. If that’s not a goal of the API, we can simply remove the custom event.

我们还注入了Http Response和UriComponentsBuilder以帮助实现可发现性,我们通过一个自定义事件将其解耦。如果这不是API的目标,我们可以简单地删除这个自定义事件。

Finally, note that the focus of this article is only the REST and web layer; to go deeper into the data access part of pagination, we can check out this article about Pagination with Spring Data.

最后,请注意,本文的重点只是REST和Web层;要深入了解分页的数据访问部分,我们可以查看这篇关于Spring Data分页的文章

4. Discoverability for REST Pagination

4.REST分页的可发现性

Within the scope of pagination, satisfying the HATEOAS constraint of REST means enabling the client of the API to discover the next and previous pages based on the current page in the navigation. For this purpose, we’ll use the Link HTTP header, coupled with the “next,” “prev,” “first,” and “last” link relation types.

在分页的范围内,满足REST的HATEOAS约束意味着使API的客户端能够根据导航中的当前页面发现下一个前一个页面。为此,我们将使用Link HTTP头,再加上”next,” “prev,” “first,” 和 “last” 链接关系类型

In REST, Discoverability is a cross-cutting concern, applicable not only to specific operations, but to types of operations. For example, each time a Resource is created, the URI of that Resource should be discoverable by the client. Since this requirement is relevant for the creation of ANY Resource, we’ll handle it separately.

在REST中,可发现性是一个跨领域的问题,不仅适用于特定的操作,而且适用于操作的类型。例如,每次创建资源时,该资源的URI应该可以被客户端发现。由于这一要求与任何资源的创建有关,我们将单独处理它。

We’ll decouple these concerns using events, as we discussed in the previous article focusing on Discoverability of a REST Service. In the case of pagination, the event, PaginatedResultsRetrievedEvent, is fired in the controller layer. Then we’ll implement discoverability with a custom listener for this event.

正如我们在前一篇文章中所讨论的那样,我们将使用事件对这些关注点进行解耦,该文章主要关注REST服务的可发现性。在分页的情况下,事件PaginatedResultsRetrievedEvent,在控制器层被触发。然后我们将通过该事件的自定义监听器来实现可发现性。

In short, the listener will check if the navigation allows for nextpreviousfirst and last pages. If it does, it’ll add the relevant URIs to the response as a ‘Link’ HTTP Header.

简而言之,监听器将检查导航是否允许nextpreviousfirstlast页。如果允许,它将将相关的URI作为 “链接 “HTTP头添加到响应中

Now let’s go step by step. The UriComponentsBuilder passed from the controller contains only the base URL (the host, the port and the context path). Therefore, we’ll have to add the remaining sections:

现在让我们一步一步来。从控制器传来的UriComponentsBuilder只包含基本URL(主机、端口和上下文路径)。因此,我们将不得不添加其余部分。

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 a StringJoiner to concatenate each link. We’ll use the uriBuilder to generate the URIs. Let’s see how we proceed with the link to the next page:

接下来,我们将使用StringJoiner来连接每个链接。我们将使用uriBuilder来生成URI。让我们看看我们如何进行到下一个页的链接。

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

Let’s have a look at the logic of the constructNextPageUri method:

让我们看一下constructNextPageUri方法的逻辑。

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

We’ll proceed similarly for the rest of the URIs that we want to include.

我们将以类似的方式处理我们想要包括的其余URI。

Finally, we’ll add the output as a response header:

最后,我们将输出作为响应头添加。

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

Note that, for brevity, only a partial code sample is included, and the full code is here.

请注意,为简洁起见,只包括部分代码样本,完整代码在此

5. Test Driving Pagination

5.试用分页法

Both the main logic of pagination and discoverability are covered by small, focused integration tests. As in the previous article, we’ll use the REST-assured library to consume the REST service and verify the results.

分页和可发现性的主要逻辑都由小型的、集中的集成测试来覆盖。正如上一篇文章,我们将使用REST-assured 库来消费REST服务并验证结果。

These are a few examples of pagination integration tests; for a full test suite, check out the GitHub project (link at the end of the article):

这些是分页集成测试的几个例子;要想获得完整的测试套件,请查看GitHub项目(文末有链接)。

@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. Test Driving Pagination Discoverability

6.测试驾驶分页的可发现性

Testing that pagination is discoverable by a client is relatively straightforward, although there’s a lot of ground to cover.

测试分页是否可以被客户发现是相对简单的,尽管有很多地方需要覆盖。

The tests will focus on the position of the current page in navigation, and the different URIs that should be discoverable from each position:

测试的重点是当前页面在导航中的位置,以及每个位置应该可以发现的不同URI。

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

Note that the full low-level code for extractURIByRel, responsible for extracting the URIs by rel relation, is here.

请注意,extractURIByRel的完整底层代码,负责通过rel关系提取URI,在这里

7. Getting All Resources

7.获得所有的资源

On the same topic of pagination and discoverability, the choice must be made if a client is allowed to retrieve all the Resources in the system at once, or if the client must ask for them paginated.

关于分页和可发现性的同一主题,必须做出选择,是允许客户一次性检索系统中的所有资源,还是客户必须要求将其分页

If it’s decided that the client can’t retrieve all Resources with a single request, and pagination is required, then several options are available for the response to get a request. One option is to return a 404 (Not Found) and use the Link header to make the first page discoverable:

如果决定客户端不能用一个请求来检索所有的资源,并且需要分页,那么对于得到一个请求的响应有几种选择。一种选择是返回404(Not Found),并使用Link头来使第一页可被发现。

Link=<http://localhost:8080/rest/api/admin/foo?page=0&size=2>; rel=”first”, <http://localhost:8080/rest/api/admin/foo?page=103&size=2>; rel=”last”

Link=<http://localhost:8080/rest/api/admin/foo?page=0&size=2>; rel=”第一”, <http://localhost:8080/rest/api/admin/foo?page=103&size=2>; rel=”最后”

Another option is to return a redirect, 303 (See Other), to the first page. A more conservative route would be to simply return to the client a 405 (Method Not Allowed) for the GET request.

另一个选择是返回一个重定向,303 (See Other),到第一个页面。一个更保守的方法是简单地给客户返回一个405(不允许的方法)的GET请求。

8. REST Paging With Range HTTP Headers

8.使用Range HTTP头的REST分页

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

实现分页的一个相对不同的方法是使用HTTP Range头信息, Range, Content-Range, If-Range,Accept-Ranges,HTTP状态代码,206(部分内容),413(Request Entity Too Large)和416(Requested Range Not Satisfiable)。

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

对这种方法的一种看法是,HTTP范围扩展不是用来分页的,它们应该由服务器管理,而不是由应用程序管理。基于HTTP范围头的扩展实现分页在技术上是可行的,尽管不像本文讨论的实现那样普遍。

9. Spring Data REST Pagination

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中,如果我们需要从完整的数据集中返回一些结果,我们可以使用任何Pageable存储库方法,因为它总是会返回一个Page.结果将根据页数、页面大小和排序方向返回。

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

Spring Data REST自动识别URL参数,如page, size, sort

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

为了使用任何存储库的分页方法,我们需要扩展PagingAndSortingRepository:

public interface SubjectRepository extends PagingAndSortingRepository<Subject, Long>{}

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

如果我们调用http://localhost:8080/subjects,Spring会自动添加页面、大小、排序参数建议与API。

"_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 http://localhost:8080/subjects?page=10.

默认情况下,页面大小为20,但我们可以通过调用类似http://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:

如果我们想在自己的自定义版本库API中实现分页,我们需要传递一个额外的Pageable参数,并确保API返回一个Page:

@RestResource(path = "nameContains")
public Page<Subject> 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 http://localhost:8080/subjects/search, we’ll see a pagination capable endpoint:

每当我们添加一个自定义的API,一个/search端点会被添加到生成的链接中。因此,如果我们调用http://localhost:8080/subjects/search,我们将看到一个能够分页的端点。

"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 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.

所有实现PagingAndSortingRepository的API都将返回一个Page。如果我们需要返回Page的结果列表,Page的getContent()API提供了作为Spring Data REST API结果获取的记录列表。

10. Convert a List into a Page

10.将列表转换成

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.

假设我们有一个Pageable对象作为输入,但是我们需要检索的信息是包含在一个列表中,而不是一个PagingAndSortingRepository。在这种情况下,我们可能需要List转换为Page

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

例如,设想我们有一个来自SOAP>服务的结果列表。

List<Foo> 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:

我们需要在发送给我们的Pageable对象所指定的特定位置访问该列表。所以让我们定义起始索引。

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<Foo> page 
  = new PageImpl<Foo>(fooList.subList(start, end), pageable, fooList.size());

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

这就是了!我们现在可以将page作为一个有效的结果返回。

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

并且注意,如果我们还想给排序提供支持,我们需要在子列表之前对列表进行排序

11. Conclusion

11.结论

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

这篇文章说明了如何使用Spring在REST API中实现分页,并讨论了如何设置和测试可发现性。

If we want to go in depth on pagination in the persistence level, we can check out the JPA or Hibernate pagination tutorials.

如果我们想深入了解持久层中的分页,我们可以查看JPAHibernate的分页教程。

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

所有这些例子和代码片段的实现都可以在GitHub项目中找到–这是一个基于Maven的项目,所以应该很容易导入并按原样运行。