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 /getUser、POST /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 之前,先问自己三个问题——这个资源是什么?它有哪些状态?客户端下一步能做什么?想清楚了再动手。