Using JSON Patch in Spring REST APIs – 在Spring REST APIs中使用JSON补丁

最后修改: 2020年 2月 22日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

Of the various HTTP methods available, the HTTP PATCH method plays a unique role. It allows us to apply partial updates to HTTP resources.

在各种可用的HTTP方法中,HTTP PATCH方法扮演着一个独特的角色。它允许我们对HTTP资源应用部分更新。

In this tutorial, we’ll look at how to use the HTTP PATCH method along with the JSON Patch document format to apply partial updates to our RESTful resources.

在本教程中,我们将看看如何使用HTTP PATCH方法以及JSON补丁文档格式来应用部分更新到我们的RESTful资源。

2. The Use Case

2.用例

Let’s start by considering an example HTTP Customer resource represented by the JSON document:

让我们首先考虑一个由JSON文档代表的HTTP Customer资源例子。

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

Let’s assume that this customer’s telephone number has changed and that the customer added a new item to their list of favorite products. This means that we need to update only the telephone and favorites fields of the Customer.

让我们假设这个客户的电话号码发生了变化,并且客户在他们最喜欢的产品列表中添加了一个新的项目。这意味着我们只需要更新电话收藏夹字段的客户

How would we do that?

我们将如何做到这一点?

The popular HTTP PUT method comes to mind first. However, because the PUT replaces a resource entirely, it’s not a suitable method to apply partial updates elegantly. Moreover, the clients have to perform a GET before the updates are applied and saved.

首先想到的是流行的HTTP PUT方法。然而,由于PUT完全取代了一个资源,它不是一个适合优雅地应用部分更新的方法。此外,在应用和保存更新之前,客户端必须执行GET。

This is where the HTTP PATCH method comes in handy.

这就是HTTP PATCH方法派上用场的地方。

Let’s understand the HTTP PATCH method and the JSON Patch formats.

让我们了解一下HTTP PATCH方法和JSON补丁格式。

3. The HTTP PATCH Method and the JSON Patch Format

3.HTTP PATCH方法和JSON补丁格式

The HTTP PATCH method offers a nice way to apply partial updates to resources. As a result, clients need to send only the differences in their requests.

HTTP PATCH方法提供了一个很好的方法来应用资源的部分更新。因此,客户需要在他们的请求中只发送不同的内容。

Let’s look at a simple example of an HTTP PATCH request:

让我们看一下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]

The HTTP PATCH request body describes how the target resource should be modified to produce a new version. Furthermore, the format used to represent the [description of changes] varies depending on the resource type. For JSON resource types, the format used to describe the changes is JSON Patch.

HTTP PATCH请求体描述了应如何修改目标资源以产生一个新的版本。此外,用于表示[变化描述]的格式因资源类型而异。对于JSON资源类型,用于描述更改的格式是JSON补丁

Simply put, the JSON Patch format uses a “series of operations” to describe how the target resource should be modified. A JSON Patch document is an array of JSON objects. Each object in the array represents exactly one JSON Patch operation.

简单地说,JSON补丁格式使用 “一系列操作 “来描述目标资源应该如何被修改。一个JSON补丁文件是一个JSON对象的数组。数组中的每个对象正好代表一个JSON补丁操作。

Let’s now look into the JSON Patch operations along with some examples.

现在让我们来看看JSON补丁的操作和一些例子。

4. JSON Patch Operations

4.JSON补丁操作

A JSON Patch operation is represented by a single op object.

一个JSON补丁操作由一个op对象表示。

For example, here we’re defining a JSON patch operation to update the customer’s telephone number:

例如,在这里我们定义了一个JSON补丁操作来更新客户的电话号码。

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

Each operation must have one path member. Also, some operation objects must contain a from member as well. The value of the path and from members is a JSON Pointer. It refers to a location within the target document. This location can point to a specific key or an array element in the target object.

每个操作必须有一个path成员。另外,一些操作对象也必须包含一个from成员。pathfrom成员的值是一个JSON 指针。它指的是目标文档中的一个位置。这个位置可以指向目标对象中的一个特定键或一个数组元素。

Let’s now briefly look at the available JSON Patch operations.

现在让我们简单地看看可用的JSON补丁操作。

4.1. The add Operation

