1. 概述
本文快速解析 HTTP PUT 和 PATCH 两种动词的语义差异,并对比它们在 REST API 中的实际应用场景。我们将通过 Spring 框架实现两个支持这两种操作的接口,帮助开发者更清晰地理解它们的区别及正确使用方式。
2. 何时使用 PUT,何时使用 PATCH?
简单粗暴地说:
- ✅ PUT:当客户端需要完全替换现有资源时使用
- ✅ PATCH:当客户端只需要部分更新资源时使用
举个实际场景:如果只需要更新资源的单个字段(如地址),发送完整的资源表示会显得笨拙且浪费带宽。这种情况下,PATCH 的语义显然更合适。
另一个关键差异是幂等性:
- ✅ PUT 是幂等的:多次执行同一操作结果不变
- ⚠️ PATCH 可能非幂等:取决于具体实现(非强制要求)
根据业务操作的幂等需求,这也是选择方法的重要依据。
3. 实现 PUT 和 PATCH 逻辑
假设我们要更新一个包含多个字段的 HeavyResource
:
public class HeavyResource {
private Integer id;
private String name;
private String address;
// ...
3.1 PUT 实现完整更新
@PutMapping("/heavyresource/{id}")
public ResponseEntity<?> saveResource(@RequestBody HeavyResource heavyResource,
@PathVariable("id") String id) {
heavyResourceRepository.save(heavyResource, id);
return ResponseEntity.ok("resource saved");
}
这是标准的资源更新接口实现。
3.2 PATCH 实现部分更新
当 address
字段需要频繁更新时,我们不想每次都发送完整的 HeavyResource
对象。这时可以:
创建专用 DTO(仅包含需更新字段):
public class HeavyResourceAddressOnly { private Integer id; private String address; // ... }
实现 PATCH 接口:
@PatchMapping("/heavyresource/{id}") public ResponseEntity<?> partialUpdateName( @RequestBody HeavyResourceAddressOnly partialUpdate, @PathVariable("id") String id) { heavyResourceRepository.save(partialUpdate, id); return ResponseEntity.ok("resource address updated"); }
这种细粒度的 DTO 只发送需要更新的字段,避免了传输完整对象的开销。
3.3 通用 PATCH 方案
当有大量部分更新操作时,可以跳过创建专用 DTO,直接使用 Map:
@RequestMapping(value = "/heavyresource/{id}", method = RequestMethod.PATCH, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> partialUpdateGeneric(
@RequestBody Map<String, Object> updates,
@PathVariable("id") String id) {
heavyResourceRepository.save(updates, id);
return ResponseEntity.ok("resource updated");
}
⚠️ 注意:这种方案更灵活,但会失去数据校验能力(如 DTO 的注解验证)。
4. 测试 PUT 和 PATCH
4.1 测试 PUT 完整更新
mockMvc.perform(put("/heavyresource/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(
new HeavyResource(1, "Tom", "Jackson", 12, "heaven street")))
).andExpect(status().isOk());
4.2 测试 PATCH 部分更新
mockMvc.perform(patch("/heavyrecource/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(
new HeavyResourceAddressOnly(1, "5th avenue")))
).andExpect(status().isOk());
4.3 测试通用 PATCH
HashMap<String, Object> updates = new HashMap<>();
updates.put("address", "5th avenue");
mockMvc.perform(patch("/heavyresource/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(updates))
).andExpect(status().isOk());
5. 处理包含 null 值的部分请求
实现 PATCH 时需明确约定:当收到 address
字段为 null
的请求时如何处理?例如:
{
"id" : 1,
"address" : null
}
可选策略:
- ✅ 显式置空:将字段值设为
null
- ✅ 忽略更新:视为无变更操作
⚠️ 重要:必须在所有 PATCH 实现中保持一致的 null
处理策略,避免踩坑。
6. 总结
本文深入对比了 HTTP PATCH 和 PUT 的核心差异:
- PUT:完整资源替换,强制幂等
- PATCH:部分资源更新,非强制幂等
我们通过 Spring 实现了:
- 使用 PUT 的完整资源更新接口
- 使用 PATCH 的部分更新接口(专用 DTO 和通用 Map 两种方案)
所有示例代码可在 GitHub 项目 中获取(Maven 项目,可直接导入运行)。