Перейти к основному содержимому

Использование JSON Patch в Spring REST API

· 8 мин. чтения

Задача: Сумма двух

Дано массив целых чисел и целая сумма. Нужно найти индексы двух чисел, сумма которых равна заданной ...

ANDROMEDA

1. Введение

Среди различных доступных HTTP-методов метод HTTP PATCH играет уникальную роль. Это позволяет нам применять частичные обновления к ресурсам HTTP.

В этом руководстве мы рассмотрим, как использовать метод HTTP PATCH вместе с форматом документа JSON Patch для применения частичных обновлений к нашим ресурсам RESTful.

2. Вариант использования

Начнем с рассмотрения примера ресурса HTTP Customer , представленного документом JSON:

{ 
"id":"1",
"telephone":"001-555-1234",
"favorites":["Milk","Eggs"],
"communicationPreferences": {"post":true, "email":true}
}

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

Как бы мы это сделали?

Первым на ум приходит популярный метод HTTP PUT. Однако, поскольку PUT полностью заменяет ресурс, этот метод не подходит для элегантного применения частичных обновлений. Более того, клиенты должны выполнить GET, прежде чем обновления будут применены и сохранены.

Вот где пригодится метод HTTP PATCH .

Давайте разберемся с методом HTTP PATCH и форматами JSON Patch.

3. Метод HTTP PATCH и формат исправления JSON

Метод HTTP PATCH предлагает хороший способ применения частичных обновлений к ресурсам. В результате клиентам нужно отправлять только различия в своих запросах.

Давайте рассмотрим простой пример запроса HTTP PATCH:

PATCH /customers/1234 HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 100

[description of changes]

Тело запроса HTTP PATCH описывает, как следует изменить целевой ресурс для создания новой версии. Кроме того, формат, используемый для представления [описания изменений], зависит от типа ресурса. Для типов ресурсов JSON для описания изменений используется формат JSON Patch .

Проще говоря, формат JSON Patch использует «серию операций» для описания того, как должен быть изменен целевой ресурс. Документ JSON Patch представляет собой массив объектов JSON. Каждый объект в массиве представляет ровно одну операцию JSON Patch.

Давайте теперь рассмотрим операции JSON Patch вместе с некоторыми примерами.

4. Операции исправления JSON

Операция JSON Patch представлена одним объектом операции .

Например, здесь мы определяем операцию исправления JSON для обновления номера телефона клиента:

{
"op":"replace",
"path":"/telephone",
"value":"001-555-5678"
}

Каждая операция должна иметь один член пути . Кроме того, некоторые операционные объекты также должны содержать элемент from . Значением пути и от членов является указатель JSON . Это относится к местоположению в целевом документе. Это местоположение может указывать на определенный ключ или элемент массива в целевом объекте.

Давайте теперь кратко рассмотрим доступные операции JSON Patch.

4.1. Операция добавления _

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

Например, добавим «Хлеб» в список избранного покупателя с индексом 0:

{
"op":"add",
"path":"/favorites/0",
"value":"Bread"
}

Измененные данные клиента после операции добавления будут такими:

{
"id":"1",
"telephone":"001-555-1234",
"favorites":["Bread","Milk","Eggs"],
"communicationPreferences": {"post":true, "email":true}
}

4.2. Операция удаления _

Операция удаления удаляет значение в целевом местоположении. Кроме того, он может удалить элемент из массива по указанному индексу.

Например, давайте удалим communcationPreferences для нашего клиента:

{
"op":"remove",
"path":"/communicationPreferences"
}

Измененные данные клиента после операции удаления будут такими:

{
"id":"1",
"telephone":"001-555-1234",
"favorites":["Bread","Milk","Eggs"],
"communicationPreferences":null
}

4.3. Операция замены _

Операция замены обновляет значение в целевом местоположении новым значением.

В качестве примера давайте обновим номер телефона для нашего клиента:

{
"op":"replace",
"path":"/telephone",
"value":"001-555-5678"
}

Измененные данные клиента после операции замены будут такими:

{ 
"id":"1",
"telephone":"001-555-5678",
"favorites":["Bread","Milk","Eggs"],
"communicationPreferences":null
}

