REST with Spring系列:
- 第1部分– 使用Spring 3.1和基于Java的配置引导Web应用程序
- 第2部分– 使用Spring 3.1和基于Java的配置构建RESTful Web服务
- 第3部分– 使用Spring Security 3.1保护RESTful Web服务
- 第4部分– RESTful Web服务可发现性
- 第5部分– 使用Spring进行REST服务发现
- 第6部分– 使用Spring Security 3.1的RESTful服务的基本身份验证和摘要身份验证
页面作为资源vs页面作为表示
在RESTful架构的上下文中设计分页时的第一个问题是将页面视为实际资源还是仅表示资源 。 将页面本身视为资源会带来许多问题,例如不再能够在调用之间唯一地标识资源。 这加上以下事实:在RESTful上下文之外,不能将页面视为适当的实体,但是在需要时构造的所有者会使选择变得简单: 页面是表示的一部分 。
在REST上下文中的分页设计中的下一个问题是在何处包括分页信息:
- 在URI路径中 :/ foo / page / 1
- URI查询 : / foo?page = 1
请记住, 页面不是资源 ,因此不再可以将页面信息编码为URI。
URI查询中的页面信息
在URI查询中对URI查询中的页面信息进行编码是解决此问题的标准方法。 但是,这种方法确实有一个缺点 –它切入了用于实际查询的查询空间:
/ foo?page = 1&size = 10
控制器
现在,对于实现– 用于分页的Spring MVC控制器非常简单:
@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分页的可发现性
在分页的范围内,满足REST的HATEOAS约束意味着使API的客户端能够基于导航中的当前页面发现下一页和上一页。 为此,将使用Link HTTP标头以及官方的 “ next ”,“ prev ”,“ first ”和“ last ”链接关系类型。
在REST中,可发现性是一个横切关注点 ,不仅适用于特定操作,还适用于操作类型。 例如,每次创建资源时,客户端应可发现该资源的URI。 由于此要求与ANY资源的创建有关,因此应分开处理并与主Controller流分离。
使用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() );
}
测试驾驶分页可发现性
测试分页的可发现性相对简单,尽管有很多基础要讲。 测试的重点是导航中当前页面的位置以及应该从每个位置发现的不同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服务的集成测试的几个示例。
获取所有资源
关于分页和可发现性的同一主题,必须选择是否允许客户端一次检索系统中的所有资源 ,或者客户端必须要求对它们进行分页。
如果选择了客户端无法通过单个请求检索所有资源,并且分页不是可选的,而是必需的,则可以使用几个选项来响应对“获取所有”请求 。
一种选择是返回404( 未找到 )并使用Link标头使第一页可被发现:
另一个选择是将重定向– 303( 请参阅其他 )返回到分页的第一页。
第三种选择是为GET请求返回405( 不允许使用方法 ) 。
带有范围HTTP标头的REST Paginag
分页的一种相对不同的方法是使用HTTP Range标头 – Range,Content-Range,If-Range,Accept-Ranges –和HTTP状态码 – 206( 部分内容 ),413( 请求实体太大) ,416 ( 请求的范围无法满足 )。 关于这种方法的一种观点是HTTP Range扩展不是用于分页的,它们应该由服务器而不是由应用程序管理。
尽管在技术上不像本文中讨论的实现那样普遍,但是基于HTTP Range标头扩展实现分页还是可行的。
结论
本文介绍了使用Spring在RESTful服务中分页的实现,并讨论了如何实现和测试可发现性。 有关分页的完整实现,请查看github项目。
如果您读完本文, 则应 在Twitter上关注我 。
参考: Baeldung博客中我们JCG合作伙伴 Eugen Paraschiv的SpringREST分页
翻译自: https://www.javacodegeeks.com/2012/01/rest-pagination-in-spring.html