Статьи Утилиты Telegram YouTube Отзывы

Spring Boot Restful Service

Исходники

28 декабря 2023

Тэги: Java, json, maven, rest, Spring Boot, руководство.

Содержание

  1. Что мы получим в результате
  2. Реализуем обработку get-запроса
  3. Добавляем обработку ошибок
  4. Выводы

Что мы получим в результате

Простой сервис на Spring Boot, который при выполнении get-запроса будет возвращать профиль пользователя в формате json в зависимости от id, который передаётся в запросе. При возникновении исключительных ситуаций (например, профиль не найден), пользователь получит соответствующий ответ.

Реализуем обработку get-запроса

Сразу оговорюсь, что здесь рассмотрю только создание самого веб-сервиса. Чаще всего, он будет обращаться к базе для получения профиля пользователя. Мы же этого здесь делать не будем, а только сымитируем загрузку профиля по id. Но всё, что касается взаимодействия по http, будет работать как положено.

Spring Boot позволяет просто и без лишних телодвижений создавать веб-сервисы. При этом конфигурацию служебных бинов он берёт на себя. Вы всегда можете переопределить дефолтное поведение, объявив тот или иной бин явно.

Давайте с помощью Spring Initializr создадим новый проект, в котором в качестве сборщика укажем maven, в качестве языка – Java 21 (поскольку это самая свежая версия с длительной поддержкой на текущий момент) и добавим одну зависимость Spring Web. Этого вполне достаточно для нашего проекта.

В нашем проекте по умолчанию уже будет основной класс с единственным статическим методом main(). Это и будет отправной точкой при старте нашего приложения.

@SpringBootApplication
public class SpringBootRestfulServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootRestfulServiceApplication.class, args);
    }
}

Обратите внимание на аннотацию @SpringBootApplication. По сути это замена трёх стандартных для спринга аннотаций @Configuration (программная конфигурация бинов), @EnableAutoConfiguration (автоматически создавать необходимые бины), @ComponentScan (где искать бины: в текущем пакете и во всех вложенных в него).

Теперь давайте создадим класс профиля пользователя. Именно в нём содержатся все поля, которые будет возвращать наш сервис (уникальный id пользователя, его имя и фамилия). В Java 17 появился новый тип классов record. В этих классах достаточно определить поля с данными, а конструктор и все get-методы, а также equals(), hashCode() и toString() будут определены автоматически. Больше Lombok нам не нужен. Также классы типа record являются неизменяемыми, что позволяет их использовать например как ключи в мапе.

public record Profile(
        int id,
        String firstName,
        String lastName
) {
}

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

public interface ProfileService {

    Profile getProfile(int personId);
}

Как я уже говорил, внутри сервиса может быть обращение к БД, но мы ограничимся лишь имитацией: если id = 123, то возвращаем некий профиль, иначе говорим, что профиль с указанным номером не существует. Давайте создадим реализацию нашего интерфейса и назовём её ProfileServiceMock, чтобы подчеркнуть, что это лишь заглушка.

@Service
public class ProfileServiceMock implements ProfileService {

    @Override
    public Profile getProfile(int personId) {
        // имитируем обращение к БД
        if (personId == 123) {
            return new Profile(
                    personId,
                    "Иван",
                    "Иванов"
            );
        } else {
            throw new ProfileNotFoundException(personId);
        }
    }
}

Аннотация @Service используется именно для сервисных компонентов, которые содержат всю бизнес-логику. При этом Spring создаст только один экземпляр данного класса. И это правильно, т.к. сервис не содержит внутренних состояний. Иными словами, любые запросы к сервису можно выполнять в любой последовательности.

Обратите внимание, что все поля класса Profile должны быть инициализированы сразу при создании объекта. Впоследствии эти поля доступны только для чтения.

