Статьи
YouTube-канал
Отзывы

Spring Boot Restful Service

Исходники

4 января 2018

Тэги: Java, maven, rest, Spring, Spring Boot.

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

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

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

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

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

Давайте создадим maven-проект, в котором в качестве родительского проекта укажем spring-boot-starter-parent. Также нам потребуется добавить одну зависимость spring-boot-starter-web. Этого вполне достаточно для нашего проекта.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Для упрощения процедуры развёртывания добавим spring-boot-maven-plugin. При сборке он создаст нам один jar-файл со всеми необходимыми зависимостями внутри, а также определит точку запуска для нашего приложения.

<build>                                                      
    <plugins>                                                
        <plugin>                                             
            <groupId>org.springframework.boot</groupId>      
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>                                            
    </plugins>                                               
</build>

Теперь создадим новый класс RestfulApplication, который будет содержать единственный статический метод main. Он и будет отправной точкой при старте нашего приложения.

@SpringBootApplication(scanBasePackages = "ru.devmark")
public class RestfulApplication {

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

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

А если перенести класс RestfulApplication в пакет ru.devmark, то и scanBasePackages указывать не обязательно, т.к. все бины в пределах этого пакета (включая вложенные) будут подтягиваться автоматически. Удобно, однако производя глобальные рефакторинги в больших проектах об этом можно легко забыть и внезапно вы обнаружите неработающее приложение. Причём ваша среда разработки такую ошибку скорее всего не обнаружит. Поэтому я за явные параметры в аннотациях.

Теперь давайте создадим класс профиля пользователя. Именно в нём содержатся все поля, которые будет возвращать наш сервис (уникальный id пользователя, его имя и фамилия). Это простой бин, которому даже не требуется специальных аннотаций. Однако будьте внимательны: Spring многое делает автоматически, но он не увидит те поля, для которых не определены getter'ы.

public class Profile {

    private int id;
    private String firstName;
    private String lastName;

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return 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) {
            Profile profile = new Profile();
            profile.setId(personId);
            profile.setFirstName("Иван");
            profile.setLastName("Иванов");
            return profile;
        } else {
            throw new ProfileNotFoundException(personId);
        }
    }
}

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

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

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

@RestController
@RequestMapping(value = "/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public class ProfileController {

    private final ProfileService profileService;

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

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

Обратите внимание на аннотацию @RequestMapping. С её помощью мы указываем, что данный контроллер обрабатывает все http-запросы, выполняемые по адресу /profile/. Если вы запускаете сервис на локальной машине, то адресом сервера будет, разумеется, localhost. Также через параметр produces мы указываем, что контроллер возвращает ответ в формате json.

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

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

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

В принципе, можете собирать приложение мавеном (mvn clean package) и запускать его через java -jar.

java -jar target/spring-boot-restful-service-1.0-SNAPSHOT.jar

Если выполнить get-запрос по адресу http://localhost:8080/profile/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 - также обычный бин, который содержит текстовое описание ошибки. Здесь тоже нельзя забывать про добавление getter'ов. В идеале, сюда бы ещё добавить код ошибки для упрощения отладки и поиска багов.

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

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

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

Обратите внимание, что текст сообщения определяется в методе getMessage() перехваченного исключения. То есть различные исключения у нас обрабатываются единообразно.

Выводы

В итоге мы получили работающий restful-сервис, который принимает запрос и возвращает ответ в формате json. В следующей статье продолжим работать над ним и добавим слой взаимодействия с БД.


Облако тэгов

Kotlin, Java, Java 16, Java 11, Java 10, Java 9, Java 8, 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.

Последние статьи


Комментарии

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

×

devmark.ru