6 июня 2016 г.

Spring Boot Restful Service

Актуальная версия статьи доступна на моём новом сайте devmark.ru

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

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

Требования

Java 8, Spring Boot 1.3.5, Maven 3

Исходники

https://github.com/nordmine/spring-boot-restful-service

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


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

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

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

<parent>                                                
    <groupId>org.springframework.boot</groupId>         
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>1.3.5.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>

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

@SpringBootApplication(scanBasePackages = "developer.remarks")
public class RestfulApplication {

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

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

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

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

public class Profile {

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

    public Profile(int id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

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

@Component
public class ProfileService {

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

Аннотация @Component гарантирует, что 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;
    }

    @RequestMapping(value = "/{personId:\\d+}", method = RequestMethod.GET)
    @ResponseBody
    public Profile getProfile(@PathVariable String personId) {
        return profileService.getProfile(Integer.valueOf(personId));
    }
}

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

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

В самом методе мы просто конвертируем полученный номер пользователя из строки в целое число (можем это делать благодаря регулярке в @RequestMapping) и дёргаем соответствующий метод у нашего сервиса, который инжектим в данный контроллер через конструктор при помощи аннотации @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/2, вы получите в ответ следующий json:

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

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


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

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

@ControllerAdvice
public class ErrorController {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ErrorInfo processException(Exception e) {
        return new ErrorInfo(e.getMessage());
    }
}

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

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

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

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

Добавляем логирование

Хорошо, клиенту мы ошибку вернули, но нам бы зафиксировать факт этой ошибки для дальнейших разбирательств. Пришла пора добавить логирование.

Spring Boot по умолчанию использует библиотеку логирования logback и ищет конфигурационный файл logback-spring.xml в проекте. Полное содержимое этого файла вы можете посмотреть в git-репозитории, а я здесь приложу только значимые части.

Сначала определяем appender. В нём указывается, как будет называться файл с логами, а также политика его ротации. В данном случае, если размер лога превысит 10 МБ, будет создан новый файл, а старый будет переименован и получит индекс.

    <appender name="error-controller" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_PATH}/exceptions.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${LOG_PATH}/exceptions.log.%i</fileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

Затем определяем сам logger, ссылаясь на этот аппендер:

<logger name="developer.remarks.controller.ErrorController" level="INFO">
    <appender-ref ref="error-controller" />                              
</logger>

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

Модифицируем наш ErrorController для ведения логов:

@ControllerAdvice
public class ErrorController {

    private 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());
    }
}

При запуске также рекомендую явно указывать путь к директории с логами при помощи параметра logging.path. Итого, полная строка запуска нашего restful-сервиса:

java -jar target/spring-boot-restful-service-1.0-SNAPSHOT.jar -Dlogging.path=путь_до_директории_с_логами


Вот и всё. Если после прочтения статьи возникли вопросы, пишите их в комментариях. Напоминаю, что более подробно работающую версию вы можете посмотреть здесь: https://github.com/nordmine/spring-boot-restful-service.

10 комментариев:

  1. Спасибо, полезная статья.Только вот я не пойму какой код можно вставить в метод getProfile класса ProfileService,чтобы имитировать обращение к БД, покажите пожалуйста.

    ОтветитьУдалить
    Ответы
    1. Любой код, который подгружает из базы некоторую запись по её идентификатору.

      Удалить
  2. Хотел еще попросить статью с примером, в которой объединены статья Spring Boot Restful Service и статья Hibernate, Spring и maven.

    ОтветитьУдалить
    Ответы
    1. Возможно, такая статья появится. Но пока не могу сказать по срокам.

      Удалить
  3. Прочитал еще про JQuery можно как то результат запроса к БД, который приходит в JSON-формате, используя JQuery выводить на страницу JSP??

    ОтветитьУдалить
    Ответы
    1. Разумеется. Из jquery делаете запрос к на целевой урл (который мапится через @RequestMapping). Этот урл вернёт вам json-объект, доступ к которому вы получите в jquery.

      Удалить
  4. Добрый день.Пытаюсь прикрутить работа с БД из вашей статьи Hibernate, Spring и maven сюда - не получается.

    ОтветитьУдалить
    Ответы
    1. Что именно вы пытаетесь прикрутить? Эти статьи в общем-то не связаны друг с другом.

      Удалить
  5. Пример работы с БД смотрите тут: http://developer-remarks.blogspot.ru/2017/02/spring-boot-dao.html

    ОтветитьУдалить
  6. Спасибо за то, что пишете для нас новичков) Так трудно найти актуальные примеры, некоторые ещё отцами написаны в далёких двухтысячных! А тут bestpractics современной java

    ОтветитьУдалить