1. 简介
在众多 HTTP 方法中,PATCH
方法有着独特的定位——它专为部分更新资源而生。
本文将深入探讨如何结合 HTTP PATCH
方法与 JSON Patch 格式,在 Spring 构建的 RESTful 接口中实现高效、精准的字段级更新。
2. 业务场景
设想一个 Customer
客户资源,其 JSON 表示如下:
{
"id": "1",
"telephone": "001-555-1234",
"favorites": ["Milk", "Eggs"],
"communicationPreferences": { "post": true, "email": true }
}
现在客户更换了电话号码,并新增了一项“最爱商品”。我们只需更新 telephone
和 favorites
两个字段。
如何实现?
第一反应可能是 PUT
。但 PUT
是全量替换,不仅效率低,客户端还得先 GET
一次原始数据,否则会误删未提交的字段 ❌。
更优雅的方案是使用 HTTP PATCH 方法 ✅。
3. HTTP PATCH 与 JSON Patch 格式
PATCH
方法的核心优势在于:客户端只需发送“差异”,而非完整资源。
一个典型的 PATCH
请求如下:
PATCH /customers/1234 HTTP/1.1
Host: www.example.com
Content-Type: application/json-patch+json
If-Match: "e0023aa4e"
Content-Length: 100
[
{ "op": "replace", "path": "/telephone", "value": "001-555-5678" }
]
关键点:
- ✅ 请求体描述了目标资源的变更方式。
- ✅ 对于 JSON 资源,变更描述格式遵循 JSON Patch 标准。
- ✅ JSON Patch 文档是一个 JSON 对象数组,每个对象代表一个操作。
4. JSON Patch 操作类型
每个操作由一个 op
字段标识,配合 path
(必填)和 from
(可选)字段。path
和 from
的值是 JSON Pointer,用于定位目标文档中的具体位置。
4.1. add
操作
用于:
- 向对象添加新字段
- 更新已有字段
- 向数组指定索引插入元素
示例:在 favorites
列表开头添加 “Bread”
{
"op": "add",
"path": "/favorites/0",
"value": "Bread"
}
执行后结果:
{
"id": "1",
"telephone": "001-555-1234",
"favorites": ["Bread", "Milk", "Eggs"],
"communicationPreferences": { "post": true, "email": true }
}
4.2. remove
操作
删除指定位置的值,或移除数组中指定索引的元素。
示例:移除 communicationPreferences
{
"op": "remove",
"path": "/communicationPreferences"
}
执行后结果:
{
"id": "1",
"telephone": "001-555-1234",
"favorites": ["Bread", "Milk", "Eggs"]
// communicationPreferences 字段被完全移除
}
⚠️ 注意:
remove
是彻底删除字段,不是设为null
。
4.3. replace
操作
用新值替换指定位置的现有值。
示例:更新电话号码
{
"op": "replace",
"path": "/telephone",
"value": "001-555-5678"
}
执行后结果:
{
"id": "1",
"telephone": "001-555-5678",
"favorites": ["Bread", "Milk", "Eggs"]
}
4.4. move
操作
将 from
位置的值移动到 path
位置。
示例:将 favorites
列表的第一个元素移到末尾
{
"op": "move",
"from": "/favorites/0",
"path": "/favorites/-"
}
执行后结果:
{
"id": "1",
"telephone": "001-555-5678",
"favorites": ["Milk", "Eggs", "Bread"]
}
✅
/favorites/0
指向数组第一个元素,/favorites/-
是一个特殊语法,指向数组末尾。
4.5. copy
操作
将 from
位置的值复制到 path
位置。
示例:复制 favorites
中的第一个元素到末尾
{
"op": "copy",
"from": "/favorites/0",
"path": "/favorites/-"
}
执行后结果:
{
"id": "1",
"telephone": "001-555-5678",
"favorites": ["Milk", "Eggs", "Bread", "Milk"]
}
4.6. test
操作
断言 path
处的值是否等于 value
。**PATCH
请求是原子操作**,任何一个 test
失败,整个请求都会被拒绝。
常用于校验前置/后置条件。
示例:校验电话号码是否已更新
{
"op": "test",
"path": "/telephone",
"value": "001-555-5678"
}
5. 使用 JSON Patch 的 HTTP PATCH 请求
回到 Customer
场景,我们发起一个 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
必须是application/json-patch+json
- ✅ 请求体是一个 JSON Patch 操作数组
服务端如何处理?自己解析?太麻烦且容易出错。
推荐使用成熟的库:json-patch。
6. 在 Spring Boot 中实现 JSON Patch
6.1. 依赖
引入 json-patch 库:
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.12</version>
</dependency>
定义 Customer
实体:
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 控制器方法
@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 patchedCustomer = applyPatch(patch, customer);
customerService.updateCustomer(patchedCustomer);
return ResponseEntity.ok(patchedCustomer);
} catch (JsonPatchException | JsonProcessingException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} catch (CustomerNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
private Customer applyPatch(JsonPatch patch, Customer target)
throws JsonPatchException, JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode patchedNode = patch.apply(objectMapper.convertValue(target, JsonNode.class));
return objectMapper.treeToValue(patchedNode, Customer.class);
}
核心逻辑 applyPatch
解析:
objectMapper.convertValue(target, JsonNode.class)
:将Customer
对象转为 Jackson 的JsonNode
树patch.apply(...)
:由json-patch
库执行所有操作,返回新的JsonNode
objectMapper.treeToValue(...)
:将JsonNode
绑定回Customer
对象
整个过程简单粗暴,且原子性由库保证。
6.3. 测试
创建客户:
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}
}'
响应:
HTTP/1.1 201 Created
Location: http://localhost:8080/customers/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):
{
"id":"1",
"telephone":"+1-555-56",
"favorites":["Bread","Milk","Eggs"],
"communicationPreferences":{"post":true,"email":true}
}
7. 总结
本文介绍了在 Spring REST 接口中利用 JSON Patch 实现部分更新的完整方案:
- ✅
PATCH
方法是部分更新的语义标准 - ✅ JSON Patch 提供了结构化的变更描述格式
- ✅ 借助
json-patch
库,服务端处理变得异常简单
相比手动解析部分更新字段,JSON Patch 更标准、更灵活,尤其适合字段多且更新场景复杂的接口。踩坑建议:务必处理好 JsonPatchException
,并考虑结合 If-Match
实现乐观锁。
示例代码已托管至 GitHub:https://github.com/baeldung/spring-rest-http