4.4. Операция перемещения _

Операция перемещения удаляет значение из указанного расположения и добавляет его в целевое расположение.

Например, давайте переместим «Хлеб» из верхней части списка избранного клиента в нижнюю часть списка:

{
"op":"move",
"from":"/favorites/0",
"path":"/favorites/-"
}

Измененные данные клиента после операции перемещения будут следующими:

{ 
"id":"1",
"telephone":"001-555-5678",
"favorites":["Milk","Eggs","Bread"],
"communicationPreferences":null
}

/ favorites/0 и /favorites/- в приведенном выше примере являются указателями JSON на начальный и конечный индексы массива избранного .

4.5. Операция копирования _

Операция копирования копирует значение из указанного расположения в целевое расположение.

Например, продублируем «Молоко» в списке избранного :

{
"op":"copy",
"from":"/favorites/0",
"path":"/favorites/-"
}

Измененные данные клиента после операции копирования будут такими:

{ 
"id":"1",
"telephone":"001-555-5678",
"favorites":["Milk","Eggs","Bread","Milk"],
"communicationPreferences":null
}

4.6. Тестовая операция _

Тестовая операция проверяет , что значение в «пути» равно «значению». Поскольку операция PATCH является атомарной, PATCH следует отбросить, если какая-либо из ее операций не удалась. Операция тестирования может использоваться для проверки того, что предварительные и последующие условия выполнены.

Например, давайте проверим, что обновление поля телефона клиента прошло успешно:

{
"op":"test",
"path":"/telephone",
"value":"001-555-5678"
}

Давайте теперь посмотрим, как мы можем применить вышеуказанные концепции к нашему примеру.

5. Запрос HTTP PATCH с использованием формата исправления JSON

Мы вернемся к нашему варианту использования Customer .

Вот HTTP-запрос PATCH для частичного обновления списка телефонов и избранного клиента с использованием формата JSON Patch:

curl -i -X PATCH http://localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[
{"op":"replace","path":"/telephone","value":"+1-555-56"},
{"op":"add","path":"/favorites/0","value":"Bread"}
]'

Самое главное, Content-Type для запросов JSON Patch — это application/json-patch+json . Также тело запроса представляет собой массив объектов операции JSON Patch:

[
{"op":"replace","path":"/telephone","value":"+1-555-56"},
{"op":"add","path":"/favorites/0","value":"Bread"}
]

Как бы мы обработали такой запрос на стороне сервера?

Один из способов — написать собственную структуру, которая последовательно оценивает операции и применяет их к целевому ресурсу как к атомарной единице. Понятно, что такой подход кажется сложным. Кроме того, это может привести к нестандартному способу использования документов исправлений.

К счастью, нам не нужно вручную обрабатывать запросы JSON Patch.

Java API для обработки JSON 1.0 или JSON-P 1.0, первоначально определенный в JSR 353 , представил поддержку JSON Patch в JSR 374 . API JSON-P предоставляет тип JsonPatch для представления реализации JSON Patch.

Однако JSON-P — это всего лишь API. Для работы с JSON-P API нам нужно использовать библиотеку, которая его реализует. Мы будем использовать одну из таких библиотек под названием json-patch для примеров в этой статье.

Давайте теперь посмотрим, как мы можем создать службу REST, которая использует HTTP-запросы PATCH с использованием формата JSON Patch, описанного выше.

6. Реализация патча JSON в приложении Spring Boot

6.1. Зависимости

Последнюю версию json-patch можно найти в репозитории Maven Central.

Для начала добавим зависимости в pom.xml :

<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.12</version>
</dependency>

Теперь давайте определим класс схемы для представления документа JSON клиента :

public class Customer {
private String id;
private String telephone;
private List<String> favorites;
private Map<String, Boolean> communicationPreferences;

// standard getters and setters
}

Далее мы рассмотрим наш метод контроллера.

6.2. Метод контроллера REST

