重新理解 RESTful:从理论约束到工程实践
大多数人理解的「RESTful API」只是 HTTP + JSON + 看起来像资源的 URL。本文从 Fielding 的论文原义出发,厘清 REST 的理论模型,再落地到 Spring Boot 工程实践中的设计决策。
一、REST 到底是什么
REST(Representational State Transfer,表述性状态转移)由 Roy Fielding 在 2000 年的博士论文 "Architectural Styles and the Design of Network-Based Software Architectures" 中提出。它不是协议,不是标准,而是一种架构风格------一组用于指导分布式超媒体系统设计的约束条件。
Fielding 本人是 HTTP/1.0 和 HTTP/1.1 规范的主要作者之一,REST 实际上是他对 Web 架构设计经验的理论提炼。
1.1 REST 的六大架构约束
REST 风格由以下六个约束共同定义:
- 客户端-服务器分离(Client-Server):关注点分离,客户端负责用户界面,服务器负责数据存储和业务逻辑,两者独立演进。
- 无状态(Stateless):每个请求必须包含理解该请求所需的全部信息,服务器不存储客户端会话状态。这意味着任何一台服务器都可以处理任意请求,天然适合水平扩展。
- 可缓存(Cacheable):响应必须明确标识是否可缓存,允许客户端或中间层缓存响应以减少交互次数。
- 统一接口(Uniform Interface):这是 REST 最核心的约束,包括资源标识(URI)、通过表述操作资源、自描述消息、以及超媒体驱动应用状态(HATEOAS)。
- 分层系统(Layered System):客户端无法判断自己是直连服务器还是连接到中间层(负载均衡、CDN、网关),各层职责独立。
- 按需代码(Code-On-Demand,可选):服务器可以向客户端传输可执行代码(如 JavaScript),扩展客户端功能。这是唯一的可选约束。
1.2 为什么大多数 API 不是「真正的 REST」
Leonard Richardson 提出了一个成熟度模型(Richardson Maturity Model),将 REST 实现分为四个层次:
- Level 0:一个 URI,一种方法(本质是 RPC over HTTP)
- Level 1:引入多个 URI(资源)
- Level 2:正确使用 HTTP 动词和状态码
- Level 3:引入超媒体控制(HATEOAS)
业界绝大多数自称 RESTful 的 API 停留在 Level 2。Level 3 的 HATEOAS(Hypermedia as the Engine of Application State)要求响应中包含指向相关操作的链接,让客户端通过超媒体发现可用操作,而不是预先硬编码所有 URL。完整实现 REST 的 API 极少------但这不妨碍 Level 2 在工程中是一个非常好的实践标准。
二、工程实践:RESTful API 设计三要素
抛开 Level 3 的超媒体约束,工程中将 REST 风格落地的核心在于三点:URL 表示资源、HTTP 方法表示操作、状态码表示结果。
2.1 URL 设计:资源导向,名词为主
URL 是资源的地址,应该是名词而非动词。HTTP 方法承担「动词」的角色。
# ❌ 动词式(RPC 风格)
GET /getUser?id=1
POST /createUser
POST /deleteUser?id=1
POST /updateUserName
# ✅ 资源式(RESTful 风格)
GET /users/1 # 获取用户
POST /users # 创建用户
DELETE /users/1 # 删除用户
PUT /users/1 # 全量更新用户
PATCH /users/1 # 部分更新用户
资源的层级关系通过 URL 路径表达:
GET /users/1/orders # 用户 1 的所有订单
GET /users/1/orders/99 # 用户 1 的 99 号订单
POST /users/1/orders # 为用户 1 创建订单
DELETE /users/1/orders/99 # 删除用户 1 的 99 号订单
命名约定 :URL 中资源名使用复数形式(/users 而非 /user),路径使用小写字母和连字符(/order-items 而非 /orderItems)。
2.2 HTTP 方法:语义明确,幂等性是核心
| 方法 | 语义 | 安全性 | 幂等性 | 说明 |
|---|---|---|---|---|
| GET | 获取资源 | ✅ 安全 | ✅ 幂等 | 不应产生副作用 |
| POST | 创建资源 | ❌ 不安全 | ❌ 非幂等 | 每次调用可能创建新资源 |
| PUT | 全量替换资源 | ❌ 不安全 | ✅ 幂等 | 发送完整资源表述 |
| PATCH | 部分更新资源 | ❌ 不安全 | ⚠️ 不保证 | 由实现决定,RFC 5789 未要求幂等 |
| DELETE | 删除资源 | ❌ 不安全 | ✅ 幂等 | 重复删除不改变服务器最终状态 |
关于幂等性的精确定义:RFC 9110 定义幂等为「多次相同请求对服务器产生的预期效果等同于单次请求的效果」。注意它关注的是服务器状态,而非响应内容------DELETE 同一个资源第二次可能返回 404,但服务器状态没有再次改变,所以仍是幂等的。
PATCH 为什么不保证幂等? 如果 PATCH 请求是「将 age 设为 30」,那它是幂等的;但如果是「将 age 加 1」,那它就不是幂等的。RFC 5789 将幂等性留给具体实现决定。
幂等性的工程价值:在网络不稳定的分布式环境中,幂等的请求可以安全重试。GET/PUT/DELETE 可以放心被中间件或客户端重试,POST 则需要额外的去重机制(如幂等键)。
2.3 HTTP 状态码:让协议层传递业务语义
2xx 成功
200 OK --- 通用成功
201 Created --- 创建成功(通常搭配 Location 头返回新资源 URL)
204 No Content --- 成功但无响应体(常用于 DELETE)
4xx 客户端错误
400 Bad Request --- 请求参数校验失败
401 Unauthorized --- 未认证(应为 Unauthenticated,历史命名不佳)
403 Forbidden --- 已认证但无权限
404 Not Found --- 资源不存在
409 Conflict --- 资源状态冲突(如重复创建)
422 Unprocessable Entity --- 语义错误(参数格式正确但业务规则不满足)
5xx 服务端错误
500 Internal Server Error --- 未处理的服务端异常
502 Bad Gateway --- 上游服务不可用
503 Service Unavailable --- 服务暂时过载
反模式:所有接口返回 200,错误信息放在 body 的 code 字段里。这种做法绕开了 HTTP 语义层,导致中间件(网关、CDN、监控)无法正确识别错误,也让 HTTP 客户端的错误处理逻辑失效。
三、Spring Boot 中的 RESTful 实践
作为 Java 开发者,Spring MVC 的注解体系天然对齐 RESTful 风格:
java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserVO> getUser(@PathVariable Long id) {
UserVO user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<UserVO> createUser(@Valid @RequestBody CreateUserRequest req) {
UserVO created = userService.create(req);
URI location = URI.create("/api/v1/users/" + created.getId());
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserVO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest req) {
UserVO updated = userService.update(id, req);
return ResponseEntity.ok(updated);
}
@PatchMapping("/{id}")
public ResponseEntity<UserVO> patchUser(
@PathVariable Long id,
@RequestBody Map<String, Object> fields) {
UserVO patched = userService.patch(id, fields);
return ResponseEntity.ok(patched);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
几个关键实践点:
@RestController=@Controller+@ResponseBody,返回值直接序列化为 JSON。ResponseEntity可以精确控制状态码和响应头。创建资源时返回 201 + Location 头是 REST 的推荐实践。@Valid触发 Bean Validation,校验失败由全局异常处理器统一返回 400。- API 版本号放在路径中(
/api/v1/)是最常见的版本管理方式。
四、RESTful 的局限性与务实取舍
理论优美,但真实业务中会遇到 REST 资源模型不好表达的场景。
4.1 复杂操作:动作型接口
「审批一个订单」「发送一条消息」这类操作很难映射为资源的 CRUD。
# 方案一:将动作建模为子资源(更 RESTful)
POST /orders/99/approval --- 为订单创建一个「审批」资源
# 方案二:用 PATCH 修改状态字段
PATCH /orders/99
Body: {"status": "approved"}
# 方案三:务实妥协,使用动词(业界常见)
POST /orders/99/approve
方案一最符合 REST 精神,方案三可读性最好。工程中选择哪种取决于团队一致性和文档清晰度。
4.2 批量操作
REST 的资源模型以单个资源为中心,批量操作没有标准答案:
# 批量删除 --- 务实方案
POST /users/batch-delete
Body: {"ids": [1, 2, 3]}
# 批量创建
POST /users/batch
Body: [{"name": "Alice"}, {"name": "Bob"}]
虽然 POST /users/batch-delete 违反了「URL 是名词」的原则,但在性能和可用性面前,这是被广泛接受的工程妥协。
4.3 复杂查询
当搜索条件复杂到 URL query string 难以承载时(URL 长度有限制,通常浏览器限制约 2000 字符):
# 务实方案:POST 搜索请求体
POST /users/search
Body: {
"age_range": [18, 30],
"city": "北京",
"sort": [{"field": "create_time", "order": "desc"}]
}
严格来说这违反了 GET 用于查询的语义,但当搜索条件本身足够复杂时,这是工程中被广泛接受的做法。ElasticSearch 的 _search API 就是这种模式。
4.4 实用原则
RESTful 是指导原则,不是教条。真实项目中做到以下三点,就已经优于大多数系统:
- 资源导向的 URL 设计 --- 清晰的名词路径
- 正确使用 HTTP 方法语义 --- GET 不修改数据、POST 用于创建
- 规范使用状态码 --- 让 HTTP 协议层传递错误语义
对于不好用资源模型表达的边界情况,工程判断优先于原则洁癖。关键是团队内保持一致,并在 API 文档中清晰说明。
五、RESTful vs RPC:选择取决于场景
| 维度 | RESTful | RPC(gRPC / Dubbo) |
|---|---|---|
| 设计视角 | 以资源为中心 | 以操作/方法为中心 |
| 接口定义 | URL + HTTP 方法 | 方法签名(IDL 或接口类) |
| 传输协议 | HTTP/1.1 或 HTTP/2 | gRPC 绑定 HTTP/2;Dubbo 默认 TCP |
| 序列化 | JSON(人类可读) | Protobuf / Hessian(二进制,高效) |
| 典型场景 | 对外 API、前后端通信、开放平台 | 内部微服务间高频调用 |
| 优势 | 通用性强,浏览器直接调试 | 性能高,强类型约束,代码生成 |
| 代表框架 | Spring MVC、Express、FastAPI | gRPC、Dubbo、Thrift |
选择策略:面向外部消费者(前端、第三方)的接口用 RESTful,可读性和通用性是第一优先级;内部微服务间的高频调用用 gRPC 或 Dubbo,性能和强类型校验是核心诉求。两者不矛盾,可以在同一个系统中共存。
六、总结
REST 不是一个简单的 URL 命名规范,而是一套完整的分布式系统架构约束。理解它的理论基础(Fielding 论文中的六大约束)有助于做出更好的设计决策,但在工程中不必追求教科书式的完美实现。
记住三个核心判断标准:接口是否以资源为中心?HTTP 方法的语义是否被正确使用?状态码是否传递了正确的信息?如果这三点都做到了,你的 API 已经是高质量的 RESTful 设计。
参考资料
- Roy Fielding, "Architectural Styles and the Design of Network-Based Software Architectures" , 2000 --- Chapter 5: REST
- RFC 9110: HTTP Semantics(取代了 RFC 7231)--- rfc-editor.org/rfc/rfc9110
- RFC 5789: PATCH Method for HTTP --- rfc-editor.org/rfc/rfc5789
- Martin Fowler, "Richardson Maturity Model" --- martinfowler.com