javascript
Spring的REST分页
REST with Spring系列:
- 第1部分– 使用Spring 3.1和基于Java的配置引導(dǎo)Web應(yīng)用程序
- 第2部分– 使用Spring 3.1和基于Java的配置構(gòu)建RESTful Web服務(wù)
- 第3部分– 使用Spring Security 3.1保護(hù)RESTful Web服務(wù)
- 第4部分– RESTful Web服務(wù)可發(fā)現(xiàn)性
- 第5部分– 使用Spring進(jìn)行REST服務(wù)發(fā)現(xiàn)
- 第6部分– 使用Spring Security 3.1的RESTful服務(wù)的基本身份驗(yàn)證和摘要身份驗(yàn)證
頁(yè)面作為資源vs頁(yè)面作為表示
在RESTful架構(gòu)的上下文中設(shè)計(jì)分頁(yè)時(shí)的第一個(gè)問(wèn)題是將頁(yè)面視為實(shí)際資源還是僅表示資源 。 將頁(yè)面本身視為資源會(huì)帶來(lái)許多問(wèn)題,例如不再能夠在調(diào)用之間唯一地標(biāo)識(shí)資源。 這加上以下事實(shí):在RESTful上下文之外,不能將頁(yè)面視為適當(dāng)?shù)膶?shí)體,但是在需要時(shí)構(gòu)造的所有者會(huì)使選擇變得簡(jiǎn)單: 頁(yè)面是表示的一部分 。
在REST上下文中的分頁(yè)設(shè)計(jì)中的下一個(gè)問(wèn)題是在何處包括分頁(yè)信息:
- 在URI路徑中 :/ foo / page / 1
- URI查詢 : / foo?page = 1
請(qǐng)記住, 頁(yè)面不是資源 ,因此不再可以將頁(yè)面信息編碼為URI。
URI查詢中的頁(yè)面信息
在URI查詢中對(duì)URI查詢中的頁(yè)面信息進(jìn)行編碼是解決此問(wèn)題的標(biāo)準(zhǔn)方法。 但是,這種方法確實(shí)有一個(gè)缺點(diǎn) –它切入了用于實(shí)際查詢的查詢空間:
/ foo?page = 1&size = 10
控制器
現(xiàn)在,對(duì)于實(shí)現(xiàn)– 用于分頁(yè)的Spring MVC控制器非常簡(jiǎn)單:
@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(); }這兩個(gè)查詢參數(shù)在請(qǐng)求映射中定義,并通過(guò)@RequestParam注入到控制器方法中; HTTP響應(yīng)和Spring UriComponentsBuilder注入到Controller方法中以包含在事件中,因?yàn)閷?shí)現(xiàn)可發(fā)現(xiàn)性將需要兩者。
REST分頁(yè)的可發(fā)現(xiàn)性
在分頁(yè)的范圍內(nèi),滿足REST的HATEOAS約束意味著使API的客戶端能夠基于導(dǎo)航中的當(dāng)前頁(yè)面發(fā)現(xiàn)下一頁(yè)和上一頁(yè)。 為此,將使用Link HTTP標(biāo)頭以及官方的 “ next ”,“ prev ”,“ first ”和“ last ”鏈接關(guān)系類型。
在REST中,可發(fā)現(xiàn)性是一個(gè)橫切關(guān)注點(diǎn) ,不僅適用于特定操作,還適用于操作類型。 例如,每次創(chuàng)建資源時(shí),客戶端應(yīng)可發(fā)現(xiàn)該資源的URI。 由于此要求與ANY資源的創(chuàng)建有關(guān),因此應(yīng)分開(kāi)處理并與主Controller流分離。
使用Spring,這種分離是通過(guò)事件來(lái)實(shí)現(xiàn)的 ,如上一篇文章中已充分討論的那樣,該文章側(cè)重于RESTful服務(wù)的可發(fā)現(xiàn)性。 對(duì)于分頁(yè),在控制器中觸發(fā)了事件– PaginatedResultsRetrievedEvent –,并且在此事件的偵聽(tīng)器中實(shí)現(xiàn)了可發(fā)現(xiàn)性:
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() ); }簡(jiǎn)而言之,偵聽(tīng)器邏輯檢查導(dǎo)航是否允許下一頁(yè),上一頁(yè),第一頁(yè)和最后一頁(yè),如果允許,則將相關(guān)的URI添加到鏈接HTTP標(biāo)頭中。 它還確保鏈接關(guān)系類型是正確的-“下一個(gè)”,“上一個(gè)”,“第一個(gè)”和“最后一個(gè)”。 這是偵聽(tīng)器的唯一職責(zé)( 此處是完整代碼 )。
測(cè)試駕駛分頁(yè)
分頁(yè)和可發(fā)現(xiàn)性的主要邏輯都應(yīng)由小型,集中的集成測(cè)試廣泛涵蓋; 與上一篇文章一樣 ,使用保證庫(kù)來(lái)使用REST服務(wù)并驗(yàn)證結(jié)果。
這些是分頁(yè)集成測(cè)試的一些示例; 要獲得完整的測(cè)試套件,請(qǐng)查看github項(xiàng)目(本文結(jié)尾的鏈接):
@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() ); }測(cè)試駕駛分頁(yè)可發(fā)現(xiàn)性
測(cè)試分頁(yè)的可發(fā)現(xiàn)性相對(duì)簡(jiǎn)單,盡管有很多基礎(chǔ)要講。 測(cè)試的重點(diǎn)是導(dǎo)航中當(dāng)前頁(yè)面的位置以及應(yīng)該從每個(gè)位置發(fā)現(xiàn)的不同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服務(wù)的集成測(cè)試的幾個(gè)示例。
獲取所有資源
關(guān)于分頁(yè)和可發(fā)現(xiàn)性的同一主題,必須選擇是否允許客戶端一次檢索系統(tǒng)中的所有資源 ,或者客戶端必須要求對(duì)它們進(jìn)行分頁(yè)。
如果選擇了客戶端無(wú)法通過(guò)單個(gè)請(qǐng)求檢索所有資源,并且分頁(yè)不是可選的,而是必需的,則可以使用幾個(gè)選項(xiàng)來(lái)響應(yīng)對(duì)“獲取所有”請(qǐng)求 。
一種選擇是返回404( 未找到 )并使用Link標(biāo)頭使第一頁(yè)可被發(fā)現(xiàn):
鏈接= <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 =“ 最后一個(gè) “另一個(gè)選擇是將重定向– 303( 請(qǐng)參閱其他 )返回到分頁(yè)的第一頁(yè)。
第三種選擇是為GET請(qǐng)求返回405( 不允許使用方法 ) 。
帶有范圍HTTP標(biāo)頭的REST Paginag
分頁(yè)的一種相對(duì)不同的方法是使用HTTP Range標(biāo)頭 – Range,Content-Range,If-Range,Accept-Ranges –和HTTP狀態(tài)碼 – 206( 部分內(nèi)容 ),413( 請(qǐng)求實(shí)體太大) ,416 ( 請(qǐng)求的范圍無(wú)法滿足 )。 關(guān)于這種方法的一種觀點(diǎn)是HTTP Range擴(kuò)展不是用于分頁(yè)的,它們應(yīng)該由服務(wù)器而不是由應(yīng)用程序管理。
盡管在技術(shù)上不像本文中討論的實(shí)現(xiàn)那樣普遍,但是基于HTTP Range標(biāo)頭擴(kuò)展實(shí)現(xiàn)分頁(yè)還是可行的。
結(jié)論
本文介紹了使用Spring在RESTful服務(wù)中分頁(yè)的實(shí)現(xiàn),并討論了如何實(shí)現(xiàn)和測(cè)試可發(fā)現(xiàn)性。 有關(guān)分頁(yè)的完整實(shí)現(xiàn),請(qǐng)查看github項(xiàng)目。
如果您讀完本文, 則應(yīng) 在Twitter上關(guān)注我 。
參考: Baeldung博客中我們JCG合作伙伴 Eugen Paraschiv的SpringREST分頁(yè)
翻譯自: https://www.javacodegeeks.com/2012/01/rest-pagination-in-spring.html
總結(jié)
以上是生活随笔為你收集整理的Spring的REST分页的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: linuxC文件读写(linux .c文
- 下一篇: FindBugs和JSR-305