Pagination is a mechanism for managing big result sets in any kind of application. This quick tutorial focuses on implementing pagination in a RESTful API, using Spring MVC emoji-leaves and Spring Data without the help of the Spring HATEOAS project.
Last active
December 13, 2023 13:50
-
-
Save matchilling/07ba65800a3b0770b7a52d0d868d0f0b to your computer and use it in GitHub Desktop.
REST Pagination in Spring with Link header
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.matchilling.api.rest.data | |
import com.fasterxml.jackson.core.JsonGenerator | |
import com.fasterxml.jackson.databind.JsonSerializer | |
import com.fasterxml.jackson.databind.SerializerProvider | |
import org.springframework.boot.jackson.JsonComponent | |
import org.springframework.data.domain.PageImpl | |
import java.io.IOException | |
@JsonComponent | |
class PageSerializer : JsonSerializer<PageImpl<*>>() { | |
@Throws(IOException::class) | |
override fun serialize( | |
page: PageImpl<*>, | |
jsonGenerator: JsonGenerator, | |
serializerProvider: SerializerProvider | |
) { | |
jsonGenerator.writeObject(page.content) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.matchilling.api.rest.servlet | |
import org.springframework.beans.factory.annotation.Value | |
import org.springframework.core.MethodParameter | |
import org.springframework.data.domain.PageImpl | |
import org.springframework.data.domain.Pageable | |
import org.springframework.http.MediaType | |
import org.springframework.http.converter.HttpMessageConverter | |
import org.springframework.http.server.ServerHttpRequest | |
import org.springframework.http.server.ServerHttpResponse | |
import org.springframework.web.bind.annotation.RestControllerAdvice | |
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice | |
import org.springframework.web.util.UriComponentsBuilder | |
@RestControllerAdvice | |
class PaginatedResponseAdvice<T>( | |
@Value("\${spring.data.web.pageable.one-indexed-parameters}") | |
private val oneIndexed: Boolean | |
) : ResponseBodyAdvice<T> { | |
override fun supports( | |
returnType: MethodParameter, | |
converterType: Class<out HttpMessageConverter<*>> | |
): Boolean { | |
return PageImpl::class.java.isAssignableFrom(returnType.parameterType) | |
} | |
override fun beforeBodyWrite( | |
page: T?, | |
returnType: MethodParameter, | |
selectedContentType: MediaType, | |
selectedConverterType: Class<out HttpMessageConverter<*>>, | |
request: ServerHttpRequest, | |
response: ServerHttpResponse | |
): T? { | |
if (page !is PageImpl<*>) { | |
return page | |
} | |
val headers = response.headers | |
headers.set( | |
"Access-Control-Expose-Headers", | |
"Link,Page-Number,Page-Size,Total-Elements,Total-Pages" | |
) | |
val links = page.links(request) | |
if (links.isNotBlank()) { | |
headers.set("Link", links) | |
} | |
val pageNumber = if (oneIndexed) | |
page.number.plus(1) | |
else | |
page.number | |
headers.set("Page-Number", pageNumber.toString()) | |
headers.set("Page-Size", page.size.toString()) | |
headers.set("Total-Elements", page.totalElements.toString()) | |
headers.set("Total-Pages", page.totalPages.toString()) | |
return page | |
} | |
private fun PageImpl<*>.links(request: ServerHttpRequest): String { | |
val links = mutableListOf<String>() | |
val builder = UriComponentsBuilder.fromUri(request.uri) | |
if (request.uri.host == "localhost") { | |
builder.port(request.uri.port) | |
} | |
if (!this.isFirst) { | |
val link = builder.replacePageAndSize(this.pageable.first()) | |
links.add("<${link.toUriString()}>; rel=\"first\"") | |
} | |
if (this.hasPrevious()) { | |
val link = builder.replacePageAndSize(this.previousPageable()) | |
links.add("<${link.toUriString()}>; rel=\"prev\"") | |
} | |
if (this.hasNext()) { | |
val link = builder.replacePageAndSize(this.nextPageable()) | |
links.add("<${link.toUriString()}>; rel=\"next\"") | |
} | |
if (!this.isLast) { | |
val last = builder.cloneBuilder() | |
last.replaceQueryParam("page", this.totalPages) | |
last.replaceQueryParam("size", this.size) | |
links.add("<${last.toUriString()}>; rel=\"last\"") | |
} | |
return links.joinToString(",") | |
} | |
private fun UriComponentsBuilder.replacePageAndSize( | |
page: Pageable | |
): UriComponentsBuilder { | |
val builder = this.cloneBuilder() | |
val pageNumber = if (oneIndexed) | |
page.pageNumber.plus(1) | |
else | |
page.pageNumber | |
builder.replaceQueryParam("page", pageNumber) | |
builder.replaceQueryParam("size", page.pageSize) | |
return builder | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment