У меня в планах уже давно есть написание нескольких циклов материалов, и я даже начинал писать некоторые из них, но всякий раз по тем или иным причинам дело не доходило до публикации. И вот я наконец созрел явить общественности свой первый цикл материалов «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.