Spring: REST

Spring: REST


Начать стоит с предтечья REST - с web-сервисов. Web-сервис - это способ коммуникации, реализованный на основе стандартов, где в качестве инструмента коммуникации выбраны сообщения (а не бинарные или текстовые объекты). Как правило, используются SOAP и WSDL.

REST - это аналог web-сервиса, созданный специально для коммуникации между приложениями (или слоями приложения). Зародился в 2000 году от Роя Филдинга (Roy Fielding) в качестве легковесной альтернативы RPC, Remote Procedure Call и web-службам. Использует паттерн CRUD (Create/Read/Update/Delete) для оперирования между узлами.

HTTP метод Действие Описание
GET Read Выполняет чтение существующего ресурса. Считается безопасной (safe) операцией, поскольку не меняет данные. Ответ от запроса к запросу не должен меняться (конечно, если не было изменений и если бизнес-логика удовлетворяет данное требование).
POST Create Используется для создания нового ресурса. Для одинаковых POST запроса должны возвращать одинаковые результаты (конечно, если бизнес-логика удовлетворяет данное требование).
PUT Update Изменяет существующий ресурс. Является не безопасной (unsafe) операцией. Так же как и POST должна выдавать одинаковые ответы при одинаковых запросах (конечно, если бизнес-логика удовлетворяет данное требование).
DELETE Delete Удаляет существующий ресурс.

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

RESTful протокол считается stateless, т.е. соединение клиент-сервер не поддерживается активным, а разрывается после окончания передачи данных. Так же, нет требований к синхронизации - действия могут выполняться асинхронно.

Аннотации Spring 4.3

В версии 4.3 появилась укороченная версия аннотаций для контроллеров:

HTTP метод До 4.3 После 4.3
GET @RequestMapping(metod="GET") @GetMapping
POST @RequestMapping(metod="POST") @PostMapping
PUT @RequestMapping(metod="PUT") @PutMapping
DELETE @RequestMapping(metod="DELETE") @DeleteMapping

@Controller в Spring REST

По ум. в Spring MVC @Controller пытается найти представление (View) если метод возвращает строку. Например:

1
2
3
4
@GetMapping
public String listObjects() {
return new ObjectMapper().writeValueAsString(repo.findAll());
}

Spring преобразует объекты, найденные в репозитории в JSON и попытается найти в представлениях View с подобным именем. Есть вероятнгость, что данное представление найдено не будет и Spring выдаст клиенту соответствующую ошибку. Существует несколько способов исправить данную проблему, в частности:

  • добавить аннотацию @ResponseBody в определение метода;
  • добавить аннотацию @ResponseBody в отпределение класса;
  • изменить аннотацию класса с @Controller на @RestController (она представляет собой мета-аннотацию @Controller + @ResponseBody).

После данной правки метод станет возвращать JSON в качестве ответа на запрос. Но это не самая успешная реализация данной задачи. Ее можно упростить, переложив преобразование результата на сторону Spring:

1
2
3
4
@GetMapping
public List<MyObject> listObjects() {
return repo.findAll();
}

Для того чтобы Spring корретно преобразовал объект (в примере вышел это List) в JSON, потребуется определить для него конвертер. В Spring уже предусмотрено некоторое количество “стандартных” конвертеров:

Требуемый тип Конвертер Условие
StringHttpMessageConverter text/plain
MappingJackson2HttpMessageConverter application/+json Наличие подключенной библиотеки Jackson2
MappingJackson2XmlMessageConverter application/+xml Наличие подключенной библиотеки Jackson2
AtomFeedHttpMessageConverter application/atom+xml Наличие подключенной библиотеки Rome
RssChannelHttpMessageConverter application/rss+xml Наличие подключенной библиотеки Rome

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnabledWebMvc
class MyWebConfig implements WebMvcConfigurer {
...
@Bean
public MappingJackson2HttpMessageConverter jsonConverter(ObjectMapper objectMapper) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
return converter;
}

@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
...
return objectMapper;
}
}

Стандартная конвертация дат

По ум. ObjectMapper (от FasterXML) преобразует даты как набор значений объекта. Это не всегда удобно для клиента. Можно либо самостоятельно реализовать преобразование типа данных в нужное значение JSON используя @JsonFormat на каждом требуемом поле объекта, либо воспользоваться готовой реализацией из библиотеки jackson-datatype-jsr310. Далее необходимо потребовать от Jackson2 выполнить сканирование на наличие модулей (конверетер является частью модуля).

1
objectMapper.findAndRegisterModules();

Либо подключить только нужный модуль:

1
objectMapper.registerModule(new JavaTimeModule());

После обнаружения, Jackson2 подключит конвертер ко всем типам с дат.

@JsonIgnore

Данная аннотация используется для игнорирования того или иного поля (или getter) при конвертации.

Различные представления для конвертации

JSON лишь один из способов отображения данных. Web-метод может определить как следует предоставить данные (а так же, получить их). Так, для определения предоставления данных используется:

1
@GetMapping(value="...", produces="application/json;charset=UTF-8", consumes="application/xml")

Клиент со своей стороны так же может регулировать какий формат ответа он предпочитает. Он может передать это в заголовке Accept.

Spring Boot REST

