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, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, 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
Спасибо большое, кратко, понятно и без воды!