虽然 RESTful API 已经成为业界对于 API 的共识,但是不得不说,但是不得不说它具有很多局限性。
RESTful API 和很多的技术流传的原因类似:始于一种非常理想化的愿景,但是在落地时却需要做出权衡和取舍。
它的流行开始于 Roy Fielding 的演讲,Roy Fielding 也是 HTTP 协议标准作者之一。
我猜测 Roy Fielding 的想法是,HTTP 协议已经是一个完善的应用层协议了,对于应用开发来说,只需要将所有的网络数据抽象为 URI 资源,然后配合可选的 Method 以及状态码就够用了。
但是,在我们实际落地的情况中,HTTP 的表达力完全不够。其限制主要有这几个:
-
不是所有的信息都能抽象为静态资源,总会有一些动态行为存在。例如 Github 的 Star 操作,即使抽象为 star-records 也违反人的表达认知。
-
HTTP Method 无法在应用层随意拓展,很多行为难以被表达为合适的 Method
-
HTTP Status 无法在应用层拓展,很多业务状态无法使用 HTTP Status 表达
基于这些原因,人们为了遵守 RESFul 不得不绞尽脑汁,而如果使用 RPC 风格的 API 只需要使用合适的动词作为 URI 以及合适的 Payload 报文格式即可。
但是,总体来说,在一定程度上,使用 RESTful 风格,可以做到自解释性,减少了文档的依赖。而它的缺点,也可以通过一些团队规约避免。
本文基于技术研讨会讨论的内容,整理了一些使用 RESTful 的一些建议,包括如何通过团队契约在一定程度上弥补表达力不够的问题。
01 使用版本号解决版本不兼容问题
版本化 API 会有很多好处,而版本化 API 有很多种风格,包括:
-
使用 URL 前缀
-
使用 URL 后缀
-
使用 Header 传参
-
使用 Query 参数
推荐使用 URL 前缀实现,这样对后面 API 定义无侵入性且能通过 URL 表达版本。
实例:GET /v1/products/{id}
02 资源路径参考领域模型
在一般情况下,可以以模块、聚合根作为路径前缀,让路径排列更有规律。
参考类似模式: /[模块]/[版本号]/[聚合根]/{id}/[实体]/{id}/[属性/动作]
-
在微服务项目中,模块部分可以是服务名
-
资源参考领域模型设计用词,因此需要使用名词复数
-
为了弥补 RESTful 不足,允许在路径结尾使用属性或者动作满足特定业务需求
-
一般 URL 路径和文件名风格类似,使用中横线(Dash)
-
和领域模型的层次类似,URL 层次不要太深,通过拆分小聚合实现短 URL
03 谨慎选择 HTTP Method
HTTP 协议提供了很多的 Method,但是处于团队理解成本的原因建议只使用下面几个 HTTP Method:
-
GET 查询
-
POST 新增
-
PUT 修改/更新
-
DELETE 删除
避免使用 PATCH,原因是无法表达业务含义,往往产生破坏性变更。例如,将保存的出库单提交,业务上需要修改单据的状态。应该避免使用 Patch 部分更新单据,建议使用 PUT /xxx/submit
的形式设计,让团队更容易理解,保证业务一致性。
04 实体的单复数应具有实际意义
通过 URL 应该能识别出返回的结构类型是否是一个列表或者分页的包装对象。
例如,通过 GET /v1/orders/{id}
能猜测出返回结果是一个资源对象。
而通过 GET /v1/orders
能猜测出其结果是一个列表。
在某些项目中,复数词汇的 URL 默认返回分页对象,而一些项目会给分页 URL 添加一个 page 后缀,例如 GET /v1/orders/page
。
05 合理实现幂等性
幂等含义:相同输入,应得到相同返回。
-
GET 天然具有幂等性
-
PUT DELETE Method 应设计为具有幂等性
-
POST 根据实际情况进行幂等性设计,例如在消息体中要求调用方传入事务 ID 实现幂等
06 提前设计查询语言或关键字
如果需要实现复杂的查询,可以提前设计一套基于 Query 参数的查询规则,尽可能实现通用的数据库字段查询。
需要考虑:
-
分页
-
排序
-
关键字搜索
-
与过滤条件
-
并过滤条件
-
开闭区间查询
这类实现一般需要结合具体数据库查询框架,例如 JPA、QueryDsl、Mybatis Plus 的 Wrapper 查询能力。
例如下面一个基于 Mybatis Plus 的通用 QueryWrapper:
// 查询条件的父类 public class QueryCondition { private String name; private Integer age; } // 为通用字段生成查询 Wrapper public class MyMapper<T> extends BaseMapper<T> { public List<T> queryByCondition(QueryCondition condition) { QueryWrapper<T> queryWrapper = new QueryWrapper<>(); LambdaQueryWrapper<T> lambdaQueryWrapper = queryWrapper.lambda(); // 根据条件动态构建查询 if (condition.getName() != null) { lambdaQueryWrapper.like(T::getName, condition.getName()); } if (condition.getAge() != null) { lambdaQueryWrapper.eq(T::getAge, condition.getAge()); } // 执行查询 return selectList(queryWrapper); } }
07 状态码的选用
状态码的选用只用于前端处理一些通用的错误,而具体的业务规则错误统一使用 409(业务规则冲突)来返回,并返回约定的错误码、报错信息。
参考如下:
-
200 请求成功
-
201 创建成功
-
400 数据校验失败
-
401 用户未认证
-
403 权限检查失败
-
404 资源找不到
-
405 不支持的 Method
-
409 业务规则冲突
-
415 不支持的数据请求格式
-
500 服务器内部错误
-
503 BFF 转发后端错误
08 批量处理接口
批量处理也是 RESTful 风格 API 不好处理的地方。有一些常见的做法:
-
由于 URL 使用复数已经代表资源,因此需要增加
/batch
后缀作为 URL 区分 -
使用类似版本号的处理方式在 URL 前增加
/batch
前缀 -
URL 上可以不区分,在传入参数的格式上区分。例如,通过传入一个列表表达批量创建用户。
方案 2 看似更符合 RESTful API 风格,实际上非常容易让人困惑,因为无法通过 URL 语义区分批量接口;方案 2 的问题是,批量接口并不常见,使用前缀会影响很多接口的语义。
在一些文章中,还会区分 bulk 和 batch 的区别,认为 bulk 是对多个资源处理同样的操作,而 batch 是针对多个资源处理不同的操作。
当然在实践中我们不用如此区分,但在微服务环境下一些基础服务往往需要提供批量接口,避免循环调用带来的性能问题。
一些分页接口的例子:
-
批量创建订单
POST /v1/orders/batch
-
批量提交订单
POST /v1/orders/batch-submit
09 动词名词化技巧和场景
有些情况下动词 API 在充分建模的情况下也可以名词化。
比如登录的接口,我们在建模充分后识别到登录行为本质上创建了一个会话或者凭证,于是可以将:
POST /v1/users/login
改写为:
POST /v1/authorizes
另外一个例子,在金融领域需要对资产进行估值,看似应该使用:
POST /v1/assets/calculate
其实应该改写为:
POST /v1/capital-rating-transactions
每次评估后都会产生一次事务 ID。
10 区分成功和错误的返回结果
当 API 报错时,有些做法是使用一个容器包装错误信息和成功的消息体。
例如:
{
"code": "xxx"
"data": {}
"error": {}
}
这种做法相当于再次封装了 payload 会显得有些冗余,主流的 API 设计一般是:当返回 HTTP 代码为 2xx 时,返回成功的对象,当使用其他代码时返回错误的对象。
错误的对象一般包含:
-
业务意义的错误码
-
错误消息
-
错误的详细对象,用于某些特殊报错需要返回更多信息
11 创建和更新只返回必要信息
在讨论中,有一个话题是创建和更新操作是否需要返回处理后的对象?
返回的好处是,在编写 API 测试时方便取数,另外也可以复用详情的返回模型。
不返回的好处时,可以简化开发工作,仅仅返回必要的字段和数据,前端需要获取详细数据,可以调用查看相关接口。
同时,不返回全量信息也可以在部分场景提高性能,避免组装、传输过多数据。
12 可以参考的 API 规范和示例
除了前面的建议外,我们也经常参考一些真实的 API 设计规范,下面是一些常见可参考模仿的 API 设计示例:
-
https://docs.github.com/en/rest?apiVersion=2022-11-28
-
https://jsonapi.org/
-
https://wiki.onap.org/display/DW/RESTful+API+Design+Specification
参考资料
-
https://en.wikipedia.org/wiki/Representational_state_transfer
-
https://www.codementor.io/blog/batch-endpoints-6olbjay1hd
-
https://medium.com/paypal-tech/batch-an-api-to-bundle-multiple-paypal-rest-operations-6af6006e002
文 | 少个分号 (转载请注明出处)
关注公众号:DDD和微服务
本文仅供学习!所有权归属原作者。侵删!文章来源: DDD和微服务 -少个分号 :http://mp.weixin.qq.com/s?__biz=MzA4Mzc2MzcyMQ==&mid=2247484590&idx=1&sn=83390073ad0c26633e542ee9b60dafdd&chksm=9ff033c8a887baded8151545176906160bed25040e9a667739fe63af342999309199c0a9e58a&scene=21#wechat_redirect
文章评论