Spring по верхам: разработка REST-сервиса

У меня в планах уже давно есть написание нескольких циклов материалов, и я даже начинал писать некоторые из них, но всякий раз по тем или иным причинам дело не доходило до публикации. И вот я наконец созрел явить общественности свой первый цикл материалов «Spring по верхам», в котором буду поверхностно рассказывать о том, как можно использовать экосистему Spring при разработке проектов.

Подготовка проекта

В качестве демонстрационного проекта будет выступать простой REST-сервис для организации списка задач. Для разработки мне потребуется Apache Maven, JDK 17 и недавно обновившийся Spring Boot 3.

Поскольку речь идёт о сервисе планирования задач, то мне потребуется класс, описывающий задачу:

Также мне потребуется компонент для хранения задач — репозиторий (хотя корректнее назвать этот компонент DAO, т.к. речь идёт о проекте, в котором не применяются практики предметно-ориентированного проектирования).

Но поскольку в рамках этой статьи я не буду касаться вопросов использования аз данных, хранить все существующие задачи я буду в памяти:

Я заранее создал 2 задачи, чтобы их можно было использовать в примерах, и зарегистрировал экземпляр InMemTaskRepository при помощи стереотипной аннотации @Repository в контексте приложения, чтобы Spring Framework мог внедрять его в зависимые компоненты.

Разработка REST-сервиса

Теперь можно перейти непосредственно к разработке REST-сервиса. Для этого нужно создать класс-контроллер:

Чтобы Spring Framework мог использовать экземпляр этого класса для обработки HTTP-запросов, его необходимо зарегистрировать в контексте приложения. Для этого я добавил аннотацию @RestController на уровне класса, хотя есть и другие способы регистрации компонентов в контексте приложения.

Я хочу, чтобы этот класс обрабатывал все запросы, путь которых начинается с api/tasks, этого я могу добиться при помощи аннотации @RequestMapping("api/tasks") на уровне класса.

В качестве свойств этого класса я добавил созданный ранее репозиторий и внедрил зависимость через конструктор.

Получение списка ресурсов

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

Поскольку метод должен обрабатывать запросы с методом GET и путём /api/tasks, я добавил на уровне метода аннотацию @GetMapping, эта аннотация — альтернатива @RequestMapping(method = RequestMethod.GET), но исключительно для GET-запросов. Путь в аргументах аннотации я не указывал, так как он наследуется от аннотации @RequestMapping("api/tasks") на уровне класса.

В качестве возвращаемого объекта я использовал экземпляр ResponseEntity, так как этого класс позволяет детально описать HTTP-ответ, например, указать код состояния HTTP, значение заголовка Content-type и задать тело ответа. На основании значения заголовка Content-type Spring Framework постарается конвертировать тело ответа в нужный формат. JSON, используемый в этом примере, является стандартным форматом представления данных в REST-сервисах, и Spring Framework поддерживает преобразование между Java-объектами и JSON при помощи библиотеки Jackson (хотя при желании могут быть использованы и другие библиотеки).

Для реализации этого метода потребуется соответствующий метод в интерфейсе репозитория:

и его крайне простая реализация:

Напомню, что в этой статье я демонстрирую использование возможностей Spring Framework и бездумно повторять в реальных условиях всё, что вы здесь видите, не следует. Если вы действительно будете хранить какие-то данные сервиса в памяти таким же образом, то побеспокойтесь об обеспечении конкурентного доступа к данным и старайтесь возвращать неизменяемые объекты и копии коллекций.

Теперь если я запущу сервис и выполню запрос к нему, то получу примерно такой ответ:

Как видите сервис вернул ответ с кодом состояния 200 OK, заголовком Content-type имеющим значение application/json и двумя задачами в теле ответа.

Создание ресурса

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

Для начала я создам класс, описывающий тело запроса на создание новой задачи. Он будет достаточно прост и содержать всего одно свойство — текстовое описание задачи.

Теперь можно перейти непосредственно к созданию метода REST-контроллера, создающего задачу:

Поскольку создание новых ресурсов реализуется при помощи HTTP-методов POST или PUT, я выбрал POST и использовал аннотацию @PostMapping на уровне метода. Как и @GetMapping эта аннотация является упрощенным вариантом аннотации @RequestMapping, но для POST-запросов. HTTP-путь, обрабатываемый методом, также наследуется от аннотации @RequestMapping на уровне класса.

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

На запрос на создание нового ресурса можно также вернуть ответ с кодом состояния HTTP 200 OK, но в контексте REST-сервисов правильнее возвращать ответ с кодом состояния HTTP 201 Created и заголовком Location, содержащим ссылку на созданный ресурс. Чтобы правильно создать ссылку на новый ресурс, можно использовать объект класса UriComponentsBuilder, который также можно получить через аргументы метода. Этот объект будет создан на основании полученного HTTP-запроса.

И теперь, если я отправлю запрос на создание новой задачи, то получу в ответ что-то вроде:

И если я попытаюсь создать задачу с пустым описанием или и вовсе со значением null, то у меня это получится. Однако в информационных системах любое создание или изменение информации проходит стадию проверки корректности изменений — валидацию. И в данном случае я не хочу, чтобы в системе создавались задачи без описаний. Если пользователь присылает запрос с некорректным описанием задачи, то я должен вернуть ему ответ с кодом состояния HTTP 400 Bad Request и описанием допущенных ошибок.

Для решения этой задачи я добавлю простую валидацию, но чтобы пользователь получал в ответ понятное ему описание ошибки, мне потребуется API интернационализации. Для этого Spring Framework предоставляет интерфейс MessageSource, экземпляр которого создаётся и регистрируется автоматически в контексте приложения при помощи автоконфигураций Spring Boot. Поэтому мне остаётся только внедрить в мой класс-контроллер эту зависимость.

А для перевода кода ошибки на нужный язык мне потребуется локаль запроса. Её можно получить через аргументы метода, Spring Framework автоматически определит нужную локаль на основании значения HTTP-заголовка Accept-language, если он указан. В противном случае по умолчанию будет использована системная локаль сервиса.

Обновлённая версия метода будет выглядеть следующим образом:

Также мне потребуется класс, описывающий отчёт об ошибках:

Ну и пара файлов локализации для поддержки русского и английского языка:

Файлы локализации по умолчанию должны находиться в директории ресурсов проекта.

Теперь, если пользователь попытается создать задачу с пустым описанием, то он получит такой ответ:

Получение ресурса по идентификатору

Но попытка получить задачу по ссылке будет заканчиваться неудачей, так как метода, обрабатывающего такие запросы, класс-контроллер не имеет. Требуемый метод имеет следующий вид:

На этот раз в аннотацию GetMapping передан путь, характерный для этого метода. Всё, что находится внутри фигурных скобок в пути называется переменной пути и её значение можно получить через аргумент метода, отметив его аннотацией @PathVariable. Если не указать название переменной пути в свойстве @PathVariable, то Spring Framework попытается сопоставить переменную пути по названию аргумента, однако всё же лучше указывать название переменной пути в аннотации.

В данном случае экземпляр ResponseEntity создаётся при помощи метода of, который принимает экземпляр Optional. И если Optional не пустой, то код состояния HTTP будет 200 OK, в противном случае — 404 Not Found.

Ссылки