«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 представлена ​​одним объектом операции.

Например, здесь мы определяем операцию исправления 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.

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

Однако 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. First, we call the customerService.findCustomer(id) method to find the customer record
  2. Subsequently, if the customer record is found, we invoke the applyPatchToCustomer(patch, customer) method. This applies the JsonPatch to the customer (more on this later)
  3. We then invoke the customerService.updateCustomer(customerPatched) to save the customer record
  4. Finally, we return a 200 OK response to the client with the patched Customer details in the response

Самое главное, настоящее волшебство происходит в методе 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. To begin with, we have our JsonPatch instance that holds the list of operations to be applied to the target Customer
  2. We then convert the target Customer into an instance of com.fasterxml.jackson.databind.JsonNode and pass it to the JsonPatch.apply method to apply the patch. Behind the scenes, the JsonPatch.apply deals with applying the operations to the target. The result of the patch is also a com.fasterxml.jackson.databind.JsonNode instance
  3. We then call the objectMapper.treeToValue method, which binds the data in the patched com.fasterxml.jackson.databind.JsonNode to the Customer type. This is our patched Customer instance
  4. Finally, we return the patched Customer instance

Давайте Теперь запустите несколько тестов для нашего 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.