Существует множество решений для реализации разбивки на страницы в REST API. Эта история не о лучших практиках или решении, которое лично мне нравится больше всего. Напротив, речь идет о том, как максимально чисто реализовать одну из часто используемых альтернатив, используя Spring Boot и JPA.

Альтернативный вариант разбивки на страницы, описанный в этой истории, использует пару параметров URL для разбивки на страницы и возвращает коллекцию элементов ресурсов и заголовок с общим количеством элементов в коллекции. Для разбитой на страницы коллекции из Resource элементов вызов будет выглядеть так: GET /api/resources?_start=10&_end=15. Допустим, у нашего ресурса есть id и name. Ожидаемый результат для GET выше будет примерно таким:

X-Total-Count: 2000
[{"id":100, "name": "Element 100"},{"id":101, "name": "Element 101"},{"id":102, "name": "Element 102"},{"id":103, "name": "Element 103"},{"id":104, "name": "Element 104"}]

Имена параметров не имеют значения, или имя заголовка может отличаться в зависимости от реализации. Есть несколько другой вариант с использованием page и elements в качестве параметров, но концепция та же: мы запрашиваем страницу коллекции ресурсов, и нам нужен список элементов, содержащихся на странице, и заголовок с общим размером нашей коллекции.

Реализация такой операции с использованием Spring и JPA

Для репозитория мы можем использовать JpaRepository и написать что-то вроде этого:

Здесь мы предполагаем, что наше приложение использует POJO, зависящие от уровня, поэтому наша служба вызовет соответствующую операцию репозитория и преобразует результат перед возвратом:

Очевидно, что этот convert метод не реализован в приведенном выше фрагменте. Мы могли бы использовать MapStruct для преобразования между POJO, но детали реализации модели не имеют отношения к решению. Единственное, что нам нужно знать, чтобы понять остальную часть истории, - это то, что наши службы получат объект Pageable в качестве параметра и в результате вернут Page.

Разрешить объект Pageable из параметров запроса

Чтобы уменьшить объем кода в наших контроллерах, мы можем использовать преобразователь аргументов для создания объекта Pageable с использованием параметров из запроса. Некоторое время назад я писал статью о том, как вводить кастомные параметры. Вы можете проверить это для уточнения, но наш преобразователь будет выглядеть так:

Нам нужно добавить преобразователь в преобразователи приложений:

Имея эту конфигурацию, мы можем написать наш метод контроллера следующим образом:

С помощью преобразователя аргументов мы сделали объект со страницами автоматически доступным для наших методов контроллера. Это очень хорошо, потому что теперь у нас нет кода для обработки создания объекта Pageable, лежащего в наших контроллерах.

Вернуть ожидаемый формат

Проблема с приведенным выше кодом заключается в том, что он не генерирует ответ, описанный в начале этой истории. Список элементов выглядит нормально, но мы не генерируем заголовок X-Total-Count.

Тривиальным решением нашей проблемы было бы следующее:

Проблема с этим решением заключается в том, что мы загрязняем наш контроллер генерацией заголовка ... и, если у нас есть несколько методов, возвращающих разбитые на страницы коллекции, мы будем повторять это снова и снова.

К счастью для нас, Spring MVC предоставляет инструмент, который позволяет нам настраивать ответ наших остальных контроллеров до того, как ответ будет записан обратно клиенту с использованием HttpMessageConverter. Этот инструмент называется ResponseBodyAdvice. Интерфейс выглядит так:

В чем-то он похож на HandlerMethodArgumentResolver. У нас есть supports метод, в котором мы решаем, поддерживаем ли мы данный тип параметра, а затем beforeBodyWrite, где мы можем настроить наш ответ. Вернемся к нашему контроллеру и просто вернем Page:

Теперь нам нужно реализовать наш ResponseBodyAdvice:

К сожалению, ResponseBodyAdvice не предназначен для параметризации с помощью типа ввода и типа вывода. Когда мы объявляем ResponseBodyAdvice<T>, T определяет тип параметра. Мы получим параметр как первый параметр нашего beforeBodyWrite метода и вернем новое значение после настройки.

Нам нужно реализоватьResponseBodyAdvice<Object> и выполнить несколько уродливых приведений, чтобы работа была выполнена… но это работает.

Подведение итогов

Эта история касается очень конкретной проблемы, но решение реализовано с использованием очень общего механизма. Жалко, что Spring не предоставляет ResponseBodyAdvice<T,K>, чтобы сделать реализацию более чистой, но, приняв пару примитивов старой школы в нашей кодовой базе, мы можем иметь действительно чистые контроллеры для такого рода API.