这篇文章是强行插播过来的,本来是在做 GraphQL 的调研,但是发现其实 REST 这边的很多进展根本就没有说啊,那后面比较的时候很多东西就有点说不清楚了呢。所以就介绍下目前 REST API 开发过程中引入的一些不错的实践,重点是如何 OpenAPI 以及其工具链完善开发流程,降低沟通成本。
就是标题了呢,绝大部分是围绕 openapi 以及其工具链介绍具体解决了什么问题。
众所周知,REST API 可以按照行为分为「读」和「写」两种:
GET
方法POST
PUT
DELETE
PATCH
自我一篇很古老的文章里就提到了,GET
请求获取的是「读模型」(区分于文中所陈述的 DDD 的「写」模型),应该按照使用方需求合理展示相应数据。不过当时对「读模型」讲述的不够深入,实际上 GET
所返回的数据不是孤立的,而是相互关联的。例如有这么一个场景,主要包含两个资源 user
和 blog
,blog
会有 user
作为其 author
。有如下两个 API:
GET /users
返回用户列表GET /blogs
返回博客列表其中 blog
下会包含 author
的信息:
{ // blog
"id": "xxx",
"title": "xxx",
"summary": "xxx",
"tags": ["spring", "rest"],
"author": { // author
"name": "xxx",
"avatar": "http://images.jpg",
"description": "bla bla bla"
}
}
然后可能会有一个用户的列表 GET /users
,里面包含了用户的信息:
{
"name": "xxx",
"avatar": "http://images.jpg",
"description": "bla bla bla"
}
blog 下的 author 和 users 下的每个 user 的结构如果没什么特殊原因应该是一致的,否则前端同学肯定一脸懵逼。这就是所谓的「结构标准化」了。就是 REST GET
的返回结果应该按照 API 服务所提供的资源之间的关系有统一的结构。 这部分和 GraphQL 的感觉有点异曲同工了。
而「写模型」数据通常是让前端去提交某种结果,其所需要的结构是在 requestBody
中。如果硬要提交的结构和返回的结构完全一致的话会有三个问题:
blog
的 author
字段通常都是自动生成的,怎么可能是提交上去的,这就不符合实际业务逻辑比如在 openbayes 创建 job 的 API 里面,需要用户去提交什么样子的资源绑定到什么路径上,如果是规范的提交方式应该是这个样子:
POST /jobs
{
"data_bindings": [
{
"path": "/input0",
"data": {
"id": "abc",
"name": "mnist",
"type": "DATASET",
"owner": {
"id": "userid",
"name": "username",
...
}
...
}
}
]
}
其中大部分数据毫无意义,因为绝大部分信息都是在提供了 userid
+ data-type
+ data-id
之后后端直接在数据库中就查的出来的,所以合理的提交方式应该是以下的样子:
POST /jobs
{
"data_bindings": [
{
"path": "/input0",
"data": {
"userid": "userid",
"type": "DATASET",
"id": "xxx"
}
}
]
}
再举个容忍度的例子,管理员创建一个新的商品:
POST /plans
{
"name": "xxx",
"description": "xxxx",
"price": {
"amount": 1.99,
"currency": "CNY"
}
}
其中这个 price
部分有点点小冗余了,如果可以同时支持以下方式可能会更友好:
POST /plans
{
"name": "xxx",
"description": "xxxx",
"price": "CNY 1.99"
}
这部分我觉得就有点额外发挥的意思了,甚至会画蛇添足。将 API 调用 SDK 化可以极大程度上减少这种额外工作的必要。
API 越写越多,接口越来越复杂,前端调用的时候总要有个文档说说到底每个 API 都返回了啥吧,那么就要用 OpenAPI 了。
OpenAPI 是一套标准,定义了如何用 json schema (的变种)去定义一套 REST API 的接口的。然后围绕这个标准产生了一系列的工具,这一系列的工具的组合一定程度上解决了我们 REST 开发中的诸多痛点。
openapi 就是个 yaml 而且其官方提供的 swagger editor 在这个 yaml 比较大的时候会很卡。干脆就用 intellij 去编辑了,但是我总是知道自己的语法写没写对吧。这个时候我就直接用 redoc 这个东西去渲染结果了,如果展示的对就是正确的了。
然后其实随着 api 的规模的增大,单个 yaml 已经承受不了了。最后分化出了多个 yaml 文档,并对 redoc 提供的 html 做了些许魔改形成了下面的这个样子:
可以看到按照边界分成了 6 个 yaml 然后 openapi 支持每个之间做了相互的数据引用。
这个文档在一定程度上解决了前后端沟通的问题。
openapi 文档会又撰写 api 的人同步的更新,为了保证 openapi 上线前不要各种挂,就引入了 openapi-validator
在 ci 那边跑测试,如果校验没过就报错了呢。openapi-validator
相对比较灵活,有点像是 lint
(巧了,openapi-validator 的命令行工具就叫 openapi-lint
)可以自动的定制一些规则,要求文档的撰写者遵循。
文档写好了,单毕竟就是个 yaml 而已,如何保证实际的代码和 openapi 一致呢?这里就魔改出来一个还算凑合的方案。
首先 restassured
是我们之前就广泛使用的 api 测试框架,然后它其实又一个 JsonSchemaValidator
组件,支持用 jsonschema 去校验请求的内容。然后我有找到一个名为 openapi2schema
的东西,可以把 openapi yaml 转换为 json schema 格式。那么我就可以实现用 openapi 文档去测试实际的代码是不是符合规范了,这里就直接截取项目中的一小段代码:
public void should_get_a_workspace_with_one_dataset_binding() {
String schema = getJsonSchema("api.json", "'/users/{userId}/jobs/{jobId}'.get.responses.200");
// bla bla bla
given()
.header("Authorization", "Bearer " + token)
.contentType(ContentType.JSON)
.when()
.get("/users/{uid}/jobs/{jid}", jobData.getUserId(), jobId)
.prettyPeek()
.then()
.statusCode(200)
.body(JsonSchemaValidator.matchesJsonSchema(schema));
}
其中 api.json
就是利用 openapi2schema
将 openapi 转换为 jsonschema
的结果了。baeldung 有篇 文章 介绍了如何使用 rest assured 的 json schema validator。
最后,也是最重要的一环,为了让 openapi 有灵魂,一定是给前端把它的调用做封装,而不是让前端同学按照文档自己去写调用接口。这里用了 openapi-generator-cli
可以生成众多语言的 sdk,甚至支持 typescript 的 sdk。
这部分在 在 Spring Boot 中使用 HATEOAS 介绍过了。简单的说就是两部分:
吐槽下目前 REST 生态吧。
REST 没什么标准(甚至还会时不时为 REST 是什么而争论),唯一看起来像样的就是 OpenAPI 但是 OpenAPI 的问题也挺多的。
OpenAPI 不是什么大厂,没有很强的运营推广能力,很多东西的贡献也全凭兴趣,持久性和可靠性令人怀疑。比如目前 openbayes 的 API 已经相对比较多了,统计下 openapi 文档的行数已经相当恐怖了。以下几个文件还是自己强行拆分出来的,OpenAPI 自身对于多文件的支持并不是很好,目前的方案也不是很完美,可能需要结合 How to split a large OpenAPI Specification into multiple files 这篇文章的方案做更多的尝试了。
ls *.yml | xargs wc
8499 15129 216144 api.yml
1543 2752 40366 finance.yml
167 286 4375 notifications.yml
823 1432 20210 orgs.yml
1366 2401 35797 servings.yml
1550 2696 37413 users.yml
13948 24696 354305 total
redoc 并不支持完整的 openapi 语法,很多时候校验全凭效果,比如 allOf
oneOf
这些继承语法。正如上文所说,「如果展示的对就是正确的了」。类似的问题在 openapi-generator-cli
也会出现,具体的问题也是千奇百怪。最终结果导致我们只能使用一个 openapi 语法的子集,并且我觉得如果 openapi 没有能力让下面的工具链与自己的标准协同更新,openapi 也就慢慢成了一纸空文。以及 openapi2schema
就更没谱了,就是个完完全全的个人作品,已经一年多没有更新了,后面还能不能用都不知道。
总的来说,上面的工具链有点像是纸糊的,摇摇欲坠,这也是我寻求 GraphQL 破局的重要原因之一吧。