Статьи

REST Pagination весной

Это седьмая из серии статей о настройке защищенного веб-сервиса RESTful с использованием Spring 3.1 и Spring Security 3.1 с настройкой на основе Java. Эта статья будет посвящена реализации нумерации страниц в веб-сервисе RESTful.

Серия «Отдых с весной»:

Вы можете проверить все REST с Spring Series здесь .

Страница как ресурс против страницы как представление

Первый вопрос при проектировании нумерации страниц в контексте архитектуры RESTful заключается в том, следует ли считать страницу реальным ресурсом или просто представлением ресурсов . Если рассматривать саму страницу как ресурс, возникает множество проблем, таких как невозможность уникальной идентификации ресурсов между вызовами. Это в сочетании с тем фактом, что вне контекста RESTful, страницу нельзя считать надлежащей сущностью, но держатель, который создается при необходимости, делает выбор простым: страница является частью представления .

Следующий вопрос в дизайне нумерации страниц в контексте REST — где включить информацию о поисковом вызове:

  • в пути URI : / foo / page / 1
  • URI запроса : ? / Foo страница = 1

Принимая во внимание, что страница не является ресурсом , кодирование информации о странице в URI больше не вариант.

Информация о странице в запросе URI

Кодирование информации подкачки в запросе URI является стандартным способом решения этой проблемы в службе RESTful. Этот подход, однако, имеет один недостаток — он врезается в пространство запросов для реальных запросов:

/ foo? page = 1 & size = 10

Контроллер

Теперь для реализации — Spring MVC Controller для разбивки на страницы прост:

@RequestMapping( value = "admin/foo",params = { "page", "size" },method = GET )
@ResponseBody
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 ResourceNotFoundException();
   }
   eventPublisher.publishEvent( new PaginatedResultsRetrievedEvent< Foo >
    ( Foo.class, uriBuilder, response, page, resultPage.getTotalPages(), size ) );

   return resultPage.getContent();
}

Два параметра запроса определены в отображении запроса и введены в метод контроллера через @RequestParam ; HTTP-ответ и Spring UriComponentsBuilder вводятся в метод Controller для включения в событие, так как оба будут необходимы для реализации обнаруживаемости .

Обнаруживаемость для пагинации REST

В рамках разбивки на страницы удовлетворение ограничения HATEOAS для REST означает предоставление клиенту API возможности обнаруживать следующие и предыдущие страницы на основе текущей страницы в навигации. Для этой цели будет использоваться HTTP-заголовок Link в сочетании с официальными типами отношений ссылки « next », « prev », « first » и « last ».

В REST Обнаружение является сквозной задачей , применимой не только к конкретным операциям, но и к типам операций. Например, каждый раз при создании ресурса URI этого ресурса должен быть обнаружен клиентом. Поскольку это требование относится к созданию ЛЮБОГО ресурса, его следует рассматривать отдельно и отделить от основного потока контроллера.

В Spring это разделение достигается с событиями , как подробно обсуждалось в предыдущей статье, посвященной Обнаруживаемости сервиса RESTful. В случае разбиения на страницы событие — PaginatedResultsRetrievedEvent — было запущено в контроллере, и обнаружение достигается в слушателе для этого события:

void addLinkHeaderOnPagedResourceRetrieval(
 UriComponentsBuilder uriBuilder, HttpServletResponse response,
 Class clazz, int page, int totalPages, int size ){

   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );

   StringBuilder linkHeader = new StringBuilder();
   if( hasNextPage( page, totalPages ) ){
      String uriNextPage = constructNextPageUri( uriBuilder, page, size );
      linkHeader.append( createLinkHeader( uriForNextPage, REL_NEXT ) );
   }
   if( hasPreviousPage( page ) ){
      String uriPrevPage = constructPrevPageUri( uriBuilder, page, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriForPrevPage, REL_PREV ) );
   }
   if( hasFirstPage( page ) ){
      String uriFirstPage = constructFirstPageUri( uriBuilder, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriForFirstPage, REL_FIRST ) );
   }
   if( hasLastPage( page, totalPages ) ){
      String uriLastPage = constructLastPageUri( uriBuilder, totalPages, size );
      appendCommaIfNecessary( linkHeader );
      linkHeader.append( createLinkHeader( uriForLastPage, REL_LAST ) );
   }
   response.addHeader( HttpConstants.LINK_HEADER, linkHeader.toString() );
}

Короче говоря, логика прослушивателя проверяет, разрешает ли навигация следующую, предыдущую, первую и последнюю страницы, и, если это так, добавляет соответствующие URI в заголовок HTTP ссылки. Это также гарантирует, что тип отношения ссылки является правильным — «следующий», «предыдущий», «первый» и «последний». Это единственная ответственность слушателя ( полный код здесь ).

