Серия «Отдых с весной»:
- Часть 1. Начальная загрузка веб-приложения с помощью Spring 3.1 и конфигурации на основе Java
- Часть 2. Создание веб-службы RESTful с использованием Spring 3.1 и конфигурации на основе Java
- Часть 3. Защита веб-службы RESTful с помощью Spring Security 3.1
- Часть 4. Обнаружение веб-службы RESTful
- Часть 5. Обнаружение службы REST с помощью Spring
- Часть 6. Базовая и дайджест-проверка подлинности для службы RESTful с Spring Security 3.1
Страница как ресурс против страницы как представление
Первый вопрос при проектировании нумерации страниц в контексте архитектуры RESTful заключается в том, следует ли считать страницу реальным ресурсом или просто представлением ресурсов . Если рассматривать саму страницу как ресурс, возникает множество проблем, таких как невозможность уникальной идентификации ресурсов между вызовами. Это в сочетании с тем фактом, что вне контекста RESTful, страницу нельзя считать надлежащей сущностью, но держатель, который создается при необходимости, делает выбор простым: страница является частью представления .
Следующий вопрос в дизайне нумерации страниц в контексте REST — где включить информацию о поисковом вызове:
- в пути URI : / foo / page / 1
- запрос URI : / foo? page = 1
Принимая во внимание, что страница не является ресурсом , кодирование информации о странице в URI больше не вариант.
Информация о странице в запросе URI
Кодирование информации подкачки в запросе URI является стандартным способом решения этой проблемы в службе RESTful. Этот подход, однако, имеет один недостаток — он врезается в пространство запросов для реальных запросов:
/ Foo? страница = 1 & размер = 10
Контроллер
Теперь для реализации — Spring MVC Controller для разбивки на страницы прост:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
@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 — было запущено в контроллере, и обнаружение достигается в слушателе для этого события:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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 (ссылка в конце статьи):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@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, которые должны быть обнаружены в каждой позиции:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@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 ( не найдено ) и использовать заголовок ссылки, чтобы сделать первую страницу доступной для обнаружения:
Другой вариант — вернуть перенаправление — 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 весной от нашего партнера JCG Евгения Параскива в блоге baeldung