Затем мы можем реализовать HTTP PATCH для варианта использования нашего клиента:

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json")
public ResponseEntity<Customer> updateCustomer(@PathVariable String id, @RequestBody JsonPatch patch) {
try {
Customer customer = customerService.findCustomer(id).orElseThrow(CustomerNotFoundException::new);
Customer customerPatched = applyPatchToCustomer(patch, customer);
customerService.updateCustomer(customerPatched);
return ResponseEntity.ok(customerPatched);
} catch (JsonPatchException | JsonProcessingException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} catch (CustomerNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}

Давайте теперь разберемся, что происходит в этом методе:

  • Для начала мы используем аннотацию @PatchMapping , чтобы пометить метод как метод обработчика PATCH.
  • Когда поступает запрос на исправление с application/json-patch+json «Content-Type», Spring Boot использует MappingJackson2HttpMessageConverter по умолчанию для преобразования полезной нагрузки запроса в экземпляр JsonPatch . В результате наш метод контроллера получит тело запроса в виде экземпляра JsonPatch.

В рамках метода:

  1. Сначала мы вызываем метод customerService.findCustomer(id) , чтобы найти запись клиента.
  2. Впоследствии, если запись клиента найдена, мы вызываем метод applyPatchToCustomer(patch, customer) . Это применяет JsonPatch к клиенту (подробнее об этом позже) .
  3. Затем мы вызываем customerService.updateCustomer(customerPatched) , чтобы сохранить запись клиента.
  4. Наконец, мы возвращаем клиенту ответ 200 OK с исправленными данными клиента в ответе.

Самое главное, настоящее волшебство происходит в методе applyPatchToCustomer(patch, customer) :

private Customer applyPatchToCustomer(
JsonPatch patch, Customer targetCustomer) throws JsonPatchException, JsonProcessingException {
JsonNode patched = patch.apply(objectMapper.convertValue(targetCustomer, JsonNode.class));
return objectMapper.treeToValue(patched, Customer.class);
}
  1. Начнем с того, что у нас есть наш экземпляр JsonPatch , который содержит список операций, которые нужно применить к целевому клиенту .
  2. Затем мы преобразуем целевого клиента в экземпляр com.fasterxml.jackson.databind.JsonNode и передаем его методу JsonPatch.apply для применения исправления. За кулисами JsonPatch.apply имеет дело с применением операций к цели. Результатом исправления также является экземпляр com.fasterxml.jackson.databind.JsonNode.
  3. Затем мы вызываем метод objectMapper.treeToValue , который связывает данные в исправленном com.fasterxml.jackson.databind.JsonNode с типом Customer . Это наш пропатченный экземпляр Customer
  4. Наконец, мы возвращаем пропатченный экземпляр Customer

Давайте теперь запустим несколько тестов для нашего API.

6.3. Тестирование

Для начала создадим клиента с помощью POST-запроса к нашему API:

curl -i -X POST http://localhost:8080/customers -H "Content-Type: application/json" 
-d '{"telephone":"+1-555-12","favorites":["Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}'

Получаем ответ 201 Created :

HTTP/1.1 201
Location: http://localhost:8080/customers/1

В заголовке ответа Location указывается местоположение нового ресурса. Это указывает на то, что идентификатор нового клиента равен 1.

Затем давайте запросим частичное обновление для этого клиента с помощью запроса PATCH:

curl -i -X PATCH http://localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[
{"op":"replace","path":"/telephone","value":"+1-555-56"},
{"op":"add","path":"/favorites/0","value": "Bread"}
]'

Мы получаем ответ 200 OK с исправленными данными клиента:

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 14 Feb 2020 21:23:14 GMT

{"id":"1","telephone":"+1-555-56","favorites":["Bread","Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}

7. Заключение

В этой статье мы рассмотрели, как реализовать JSON Patch в Spring REST API.

Для начала мы рассмотрели метод HTTP PATCH и его способность выполнять частичные обновления.

Затем мы рассмотрели, что такое JSON Patch, и разобрались с различными операциями JSON Patch.

Наконец, мы обсудили, как обрабатывать HTTP-запрос PATCH в приложении Spring Boot с использованием библиотеки json-patch.

Как всегда, исходный код примеров, используемых в этой статье, доступен на GitHub .