重新理解 RESTful:从理论约束到工程实践

重新理解 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 是指导原则,不是教条。真实项目中做到以下三点,就已经优于大多数系统:

  1. 资源导向的 URL 设计 --- 清晰的名词路径
  2. 正确使用 HTTP 方法语义 --- GET 不修改数据、POST 用于创建
  3. 规范使用状态码 --- 让 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 设计。


参考资料

相关推荐
奔跑的Ma~9 小时前
企业级 Codex 部署与团队协作方案
后端·python·ai编程·codex·ai学习
明月_清风9 小时前
实战选型决策树——一张图搞定"我这个场景该用什么序列化方案"
后端
武子康9 小时前
Java-09 深入浅出 MyBatis 注解开发详解:从 CRUD 到复杂关系映射
java·后端·spring
神奇小汤圆9 小时前
Java 泛型解析太痛苦?你可能需要一枚「蛋」
后端
用户2986985301410 小时前
Java 进阶:在 Word 文档中动态增删页面
java·后端
洛阳泰山10 小时前
MaxKB4j 近三月开发进展速览:从 RAG 引擎到全能 AI 工作流平台
人工智能·后端
我是一只码蚁10 小时前
记一次苍穹外卖项目 Maven 编译报错的排查与解决全过程
java·经验分享·笔记·后端·架构·maven
Mahir0810 小时前
MyBatis 分页与插件深度解密:从插件机制到三大分页方案原理全解
java·后端·mybatis·mybatis-plus·大厂面试题
折哥的程序人生 · 物流技术专研11 小时前
《Java 100 天进阶之路》第40篇:浮点数转成十进制问题
java·开发语言·后端·面试·求职招聘