Это седьмая из серии статей о настройке защищенного веб-сервиса RESTful с использованием Spring 3.1 и Spring Security 3.1 с настройкой на основе Java. Эта статья будет посвящена реализации нумерации страниц в веб-сервисе RESTful.
Серия «Отдых с весной»:
- Часть 1 — Бутстрапирование веб — приложений с Spring 3.1 и Java на основе конфигурации
- P art 2 — Создание веб-службы RESTful с помощью Spring 3.1 и конфигурации на основе Java
- P art 3 — Защита веб-службы RESTful с помощью Spring Security 3.1
- Часть 4 — RESTful Web Service Discoverability
- Часть 5 — REST Service Discoverability с весной
- Часть 6 — Основные и дайджест аутентификации для службы RESTful с Spring Security 3.1
Вы можете проверить все 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