4 января 2018
Тэги: rest Java maven Spring Boot Spring
Простой сервис на Spring Boot, который при выполнении get-запроса будет возвращать профиль пользователя в формате json в зависимости от id, который передаётся в запросе. При возникновении исключительных ситуаций (например, профиль не найден), пользователь получит соответствующий ответ.
Сразу оговорюсь, что здесь рассмотрю только создание самого веб-сервиса. Чаще всего, он будет обращаться к базе для получения профиля пользователя. Мы же этого здесь делать не будем, а только сымитируем загрузку профиля по id. Но всё, что касается взаимодействия по http, будет работать как положено.
Spring Boot позволяет просто и без лишних телодвижений создавать веб-сервисы. При этом конфигурацию служебных бинов он берёт на себя. Вы всегда можете переопределить дефолтное поведение, объявив тот или иной бин явно.
Давайте создадим maven-проект, в котором в качестве родительского проекта укажем spring-boot-starter-parent. Также нам потребуется добавить одну зависимость spring-boot-starter-web. Этого вполне достаточно для нашего проекта.
Для упрощения процедуры развёртывания добавим spring-boot-maven-plugin. При сборке он создаст нам один jar-файл со всеми необходимыми зависимостями внутри, а также определит точку запуска для нашего приложения.
Теперь создадим новый класс RestfulApplication, который будет содержать единственный статический метод main. Он и будет отправной точкой при старте нашего приложения.
Обратите внимание на аннотацию @SpringBootApplication. По сути это замена трёх стандартных для спринга аннотаций @Configuration (программная конфигурация бинов), @EnableAutoConfiguration (автоматически создавать необходимые бины), @ComponentScan (где искать бины). Также важно указать параметр scanBasePackages. Он содержит базовый пакет, в котором Spring Boot будет искать наши бины. Если этого не сделать или указать пакет неправильно - ничего работать не будет. Вместо пакета можно также явно указать конкретный класс.
А если перенести класс RestfulApplication в пакет ru.devmark, то и scanBasePackages указывать не обязательно, т.к. все бины в пределах этого пакета (включая вложенные) будут подтягиваться автоматически. Удобно, однако производя глобальные рефакторинги в больших проектах об этом можно легко забыть и внезапно вы обнаружите неработающее приложение. Причём ваша среда разработки такую ошибку скорее всего не обнаружит. Поэтому я за явные параметры в аннотациях.
Теперь давайте создадим класс профиля пользователя. Именно в нём содержатся все поля, которые будет возвращать наш сервис (уникальный id пользователя, его имя и фамилия). Это простой бин, которому даже не требуется специальных аннотаций. Однако будьте внимательны: Spring многое делает автоматически, но он не увидит те поля, для которых не определены getter'ы.
Теперь мы можем добавить сервис, содержащий уровень бизнес-логики. Для всех спринговых компонентов удобно определять интерфейсы, чтобы в любой момент можно было подменять реализацию. Интерфейс нашего сервиса будет выглядеть просто:
Как я уже говорил, внутри сервиса может быть обращение к БД, но мы ограничимся лишь имитацией: если id = 123, то возвращаем некий профиль, иначе говорим, что профиль с указанным номером не существует. Давайте создадим реализацию нашего интерфейса и назовём её ProfileServiceMock, чтобы подчеркнуть, что это лишь заглушка.
Аннотация @Service используется именно для сервисных компонентов, которые содержат всю бизнес-логику. При этом Spring создаст только один экземпляр данного класса. И это правильно, т.к. сервис не содержит внутренних состояний. Иными словами, любые запросы к сервису можно выполнять в любой последовательности.
ProfileNotFoundException наследуется от RuntimeException и не требует явного указания в сигнатуре метода, т.е. это исключение непроверяемое (unchecked). Оно отличается от стандартного тем, что содержит id профиля пользователя, который привёл к ошибке, а также переопределяет метод getMessage(), чтобы клиент, выполняющий запрос, получил более понятное описание ошибки.
Теперь мы готовы добавить в наш проект обработчик входящих rest-запросов. Снабдим его соответствующей аннотацией @RestController:
Обратите внимание на аннотацию @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:
Сервис вроде бы работает. Однако что получит пользователь, если профиль не найден? Желательно, чтобы он получал корректное описание ошибки также в формате json.
Spring Boot позволяет перехватывать все исключения, возникающие в каком-либо из контроллеров, при помощи аннотации @ControllerAdvice.
В @ExceptionHandler указывается одно или несколько перехватываемых исключений. Если требуется указать более одного, их нужно взять в фигурные скобки. ErrorInfo - также обычный бин, который содержит текстовое описание ошибки. Здесь тоже нельзя забывать про добавление getter'ов. В идеале, сюда бы ещё добавить код ошибки для упрощения отладки и поиска багов.
Интерфейс org.slf4j.Logger позволяет сделать запись в лог. В качестве параметра в метод logger.error() также передаём объект исключения, чтобы зафиксировать подробный stacktrace.
Теперь, если выполнить запрос http://localhost:8080/profile/1, вы получите ответ:
Обратите внимание, что текст сообщения определяется в методе getMessage() перехваченного исключения. То есть различные исключения у нас обрабатываются единообразно.
В итоге мы получили работающий restful-сервис, который принимает запрос и возвращает ответ в формате json. В следующей статье продолжим работать над ним и добавим слой взаимодействия с БД.
Kotlin, Java, Java 11, Java 10, Java 9, Java 8, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Hibernate, Collections, Stream API, многопоточность, Apache, maven, gradle, JUnit, ООП, алгоритмы, головоломки, rest