4.1.添加操作

We use the add operation to add a new member to an object. Also, we can use it to update an existing member and to insert a new value into the array at the specified index.

我们使用add操作向一个对象添加一个新成员。此外,我们还可以用它来更新一个现有的成员,并在指定的索引处向数组中插入一个新值。

For example, let’s add “Bread” to the customer’s favorites list at index 0:

例如,让我们把 “Bread “添加到顾客的favorites列表中,索引为0。

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

The modified customer details after the add operation would be:

添加操作后,修改后的客户资料将是。

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

4.2. The remove Operation

4.2.remove操作

The remove operation removes a value at the target location. Besides, it can remove an element from an array at the specified index.

remove操作在目标位置删除一个值。此外,它可以在指定的索引处从数组中删除一个元素。

For instance, let’s remove the communcationPreferences for our customer:

例如,让我们为我们的客户删除communcationPreferences

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

The modified customer details after the remove operation would be:

remove操作之后,修改后的客户资料将是。

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

4.3. The replace Operation

4.3.replace操作

The replace operation updates the value at the target location with a new value.

replace操作用一个新值更新目标位置的值。

As an example, let’s update the telephone number for our customer:

作为一个例子,让我们更新客户的电话号码。

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

The modified customer details after the replace operation would be:

replace操作之后,修改后的客户资料将是。

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

4.4. The move Operation

4.4.移动操作

The move operation removes the value at the specified location and adds it to the target location.

move操作删除指定位置的值并将其添加到目标位置。

For instance, let’s move “Bread” from the top of the customer’s favorites list to the bottom of the list:

例如,让我们把 “面包 “从客户最喜欢的列表的顶部移到列表的底部。

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

The modified customer details after the move operation would be:

移动操作后,修改后的客户资料将是。

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

The /favorites/0 and /favorites/- in the above example are JSON pointers to the start and end indices of the favorites array.

上例中的/favorites/0/favorites/-是指向favorites数组的开始和结束索引的JSON指针。

4.5. The copy Operation

4.5.复制操作

The copy operation copies the value at the specified location to the target location.

copy操作将指定位置的值复制到目标位置。

For example, let’s duplicate “Milk” in the favorites list:

例如,让我们在favorites列表中复制 “Milk”。

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

The modified customer details after the copy operation would be:

copy操作之后,修改后的客户资料将是。

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

4.6. The test Operation

4.6.测试操作

The test operation tests that the value at the “path” is equal to the “value”. Because the PATCH operation is atomic, the PATCH should be discarded if any of its operations fail. The test operation can be used to validate that the preconditions and post-conditions have been met.

test操作测试 “path “处的值是否等于 “value”。因为PATCH操作是原子性的,如果它的任何操作失败,PATCH应该被丢弃。test操作可用于验证前提条件和后置条件是否已经满足。

For instance, let’s test that the update to the customer’s telephone field has been successful:

例如,让我们测试一下对客户telephone字段的更新是否已经成功。

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

Let’s now see how we can apply the above concepts to our example.

现在让我们看看如何将上述概念应用于我们的例子。

5. HTTP PATCH Request Using the JSON Patch Format

5.使用JSON补丁格式的HTTP PATCH请求

We’ll revisit our Customer use case.

我们再来看看我们的Customer用例。

Here is the HTTP PATCH request to perform a partial update to the customer’s telephone and favorites list using the JSON Patch format:

下面是HTTP PATCH请求,使用JSON补丁格式对客户的电话最爱列表进行部分更新。

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"}
]'

Most importantly, the Content-Type for JSON Patch requests is application/json-patch+json. Also, the request body is an array of JSON Patch operation objects:

最重要的是,JSON补丁请求的Content-Typeapplication/json-patch+json。另外,请求主体是一个JSON补丁操作对象的数组:

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

How would we process such a request on the server-side?

我们将如何在服务器端处理这样一个请求?

One way is to write a custom framework that evaluates the operations sequentially and applies them to the target resource as an atomic unit.  Clearly, this approach sounds complicated. Also, it can lead to a non-standardized way of consuming patch documents.

一种方法是编写一个自定义框架,按顺序评估操作,并将其作为一个原子单元应用于目标资源。 显然,这种方法听起来很复杂。而且,它可能会导致消耗补丁文件的非标准化方式。

