作为Web应用程序的用户,我们希望页面能够快速加载并仅显示与我们相关的信息。对于显示项目列表的页面,这意味着仅显示项目的一部分,而不是一次显示所有项目。
一旦第一页快速加载,UI就可以提供过滤,排序和分页等选项,帮助用户快速找到他或她正在寻找的项目。
在本教程中,我们将检查Spring Data的分页支持,并创建如何使用和配置它的示例,以及有关它如何工作的一些信息。
github上 的工作示例代码。
Paging vs. Pagination
这两个通常是同义词。然而,它们并不完全相同。在咨询了各种网络词典之后,我拼凑了以下定义,我将在本文中使用:
初始化示例项目
我们在本教程中使用Spring Boot来引导项目。您可以使用 Spring Initializr 并选择以下依赖项来创建类似的项目:
我还用JUnit 5替换了JUnit 4,因此生成的依赖项看起来像这样(Gradle表示法):
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' testImplementation('org.junit.jupiter:junit-jupiter:5.4.0') testImplementation('org.springframework.boot:spring-boot-starter-test'){ exclude group: 'junit', module: 'junit' } }
Spring Data的 Pageable
无论我们是想进行传统的分页,无限滚动还是简单的“前一个”和“下一个”链接,后端的实现都是一样的。
如果客户端只想显示项列表的“切片”,则需要提供一些描述该切片的输入参数。在Spring Data中,这些参数捆绑在Pageable接口中。它提供了以下方法,其中包括(评论是我的):
<b>public</b> <b>interface</b> Pageable { <font><i>// number of the current page </i></font><font> <b>int</b> getPageNumber(); </font><font><i>// size of the pages</i></font><font> <b>int</b> getPageSize(); </font><font><i>// sorting parameters</i></font><font> Sort getSort(); </font><font><i>// ... more methods</i></font><font> } </font>
每当我们只想加载一个完整的项目列表时,我们就可以使用一个Pageable实例作为输入参数,因为它提供了要加载的页面数量以及页面的大小。通过Sort该类,它还允许定义要排序的字段以及它们应该排序的方向(升序或降序)。
创建Pageable实例的最常用方法是使用PageRequest实现:
Pageable pageable = PageRequest.of(0, 5, Sort.by( Order.asc(<font>"name"</font><font>), Order.desc(</font><font>"id"</font><font>))); </font>
这将创建第一页的请求,其中5个项目首先按名称(升序)排序,第二个按ID(降序)排序。请注意,页面索引默认情况下从零开始!
困惑与java.awt.print.Pageable?
使用时Pageable,您会注意到IDE有时会建议导入java.awt.print.Pageable而不是Spring的Pageable 类。由于我们很可能不需要java.awt包中的任何类,我们可以告诉我们的IDE完全忽略它。
在IntelliJ中,转到设置中的“常规 - >编辑器 - >自动导入”,然后添加 java.awt.*到标记为“从导入和完成中排除”的列表中。
在Eclipse中,转到首选项中的“Java - >外观 - >类型过滤器”并添加java.awt.*到包列表中。
Spring Data Page和Slice分片
在Pageable捆绑分页请求的输入参数时,Page和Slice接口提供返回给客户端的项目页面的元数据:
<b>public</b> <b>interface</b> Page<T> <b>extends</b> Slice<T>{ <font><i>// total number of pages</i></font><font> <b>int</b> getTotalPages(); </font><font><i>// total number of items</i></font><font> <b>long</b> getTotalElements(); </font><font><i>// ... more methods</i></font><font> } </font>
<b>public</b> <b>interface</b> Slice<T> { <font><i>// current page number</i></font><font> <b>int</b> getNumber(); </font><font><i>// page size</i></font><font> <b>int</b> getSize(); </font><font><i>// number of items on the current page</i></font><font> <b>int</b> getNumberOfElements(); </font><font><i>// list of items on this page</i></font><font> List<T> getContent(); </font><font><i>// ... more methods</i></font><font> } </font>
通过Page接口提供的数据,客户端具有提供分页功能所需的所有信息。
如果我们不需要项目或页面的总数,我们可以使用Slice接口,例如,如果我们只想提供“上一页”和“下一页”按钮而不需要“第一页”和“最后一页“按钮。
该Page接口最常见的实现由PageImpl类提供:
Pageable pageable = ...; List<MovieCharacter> listOfCharacters = ...; <b>long</b> totalCharacters = 100; Page<MovieCharacter> page = <b>new</b> PageImpl<>(listOfCharacters, pageable, totalCharacters);
在Web控制器中分页
如果我们想要在Web控制器中返回Page(或Slice)项目,则需要接受Pageable定义分页参数的参数,将其传递给数据库,然后将Page对象返回给客户端。
激活Spring Data Web Support
基础持久层必须支持分页,以便为任何查询提供分页答案。这就是为什么在Pageable和Page类起源于Spring data模块。
Spring Boot应用程序中默认设置启用了自动配置,我们不必执行任何操作,因为SpringDataWebAutoConfiguration默认情况下它将加载,其中包括@EnableSpringDataWebSupport加载必要bean 的注释。
在没有Spring Boot 的普通Spring应用程序中,我们必须自己@EnableSpringDataWebSupport 在@Configuration类上使用:
@Configuration @EnableSpringDataWebSupport <b>class</b> PaginationConfiguration { }
如果我们在没有激活Spring Data Web支持的情况下在Web控制器方法中使用Pageable或Sort参数,我们将获得以下异常:
java.lang.NoSuchMethodException: org.springframework.data.domain.Pageable.<init>() java.lang.NoSuchMethodException: org.springframework.data.domain.Sort.<init>()
这些异常意味着Spring尝试创建一个Pageable或Sort实例并失败,因为它们没有默认构造函数。
这是由Spring data Web支持的,因为它添加 PageableHandlerMethodArgumentResolver 和 SortHandlerMethodArgumentResolver bean到应用环境中,这是负责寻找Web控制器方法的Pageable Sort类型的参数,并会使用查询参数中page,size和sort值填充导入进去 。
接受Pageable参数
启用Spring Data Web支持后,我们可以简单地使用a Pageable作为Web控制器方法的输入参数,并将Page对象返回给客户端:
@RestController @RequiredArgsConstructor <b>class</b> PagedController { <b>private</b> <b>final</b> MovieCharacterRepository characterRepository; @GetMapping(path = <font>"/characters/page"</font><font>) Page<MovieCharacter> loadCharactersPage(Pageable pageable) { <b>return</b> characterRepository.findAllPage(pageable); } } </font>
集成测试 表明,查询参数page,size以及sort现在正在被“注入”到我们的Web控制器方法的Pageable参数中:
@WebMvcTest(controllers = PagedController.<b>class</b>) <b>class</b> PagedControllerTest { @MockBean <b>private</b> MovieCharacterRepository characterRepository; @Autowired <b>private</b> MockMvc mockMvc; @Test <b>void</b> evaluatesPageableParameter() throws Exception { mockMvc.perform(get(<font>"/characters/page"</font><font>) .param(</font><font>"page"</font><font>, </font><font>"5"</font><font>) .param(</font><font>"size"</font><font>, </font><font>"10"</font><font>) .param(</font><font>"sort"</font><font>, </font><font>"id,desc"</font><font>) </font><font><i>// <-- no space after comma!</i></font><font> .param(</font><font>"sort"</font><font>, </font><font>"name,asc"</font><font>)) </font><font><i>// <-- no space after comma!</i></font><font> .andExpect(status().isOk()); ArgumentCaptor<Pageable> pageableCaptor = ArgumentCaptor.forClass(Pageable.<b>class</b>); verify(characterRepository).findAllPage(pageableCaptor.capture()); PageRequest pageable = (PageRequest) pageableCaptor.getValue(); assertThat(pageable).hasPageNumber(5); assertThat(pageable).hasPageSize(10); assertThat(pageable).hasSort(</font><font>"name"</font><font>, Sort.Direction.ASC); assertThat(pageable).hasSort(</font><font>"id"</font><font>, Sort.Direction.DESC); } } </font>
该测试捕获传递到存储库方法的Pageable参数,并验证它是否具有查询参数定义的属性。
请注意,我使用 自定义AssertJ断言 在Pageable实例上创建可读断言。
另请注意,为了按多个字段排序,我们必须sort多次提供查询参数。每个可以只包含一个字段名称,比如是升序,或者一个带有排序的字段名称,用逗号分隔,不带空格。如果字段名称和顺序之间有空格,则顺序不会执行排列。
接受Sort参数
同样,我们可以在Web控制器方法中使用独立Sort参数:
@RestController @RequiredArgsConstructor <b>class</b> PagedController { <b>private</b> <b>final</b> MovieCharacterRepository characterRepository; @GetMapping(path = <font>"/characters/sorted"</font><font>) List<MovieCharacter> loadCharactersSorted(Sort sort) { <b>return</b> characterRepository.findAllSorted(sort); } } </font>
当然,Sort对象只使用sort查询参数的值进行导入,因为此测试显示:
@WebMvcTest(controllers = PagedController.<b>class</b>) <b>class</b> PagedControllerTest { @MockBean <b>private</b> MovieCharacterRepository characterRepository; @Autowired <b>private</b> MockMvc mockMvc; @Test <b>void</b> evaluatesSortParameter() throws Exception { mockMvc.perform(get(<font>"/characters/sorted"</font><font>) .param(</font><font>"sort"</font><font>, </font><font>"id,desc"</font><font>) </font><font><i>// <-- no space after comma!!!</i></font><font> .param(</font><font>"sort"</font><font>, </font><font>"name,asc"</font><font>)) </font><font><i>// <-- no space after comma!!!</i></font><font> .andExpect(status().isOk()); ArgumentCaptor<Sort> sortCaptor = ArgumentCaptor.forClass(Sort.<b>class</b>); verify(characterRepository).findAllSorted(sortCaptor.capture()); Sort sort = sortCaptor.getValue(); assertThat(sort).hasSort(</font><font>"name"</font><font>, Sort.Direction.ASC); assertThat(sort).hasSort(</font><font>"id"</font><font>, Sort.Direction.DESC); } } </font>
自定义全局分页默认值
当调用一个带有Pageable参数的控制器方法时,如果我们不提供page,size或者sort查询参数,它会使用默认值来填充。
Spring Boot使用 @ConfigurationProperties功能 将以下属性绑定到类型SpringDataWebProperties的一个bean上 :
spring.data.web.pageable.size-parameter=size spring.data.web.pageable.page-parameter=page spring.data.web.pageable.<b>default</b>-page-size=20 spring.data.web.pageable.one-indexed-parameters=false spring.data.web.pageable.max-page-size=2000 spring.data.web.pageable.prefix= spring.data.web.pageable.qualifier-delimiter=_
上面的值是默认值。其中一些属性不是不言自明的,所以这是他们的工作:
qualifier-delimiter属性是一个非常特殊的情况。我们可以在Pageable方法参数是使用@Qualifier注释来为分页查询参数提供本地前缀:
@RestController <b>class</b> PagedController { @GetMapping(path = <font>"/characters/qualifier"</font><font>) Page<MovieCharacter> loadCharactersPageWithQualifier( @Qualifier(</font><font>"my"</font><font>) Pageable pageable) { ... } } </font>
这与prefix上面的属性有类似的效果,但它也适用于 sort参数。该qualifier-delimiter用于分隔从参数名称的前缀。在上面的例子中,仅查询参数my_page,my_size并my_sort 有效。
spring.data.web.* 属性无效?
如果对上述配置属性的更改无效,则SpringDataWebProperties bean可能未加载到应用程序上下文中。
其中一个原因可能是您已经习惯@EnableSpringDataWebSupport 了激活分页支持。这将覆盖SpringDataWebAutoConfiguration,在其中SpringDataWebProperties创建bean。 只在一个普通的 Spring应用程序中使用@EnableSpringDataWebSupport。
自定义本地分页默认值
有时我们可能只想为单个控制器方法定义默认的分页参数。对于这种情况,我们可以使用@PagableDefault和@SortDefault注释:
@RestController <b>class</b> PagedController { @GetMapping(path = <font>"/characters/page"</font><font>) Page<MovieCharacter> loadCharactersPage( @PageableDefault(page = 0, size = 20) @SortDefault.SortDefaults({ @SortDefault(sort = </font><font>"name"</font><font>, direction = Sort.Direction.DESC), @SortDefault(sort = </font><font>"id"</font><font>, direction = Sort.Direction.ASC) }) Pageable pageable) { ... } } </font>
如果未给出查询参数,Pageable则现在将使用注释中定义的默认值填充对象。
请注意,@PageableDefault注释也有一个sort属性,但如果我们要定义多个字段以按不同方向排序,我们必须使用@SortDefault。
在Spring Data Repository中进行分页
由于本文中描述的分页功能来自Spring Data,因此Spring Data完全支持分页并不奇怪。但是,如何支持很快得到解释,我们只需要向存储库接口添加正确的参数和返回值。
传递分页参数
我们可以简单地将一个Pageable或Sort实例传递给任何Spring Data存储库方法:
<b>interface</b> MovieCharacterRepository <b>extends</b> CrudRepository<MovieCharacter, Long> { List<MovieCharacter> findByMovie(String movieName, Pageable pageable); @Query(<font>"select c from MovieCharacter c where c.movie = :movie"</font><font>) List<MovieCharacter> findByMovieCustom( @Param(</font><font>"movie"</font><font>) String movieName, Pageable pageable); @Query(</font><font>"select c from MovieCharacter c where c.movie = :movie"</font><font>) List<MovieCharacter> findByMovieSorted( @Param(</font><font>"movie"</font><font>) String movieName, Sort sort); } </font>
即使Spring Data提供了一个PagingAndSortingRepository,我们也不必使用它来获得分页支持 。它只提供了两种方便的findAll方法,一种是a Sort,一种是Pageable 参数。
返回页面元数据
如果我们想要将页面信息返回给客户端而不是简单列表,我们只需让我们的存储库方法只返回一个Slice或一个Page:
<b>interface</b> MovieCharacterRepository <b>extends</b> CrudRepository<MovieCharacter, Long> { Page<MovieCharacter> findByMovie(String movieName, Pageable pageable); @Query(<font>"select c from MovieCharacter c where c.movie = :movie"</font><font>) Slice<MovieCharacter> findByMovieCustom( @Param(</font><font>"movie"</font><font>) String movieName, Pageable pageable); } </font>
每个返回一个Slice或Page必须只有一个Pageable参数的方法,否则Spring Data会在启动时抱怨异常。
结论
Spring Data Web支持使普通的Spring应用程序以及Spring Boot应用程序中的分页变得容易。这是激活它然后在控制器和存储库方法中使用正确的输入和输出参数的问题。
使用Spring Boot的配置属性,我们可以对默认值和参数名称进行细粒度控制。
可以 在github上 找到本文中使用的示例代码。