Для включения REST в StringBoot приложение следует добавить зависимость spring-boot-starter-web, который включает в себя в т.ч. следующие:

  • spring-boot-starter-json. Загружает Jackson2 зависимости и создает бин ObjectMapper;
  • spring-boot-starter-validation. Добавляет Hibernate валидации (типа @Min, @NotEmpty, @NotNull и т.д.);
  • spring-boot-starter-tomcat. Выбирается как сервер по ум. Если необходимо заменить его, то следует исключить данную библиотеку из зависимостей. Замена возможна, например, на spring-boot-starter-jetty.

HTTP Status code

Детальное описание кодов и их рекомендованное поведение представлено на отдельном ресурсе.

Некоторые из них:

  • 200 / OK - GET запрос успешно выполнен;
  • 201 / CREATED - POST или PUT (если ресурса ранее не было) успешно выполнены. Так же необходимо дополнительно вернуть заголовок, в котором будет ссылка на новый ресурс;
  • 204 / NO_CONTENT - успешноре выполнение PUT (изменения) или DELETE. В теле ничего быть не должно;
  • 404 / NOT_FOUND - ресурс не найден;
  • 403 / FORBIDDEN - ползователь, выполняющий запрос не авторизован;
  • 405 / METHOD_NOT_ALLOWED - HTTP-метод не допускается к выполнению для данного ресурса;
  • 409 / CONFLICT -попытка добавить или изменить ресурс, который нарушает какое-либо правило уникальности. Актульано для POST и PUT;
  • 415 / UNSUPPORTED_MEDIA_TYPE - не поддерживаемый формат данных в запросе. Или запрашиваемый формат ответа не поддерживается.

Успешный HTTP статус можно установить для метода аннотацией @ResponseStatus. В случае ошибки обработка пойдет по стандартному Spring пути и выдаст код статуса, соответствующий ошибке.

Валидация данных на входе

Добавление аннотации @Validated(Class<?>[]) позволяет активизировать принудительную валидацию данных на входе в метод (только при условии запуска в контексте Spring). Параметр класса опциональный и его задача заключается в группировке типов проверок. Например, необходимо в разных методах выполнять разные проверки на длинну строки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyObject {
interface ShortName{};
interface LongName{};
interface Adult{};
interface Tall{};

@Size(min = 5, max=20, groups = {ShortName.class})
@Size(min = 10, max=100, groups = {LongName.class})
public String name;
@Min(value = 18, groups = Adult.class)
public Integer age;
@NotEmpty
public String phone;
@Min(value = 200, groups = Tall.class)
public int height;
}

@PostMapping
public void addMyObject(@Validated({Contract.ShortName.class, Contract.Adult.class}) MyObject obj) {...}

В примере выше, сработают 3 проверки - name будет проверен на короткое имя; age будет проверен на значение от 18; phone будет проверен на не пустую строку. height проверен не будет, поскольку класс проверки указан, но в @Validated он не фигурирует.

Запрет на вывод

Ранее отмечалась аннотация @JsonIgnore, но она запрещает чтение и запись в поле объекта. Если необходимо добавить только чтения или только записи для поля, то можно применить аннотацию @JsonProperty:

1
2
@JsonProperty(access = JsonProperty.Access.READ_WRITE)
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)

Перехват ошибок

Правила такие же как и для MVC. В контроллере молжно указать перехватку ошибок, с указанием статуса, типа исключения и дополнительно обработать (если потребуется).

Но поскольку ранее была затронута тема валидации, следует добавить возможность перехвата подобных ошибок, вызванных действиями пользователя:

1
2
3
4
5
6
public void create(@Validated @RequestBody MyObjectDto obj, BindingResult result) {
if(result.hasErrors()) {
throw new IllegalArgumentException(“invalid data: ” + result);
}
...
}

В случае ошибки валидации данный метод вызовет исключение (текст можно записатьподробнее). Пользуясь возможностям MVC можно перехватить данное исключение:

1
2
3
4
5
6
7
8
9
10
@RestController
public class MyObjectController {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<MyJsonError> onValidationException(Exception e) {
if(e instanceof IllegalArgumentException) {
return ResponseEntity.badRequest().body(MyJsonError.badRequest(e.getMessage));
}
return ResponseEntity.internalServerError().body(MyJsonError.unknown(e.getMessage()));
}
}

Так же как и в MVC, удачной практикой является добавление класса с аннотацией @ControllerAdvice, в котором будут содержаться перехваты ошибок. К @ControllerAdvice можно добавить ограничение по классам (де-факто, контроллерам), к которым перехваты ошибок будут применяться:

1
2
@ControllerAdvice(basePackageClasses={MyObjectController.class})
public class ObjectOnlyControllersExceptionsHandler {...}

RestTemplate

Класс используется для выполенения HTTP запросов к удаленным узлам и получения и обработки ответов. Фактически, это HTTP-клиент, являющийся частью Spring.

Его часто используют для тестирования приложения, поскольку он уже входит в состав Spring. При этом нельзя назвать его удобным инструментом!

Чем REST так хорош?

  • можно подключать различные форматы для передачи данных (JSON, XML, PlainText и т.д.);
  • применительно к Spring - поскольку в основе лежит MVC, доступны все инструменты MVC для распределения нагрузки;
  • потребителем может выступать любой HTTP-клиент;
  • существуют готовые инструменты для документации сервисов для потребителей (см. Swagger).
 Comments
Comment plugin failed to load
Loading comment plugin