28 декабря 2023
Тэги: Java, json, maven, rest, Spring Boot, руководство.
Простой сервис на Spring Boot, который при выполнении get-запроса будет возвращать профиль пользователя в формате json в зависимости от id, который передаётся в запросе. При возникновении исключительных ситуаций (например, профиль не найден), пользователь получит соответствующий ответ.
Сразу оговорюсь, что здесь рассмотрю только создание самого веб-сервиса. Чаще всего, он будет обращаться к базе для получения профиля пользователя. Мы же этого здесь делать не будем, а только сымитируем загрузку профиля по id. Но всё, что касается взаимодействия по http, будет работать как положено.
Spring Boot позволяет просто и без лишних телодвижений создавать веб-сервисы. При этом конфигурацию служебных бинов он берёт на себя. Вы всегда можете переопределить дефолтное поведение, объявив тот или иной бин явно.
Давайте с помощью Spring Initializr создадим новый проект, в котором в качестве сборщика укажем maven, в качестве языка – Java 21 (поскольку это самая свежая версия с длительной поддержкой на текущий момент) и добавим одну зависимость Spring Web. Этого вполне достаточно для нашего проекта.
В нашем проекте по умолчанию уже будет основной класс с единственным статическим методом main(). Это и будет отправной точкой при старте нашего приложения.
Обратите внимание на аннотацию @SpringBootApplication. По сути это замена трёх стандартных для спринга аннотаций @Configuration (программная конфигурация бинов), @EnableAutoConfiguration (автоматически создавать необходимые бины), @ComponentScan (где искать бины: в текущем пакете и во всех вложенных в него).
Теперь давайте создадим класс профиля пользователя. Именно в нём содержатся все поля, которые будет возвращать наш сервис (уникальный id пользователя, его имя и фамилия). В Java 17 появился новый тип классов record. В этих классах достаточно определить поля с данными, а конструктор и все get-методы, а также equals(), hashCode() и toString() будут определены автоматически. Больше Lombok нам не нужен. Также классы типа record являются неизменяемыми, что позволяет их использовать например как ключи в мапе.
Теперь мы можем добавить сервис, содержащий уровень бизнес-логики. Для всех спринговых компонентов удобно определять интерфейсы, чтобы в любой момент можно было подменять реализацию. Интерфейс нашего сервиса будет выглядеть просто:
Как я уже говорил, внутри сервиса может быть обращение к БД, но мы ограничимся лишь имитацией: если id = 123, то возвращаем некий профиль, иначе говорим, что профиль с указанным номером не существует. Давайте создадим реализацию нашего интерфейса и назовём её ProfileServiceMock, чтобы подчеркнуть, что это лишь заглушка.
Аннотация @Service используется именно для сервисных компонентов, которые содержат всю бизнес-логику. При этом Spring создаст только один экземпляр данного класса. И это правильно, т.к. сервис не содержит внутренних состояний. Иными словами, любые запросы к сервису можно выполнять в любой последовательности.
Обратите внимание, что все поля класса Profile должны быть инициализированы сразу при создании объекта. Впоследствии эти поля доступны только для чтения.
ProfileNotFoundException наследуется от RuntimeException и не требует явного указания в сигнатуре метода, т.е. это исключение непроверяемое (unchecked). Оно отличается от стандартного тем, что содержит id профиля пользователя, который привёл к ошибке, а также переопределяет метод getMessage(), чтобы клиент, выполняющий запрос, получил более понятное описание ошибки.
Теперь мы готовы добавить в наш проект обработчик входящих rest-запросов. Снабдим его соответствующей аннотацией @RestController:
Обратите внимание на аннотацию @RequestMapping. С её помощью мы указываем, что данный контроллер обрабатывает все http-запросы, выполняемые по пути /profiles (по rest соглашениям имена сущностей в url указываются во множественном числе). Если вы запускаете сервис на локальной машине, то адресом сервера будет, разумеется, localhost. По умолчанию, ответ будет приходить в формате json.
Далее целевой метод при помощи @GetMapping мапится уже на get-запрос /profile/ид_пользователя. Причём номер пользователя должен содержать только цифры. Spring автоматически поместит значение из адреса в целочисленную переменную personId благодаря аннотации @PathVariable.
В самом методе мы просто вызываем соответствующий метод у нашего сервиса, который Spring автоматически подставит в данный контроллер через конструктор. Он будет искать все реализации указанного интерфейса. Можно было бы инжектить и напрямую в поле, но мировая общественность выступает за то, что удобнее это делать через конструктор. В частности, так лучше видны все зависимости данного компонента на этапе компиляции.
Важно отметить, что мы инкапсулируем всю бизнес-логику в ProfileService и изолируем её от непосредственного rest-взаимодействия. Таким образом, если завтра нам помимо json потребуется добавить ещё и xml-контроллер, мы легко это сделаем без копипасты, задействовав тот же экземпляр ProfileService.
В принципе, можно запустить приложение мавеном:
Если выполнить get-запрос по адресу http://localhost:8080/profiles/123, вы получите в ответ следующий json:
Сервис вроде бы работает. Однако что получит пользователь, если профиль не найден? Желательно, чтобы он получал корректное описание ошибки в формате json.
Spring Boot позволяет перехватывать все исключения, возникающие в каком-либо из контроллеров, при помощи аннотации @ControllerAdvice.
В @ExceptionHandler указывается одно или несколько перехватываемых исключений. Если требуется указать более одного, их нужно взять в фигурные скобки. ErrorInfo – также обычный record-класс, который содержит текстовое описание ошибки. В идеале, сюда бы ещё добавить код ошибки для упрощения отладки и поиска багов.
Интерфейс org.slf4j.Logger позволяет сделать запись в лог. В качестве параметра в метод logger.error() также передаём объект исключения, чтобы зафиксировать подробный stacktrace.
Теперь, если выполнить запрос http://localhost:8080/profiles/1, вы получите ответ:
Обратите внимание, что текст сообщения определяется в методе getMessage() перехваченного исключения. То есть различные исключения у нас обрабатываются единообразно.
В итоге мы получили работающий restful-сервис, который принимает запрос и возвращает ответ в формате json. В следующей статье Работа с БД в Spring Boot на примере postgresql мы добавим слой взаимодействия с БД.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.
20.09.2022 00:34 Евгения
"В Java 17 появился новый тип классов record. В этих классах достаточно определить поля с данными, а конструктор и все get- и set-методы а также equals(), hashCode() и toString() будут определены автоматически. Больше Lombok нам не нужен. Также классы типа record являются неизменяемыми, что позволяет их использовать например как ключи в мапе."
Поясните, пожалуйста, зачем классу set-методы, когда этот класс неизменяемый?
20.09.2022 13:22 devmark
Вы правы, set-методов в record не бывает, это опечатка. Все значения полей record устанавливаются только через конструктор.
28.12.2023 21:33 devmark
Перевёл пример проекта в github на актуальную версию Spring Boot 3 и Java 21.
02.09.2024 17:20 sg
Спасибо большое, кратко, понятно и без воды!