4. 服务接口设计基础:RESTful API设计原则、资源命名规范
好,咱们今天聊聊接口设计。说实话,我在SOA项目里踩过最大的坑,就是接口设计没想清楚。你想想看,服务拆得再漂亮,接口一塌糊涂,那整个系统就像个歪歪扭扭的积木塔,碰一下就倒。
RESTful API现在基本是SOA服务接口的事实标准。为什么?因为它简单、直观,而且跟HTTP协议结合得天衣无缝。我个人习惯,只要不是特别特殊的场景,优先选REST。
4.1 RESTful API的六大设计原则
REST不是个标准,它是一套设计风格。Roy Fielding博士在2000年提出的时候,估计也没想到今天会这么火。我总结下来,核心就六条:
- 客户端-服务器分离:界面和存储分开,各自独立演进。说白了,前端不用管数据怎么存的,后端不用管界面长啥样。
- 无状态:每个请求都包含所有必要信息。服务器不存客户端上下文。嗯,这里要注意,不是说应用不能有状态,而是说服务端不维护会话状态。
- 可缓存:响应要明确标记是否可缓存。我在项目中遇到过,一个接口没设缓存头,结果每次请求都查数据库,QPS一上来直接打挂了。
- 统一接口:这是REST的核心。所有资源通过统一的接口操作,比如HTTP的GET/POST/PUT/DELETE。
- 分层系统:客户端不知道它直接连的是最终服务器还是中间代理。这给负载均衡、缓存代理留了空间。
- 按需代码(可选):服务器可以给客户端返回可执行代码,比如JavaScript。这个在SOA里用得少,我基本没碰过。
核心要点:RESTful API的核心是「资源」和「统一接口」。资源就是你要操作的东西,接口就是操作方式。这两者搞清楚了,设计就成功了一半。
4.2 资源命名规范——这事儿真不能随便
资源命名看起来简单,但我在代码评审里见过太多奇葩命名了。比如 /getUserInfo、/deleteOrder、/api/v1/queryAllProducts……这些都不符合REST风格。
为什么?因为REST强调用HTTP动词表达操作,而不是把动词塞进URL里。你想想看,GET /users/123 多清晰,一看就知道是获取用户123的信息。
我总结了一套命名规范,这些年一直在用,基本没出过问题:
| 场景 | 推荐写法 | 不推荐写法 |
|---|---|---|
| 获取用户列表 | GET /users |
GET /getUsers |
| 获取单个用户 | GET /users/{id} |
GET /user?id=123 |
| 创建用户 | POST /users |
POST /createUser |
| 更新用户 | PUT /users/{id} |
POST /updateUser |
| 删除用户 | DELETE /users/{id} |
GET /deleteUser?id=123 |
| 获取用户的订单 | GET /users/{id}/orders |
GET /getUserOrders?userId=123 |
小技巧:资源名用复数名词。比如 /users 而不是 /user,/orders 而不是 /order。这样统一,也符合直觉。
4.3 命名规范的具体细则
光说原则不够,咱们来点具体的。这些是我在实际项目中一条条踩出来的:
- 全小写:URL里不要出现大写字母。比如
/userOrders改成/user-orders。 - 用连字符代替下划线:
/user_orders改成/user-orders。为什么?因为连字符在URL里更友好,而且搜索引擎也推荐。 - 避免文件扩展名:不要写
/users.json或/users.xml。用Accept头来控制返回格式。 - 层级关系用斜杠:
/users/123/orders/456表示用户123的订单456。别超过三层,太深了不好维护。 - 查询参数用于过滤和排序:
GET /users?status=active&sort=created_at。别把查询参数当资源路径用。
避坑指南:我曾经在一个项目里看到 /api/v1/getAllActiveUsersWithOrders 这种URL。这哪是REST啊,这分明是RPC披着REST的皮。后来重构的时候,我们花了整整两周才把接口理清楚。所以一开始就定好规范,后面省大事。
4.4 实际案例:设计一个订单服务接口
光说不练假把式。咱们拿一个订单服务来举例。假设我们要设计订单相关的API:
// 订单资源
GET /orders // 获取订单列表
POST /orders // 创建新订单
GET /orders/{orderId} // 获取单个订单详情
PUT /orders/{orderId} // 更新订单(全量更新)
PATCH /orders/{orderId} // 部分更新订单(比如改状态)
DELETE /orders/{orderId} // 删除订单
// 订单项资源(子资源)
GET /orders/{orderId}/items // 获取订单的所有商品项
POST /orders/{orderId}/items // 往订单里添加商品
DELETE /orders/{orderId}/items/{itemId} // 删除某个商品项
// 特殊操作——用动词做子资源
POST /orders/{orderId}/cancel // 取消订单
POST /orders/{orderId}/pay // 支付订单
你看,最后两个用了动词。有人会问:「不是说REST里不能用动词吗?」嗯,这里要灵活。对于「取消」这种业务操作,它本质上是一个动作,不是对资源的CRUD。用 POST /orders/{id}/cancel 比用 PATCH /orders/{id} 改状态更清晰。我个人觉得,原则是死的,业务是活的。
4.5 版本管理——别让接口变来变去
接口版本管理是个大话题。我见过两种主流做法:
- URL路径版本:
/api/v1/orders、/api/v2/orders。简单粗暴,容易理解。 - 请求头版本:用
Accept: application/vnd.myapp.v1+json。更优雅,但调试起来麻烦。
我个人倾向用URL路径版本。为什么?因为直观。你在浏览器里直接就能看到版本号,排查问题也方便。我在项目中用过请求头版本,结果每次联调都要跟前端解释半天「你那个Accept头写对了没」……后来还是改回URL版本了。
经验之谈:版本号不要轻易升级。小改动加字段就行,大改动才升版本。我见过一个项目,半年升到v8,结果客户端兼容性搞得一团糟。记住,向后兼容是接口设计的底线。
4.6 响应格式——让调用方省心
接口设计不只是URL的事,响应格式同样重要。我习惯统一用JSON,结构如下:
// 成功响应
{
"code": 0,
"message": "success",
"data": {
"id": "123",
"name": "张三",
"orders": [...]
}
}
// 错误响应
{
"code": 40001,
"message": "用户不存在",
"details": "userId: 999 在数据库中未找到"
}
code为0表示成功,非0表示错误。这样调用方只需要判断code,不用解析HTTP状态码。当然,HTTP状态码也要用对——200表示成功,400表示参数错误,404表示资源不存在,500表示服务器内部错误。
小建议:错误信息要写清楚,别只返回一个「error」。我见过一个接口,报错就返回 {"msg": "fail"},调用方根本不知道哪里错了。后来我们加了错误码和详细描述,联调效率提升了一倍。
好了,关于RESTful API设计原则和资源命名规范,今天就聊到这儿。说白了,接口设计就是「让别人用得舒服」。你多站在调用方的角度想想,自然就能设计出好接口。下一节咱们聊聊接口安全,那也是个容易踩坑的地方。