Тест вождения Пагинация

Как основная логика разбиения на страницы, так и обнаруживаемость должны быть подробно описаны в небольших целенаправленных интеграционных тестах; Как и в предыдущей статье , библиотека с гарантированным отдыхом используется для использования службы REST и проверки результатов.

Вот несколько примеров тестов на интеграцию страниц; для полного набора тестов проверьте проект github (ссылка в конце статьи):

@Test
public void whenResourcesAreRetrievedPaged_then200IsReceived(){
   Response response = givenAuth().get( paths.getFooURL() + "?page=1&size=10" );

   assertThat( response.getStatusCode(), is( 200 ) );
}
@Test
public void
 whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
   Response response = givenAuth().get(
    paths.getFooURL() + "?page=" + randomNumeric( 5 ) + "&size=10" );

   assertThat( response.getStatusCode(), is( 404 ) );
}
@Test
public void
 givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){
   restTemplate.createResource();

   Response response = givenAuth().get( paths.getFooURL() + "?page=1&size=10" );

   assertFalse( response.body().as( List.class ).isEmpty() );
}

Тест вождения Pagination Обнаружение

Тестирование обнаруживаемости нумерации страниц относительно просто, хотя есть много оснований для покрытия. Тесты сфокусированы на положении текущей страницы в навигации и различных URI, которые должны быть обнаружены в каждой позиции:

@Test
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = givenAuth().get( paths.getFooURL()+"?page=0&size=10" );

   String uriToNextPage = extractURIByRel( response.getHeader( LINK ), REL_NEXT );
   assertEquals( paths.getFooURL()+"?page=1&size=10", uriToNextPage );
}
@Test
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = givenAuth().get( paths.getFooURL()+"?page=0&size=10" );

   String uriToPrevPage = extractURIByRel( response.getHeader( LINK ), REL_PREV );
   assertNull( uriToPrevPage );
}
@Test
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = givenAuth().get( paths.getFooURL()+"?page=1&size=10" );

   String uriToPrevPage = extractURIByRel( response.getHeader( LINK ), REL_PREV );
   assertEquals( paths.getFooURL()+"?page=0&size=10", uriToPrevPage );
}
@Test
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = givenAuth().get( paths.getFooURL()+"?page=0&size=10" );
   String uriToLastPage = extractURIByRel( first.getHeader( LINK ), REL_LAST );

   Response response = givenAuth().get( uriToLastPage );

   String uriToNextPage = extractURIByRel( response.getHeader( LINK ), REL_NEXT );
   assertNull( uriToNextPage );
}

Это всего лишь несколько примеров интеграционных тестов, использующих сервис RESTful.

Получение всех ресурсов

По той же теме разбивки на страницы и возможности обнаружения должен быть сделан выбор, если клиенту разрешено извлекать все ресурсы в системе одновременно или если клиент ДОЛЖЕН запросить их разбиение на страницы.

Если сделан выбор, что клиент не может извлечь все Ресурсы одним запросом, и разбиение на страницы не является обязательным, но требуется, тогда для ответа на запрос get all доступно несколько вариантов .

Один из вариантов — вернуть 404 ( не найдено ) и использовать заголовок ссылки, чтобы сделать первую страницу доступной для обнаружения:

Ссылка = < http: // localhost: 8080 / rest / api / admin / foo? Page = 0 & size = 10 >; rel = « first », < http: // localhost: 8080 / rest / api / admin / foo? page = 103 & size = 10 >; rel = » last «

Другой вариант — вернуть перенаправление — 303 (см. Другое ) — на первую страницу нумерации страниц.

Третий вариант — вернуть 405 ( метод не разрешен) для запроса GET.

REST Paginag с заголовками Range HTTP

Относительно другой способ разбивки на страницы заключается в работе с заголовками HTTP- диапазонаRange , Content-Range , If-Range , Accept-Ranges — и HTTP-кодами состояния — 206 ( частичное содержимое ), 413 ( запрос слишком большой ), 416 ( Запрошенный диапазон не удовлетворяется ). Одно из представлений об этом подходе состоит в том, что расширения диапазона HTTP не были предназначены для разбивки на страницы и что ими должен управлять сервер, а не приложение.

Реализация разбиения на страницы на основе расширений заголовка диапазона HTTP, тем не менее, технически возможна, хотя и не так часто, как реализация, обсуждаемая в этой статье.

Вывод

В этой статье описывается реализация Pagination в RESTful-сервисе с Spring, обсуждается, как реализовать и протестировать Discoverability. Для полной реализации нумерации страниц проверьте проект github .

Из оригинальной REST Pagination весной из серии REST with Spring