4、微服务通信之RESTful API:REST设计原则、资源建模、版本管理策略、HATEOAS与超媒体驱动

4.1 REST设计原则:别把RPC当REST用

说到微服务间的通信方式,RESTful API 绝对是绕不开的话题。很多人觉得 REST 就是「用 HTTP 调接口」,其实没那么简单。

我个人习惯把 REST 看作一种架构风格,而不是协议。它有几个核心约束,我一个个讲。

  • 客户端-服务器分离:UI 和数据处理分开,各自独立演进。我在项目中见过把业务逻辑塞到前端代码里的,后来改起来真是欲哭无泪。
  • 无状态:每个请求都包含所有必要信息,服务器不存客户端上下文。嗯,这里要注意——Session 这种东西在 REST 里是禁忌。
  • 可缓存:响应要显式声明是否可缓存。你想想看,如果每个请求都穿透到数据库,再大的集群也扛不住。
  • 统一接口:这是 REST 的灵魂。资源通过 URI 标识,操作通过标准 HTTP 方法表达。
  • 分层系统:客户端不知道它直接连的是最终服务器还是中间代理。这种透明性让架构扩展变得非常灵活。
  • 按需代码(可选):服务器可以返回可执行脚本。说实话,这个在实际项目中用得不多。

核心要点:REST 不是 URL 长得好看就叫 REST。我见过太多人把 POST 当 RPC 用,比如 POST /getUserPOST /deleteOrder。这其实是披着 REST 外衣的 RPC。

4.2 资源建模:把业务抽象成资源

资源建模是 REST 设计的第一步,也是最容易出错的地方。说白了,你要回答一个问题:我的系统里有哪些「东西」?

我一般遵循几个原则:

  • 名词优先:资源是名词,不是动词。比如 /orders 而不是 /getOrders
  • 层次化组织:用路径表达资源间的关系。比如 /users/{userId}/orders 表示某个用户的订单。
  • 集合与个体:集合用复数,个体用 ID。比如 GET /users 获取用户列表,GET /users/123 获取单个用户。

举个例子,一个电商系统的资源建模:

# 用户资源
GET    /users                    # 获取用户列表
POST   /users                    # 创建用户
GET    /users/{id}               # 获取单个用户
PUT    /users/{id}               # 全量更新用户
PATCH  /users/{id}               # 部分更新用户
DELETE /users/{id}               # 删除用户

# 订单资源(属于某个用户)
GET    /users/{userId}/orders    # 获取用户的订单列表
POST   /users/{userId}/orders    # 为用户创建订单
GET    /users/{userId}/orders/{orderId}  # 获取某个订单详情

# 商品资源
GET    /products                 # 商品列表
GET    /products/{id}            # 商品详情

避坑指南:我曾经把一个「激活用户」操作设计成 POST /users/{id}/activate。后来发现这其实是个状态变更,用 PATCH /users/{id} 带上 { "status": "active" } 更符合 REST 风格。记住:动作应该由 HTTP 方法表达,而不是 URL。

4.3 版本管理策略:别让 API 版本成为噩梦

API 版本管理,说白了就是「如何在不破坏现有客户端的前提下,让 API 继续演进」。我见过三种主流策略:

策略 实现方式 优点 缺点
URI 路径版本 /v1/users/v2/users 简单直观,容易路由 版本扩散,URL 不干净
请求头版本 Accept: application/vnd.myapp.v1+json URL 干净,符合 REST 理念 调试不方便,客户端实现复杂
查询参数版本 /users?version=1 实现简单 容易混淆,缓存困难

我个人习惯用 URI 路径版本。为什么?因为简单。你想想看,运维同学排查问题的时候,看一眼 URL 就知道是哪个版本的接口,不用去翻请求头。我在项目中遇到过用请求头版本管理的,后来线上出问题,排查了半天才发现是版本号传错了。

注意:不管用哪种策略,不要轻易废弃旧版本。我曾经在一个项目中,v1 接口用了两年,突然说要下线。结果发现好几个老客户还在用,最后不得不延长维护期。建议至少保留两个大版本,给客户端足够的迁移时间。

4.4 HATEOAS 与超媒体驱动:REST 的终极形态

HATEOAS(Hypermedia As The Engine Of Application State),这个名字很长,但核心思想很简单:服务器告诉客户端接下来能做什么

什么意思呢?传统的 API 返回的是数据,客户端得自己知道下一步该调哪个接口。而 HATEOAS 会在响应里带上链接,告诉客户端「你可以从这里去那里」。

看个例子:

{
  "orderId": 12345,
  "status": "pending",
  "total": 299.00,
  "_links": {
    "self": { "href": "/orders/12345" },
    "pay": { "href": "/orders/12345/payments", "method": "POST" },
    "cancel": { "href": "/orders/12345", "method": "DELETE" },
    "customer": { "href": "/users/678", "method": "GET" }
  }
}

看到没?响应里多了个 _links 字段。客户端拿到这个订单后,就知道:

  • 可以支付(POST 到 pay 链接)
  • 可以取消(DELETE 到当前资源)
  • 可以查看客户信息(GET 到 customer 链接)

这样做的好处是什么?客户端不再硬编码 URL。服务器可以随时调整资源结构,只要链接关系不变,客户端就不用改代码。

我的经验:HATEOAS 在理论上很美,但实际落地时阻力不小。我曾经在一个大型电商项目中尝试全面推行 HATEOAS,结果前端团队抱怨「链接太多,不知道用哪个」。后来我们折中了一下:核心流程用 HATEOAS,边缘场景用传统方式。嗯,有时候完美主义要适当让步。

4.5 总结与避坑清单

说了这么多,我总结几个关键点:

  • REST 是风格,不是协议。别把 HTTP 当 RPC 用,该用 GET 就别用 POST。
  • 资源建模用名词。动作交给 HTTP 方法,URL 只表达资源关系。
  • 版本管理选一种,坚持用。我个人推荐 URI 路径版本,简单就是美。
  • HATEOAS 是好东西,但别强求。根据团队能力和业务复杂度决定是否采用。

最后一句:我曾经接手过一个「RESTful API」项目,结果发现里面全是 POST /api/doSomething 这种接口。嗯,从那以后我养成了一个习惯:设计 API 之前,先问自己三个问题——这个资源是什么?它有哪些状态?客户端下一步能做什么?想清楚了再动手。