Статьи
Утилиты 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 мы добавим слой взаимодействия с БД.



Комментарии

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