ProfileNotFoundException наследуется от RuntimeException и не требует явного указания в сигнатуре метода, т.е. это исключение непроверяемое (unchecked). Оно отличается от стандартного тем, что содержит id профиля пользователя, который привёл к ошибке, а также переопределяет метод getMessage(), чтобы клиент, выполняющий запрос, получил более понятное описание ошибки.

public class ProfileNotFoundException extends RuntimeException {

    private final int personId;

    public ProfileNotFoundException(int personId) {
        this.personId = personId;
    }

    @Override
    public String getMessage() {
        return "Profile with id = " + personId + " not found";
    }
}

Теперь мы готовы добавить в наш проект обработчик входящих rest-запросов. Снабдим его соответствующей аннотацией @RestController:

@RestController
@RequestMapping(value = "/profiles")
public class ProfileController {

    private final ProfileService profileService;

    public ProfileController(ProfileService profileService) {
        this.profileService = profileService;
    }

    @GetMapping(value = "/{personId:\\d+}")
    public Profile getProfile(@PathVariable int personId) {
        return profileService.getProfile(personId);
    }
}

Обратите внимание на аннотацию @RequestMapping. С её помощью мы указываем, что данный контроллер обрабатывает все http-запросы, выполняемые по пути /profiles (по rest соглашениям имена сущностей в url указываются во множественном числе). Если вы запускаете сервис на локальной машине, то адресом сервера будет, разумеется, localhost. По умолчанию, ответ будет приходить в формате json.

Далее целевой метод при помощи @GetMapping мапится уже на get-запрос /profile/ид_пользователя. Причём номер пользователя должен содержать только цифры. Spring автоматически поместит значение из адреса в целочисленную переменную personId благодаря аннотации @PathVariable.

В самом методе мы просто вызываем соответствующий метод у нашего сервиса, который Spring автоматически подставит в данный контроллер через конструктор. Он будет искать все реализации указанного интерфейса. Можно было бы инжектить и напрямую в поле, но мировая общественность выступает за то, что удобнее это делать через конструктор. В частности, так лучше видны все зависимости данного компонента на этапе компиляции.

Важно отметить, что мы инкапсулируем всю бизнес-логику в ProfileService и изолируем её от непосредственного rest-взаимодействия. Таким образом, если завтра нам помимо json потребуется добавить ещё и xml-контроллер, мы легко это сделаем без копипасты, задействовав тот же экземпляр ProfileService.

В принципе, можно запустить приложение мавеном:

# обычная сборка
./mvnw clean package

# сборка + запуск
./mvnw spring-boot:run

Если выполнить get-запрос по адресу http://localhost:8080/profiles/123, вы получите в ответ следующий json:

{"id":123,"firstName":"Иван","lastName":"Петров"}

Добавляем обработку ошибок

Сервис вроде бы работает. Однако что получит пользователь, если профиль не найден? Желательно, чтобы он получал корректное описание ошибки в формате json.

Spring Boot позволяет перехватывать все исключения, возникающие в каком-либо из контроллеров, при помощи аннотации @ControllerAdvice.

@ControllerAdvice
public class ErrorController {

    private static final Logger logger = LoggerFactory.getLogger(ErrorController.class);

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorInfo processException(Exception e) {
        logger.error("Unexpected error", e);
        return new ErrorInfo(e.getMessage());
    }
}

В @ExceptionHandler указывается одно или несколько перехватываемых исключений. Если требуется указать более одного, их нужно взять в фигурные скобки. ErrorInfo – также обычный record-класс, который содержит текстовое описание ошибки. В идеале, сюда бы ещё добавить код ошибки для упрощения отладки и поиска багов.

Интерфейс org.slf4j.Logger позволяет сделать запись в лог. В качестве параметра в метод logger.error() также передаём объект исключения, чтобы зафиксировать подробный stacktrace.

Теперь, если выполнить запрос http://localhost:8080/profiles/1, вы получите ответ:

{"message":"Profile with id = 1 not found"}

Обратите внимание, что текст сообщения определяется в методе 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.

Добавить комментарий

×

devmark.ru