Fortunately, we do not have to hand-craft the processing of JSON Patch requests.

幸运的是,我们不需要手工处理JSON补丁请求。

The Java API for JSON Processing 1.0, or JSON-P 1.0, defined originally in JSR 353, introduced support for the JSON Patch in JSR 374. The JSON-P API provides the JsonPatch type to represent the JSON Patch implementation.

最初在JSR 353中定义的用于JSON处理的Java API 1.0,或JSON-P 1.0,在JSR 374中引入了对JSON补丁的支持。JSON-P API提供了JsonPatch类型来代表JSON补丁的实现。

However, JSON-P is only an API. To work with the JSON-P API, we need to use a library that implements it. We’ll use one such library called json-patch for the examples in this article.

然而,JSON-P只是一个API。为了使用JSON-P API,我们需要使用一个实现它的库。在本文的例子中,我们将使用一个叫做json-patch的这样的库。

Let’s now look at how we can build a REST service that consumes HTTP PATCH requests using the JSON Patch format described above.

现在让我们来看看我们如何建立一个REST服务,使用上面描述的JSON补丁格式来消费HTTP PATCH请求。

6. Implementing JSON Patch in a Spring Boot Application

6.在Spring Boot应用程序中实现JSON补丁

6.1. Dependencies

6.1.依赖性

The latest version of json-patch can be found from the Maven Central repository.

json-patch的最新版本可以从Maven Central仓库找到。

To begin with, let’s add the dependencies to the pom.xml:

首先,让我们在pom.xml中添加依赖项。

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

Now, let’s define a schema class to represent the Customer JSON document :

现在,让我们定义一个模式类来表示Customer JSON文档。

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

    // standard getters and setters
}

Next, we’ll look at our controller method.

接下来,我们来看看我们的控制器方法。

6.2. The REST Controller Method

6.2.REST控制器方法

Then, we can implement HTTP PATCH for our customer use case:

然后,我们可以为我们的客户用例实现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();
    }
}

Let’s now understand what is going on in this method:

现在让我们了解一下这个方法是怎么回事。

  • To begin with, we use the @PatchMapping annotation to mark the method as a PATCH handler method
  • When a patch request with the application/json-patch+json “Content-Type” arrives, Spring Boot uses the default MappingJackson2HttpMessageConverter to convert the request payload to a JsonPatch instance. As a result, our controller method will receive the request body as a JsonPatch instance

Within the method:

在该方法内。

  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

Most importantly, the real magic happens in the applyPatchToCustomer(patch, customer) method:

最重要的是,真正的魔法发生在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

Let’s now run some tests against our API.

现在让我们针对我们的API运行一些测试。

6.3. Testing

6.3.测试

To begin with, let’s create a customer using a POST request to our API:

首先,让我们用一个POST请求来创建一个客户。

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}}'

We receive a 201 Created response:

我们收到了一个201创建的回复。

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

The Location response header is set to the location of the new resource. It indicates that the id of the new Customer is 1.

Location响应头被设置为新资源的位置。它表示新客户id是1。

Next, let’s request a partial update to this customer using a PATCH request:

接下来,让我们使用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"}
]'

We receive a 200 OK response with the patched customer details:

我们收到一个200OK的响应,其中包含了被修补的客户信息。

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. Conclusion

7.结语

In this article, we looked at how to implement JSON Patch in Spring REST APIs.

在这篇文章中,我们研究了如何在Spring REST APIs中实现JSON补丁。

To begin with, we looked at the HTTP PATCH method and its ability to perform partial updates.

首先,我们研究了HTTP PATCH方法和它执行部分更新的能力。

We then looked into what is JSON Patch and understood the various JSON Patch operations.

然后我们研究了什么是JSON补丁,并了解了各种JSON补丁操作。

Finally, we discussed how to handle an HTTP PATCH request in a Spring Boot application using the json-patch library.

最后,我们讨论了如何使用json-patch库在Spring Boot应用程序中处理HTTP PATCH请求。

As always, the source code for the examples used in this article is available over on GitHub.

一如既往,本文中所使用的示例的源代码可在GitHub上获取