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