2022-03-07 21:23:06 +08:00
|
|
|
|
[[_TOC_]]
|
|
|
|
|
|
2022-03-07 20:49:02 +08:00
|
|
|
|
# OpenAPI3 Framworks Compare
|
|
|
|
|
|
|
|
|
|
本文会横向对比几种支持OpenAPI文档生成的工具/框架。
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
OpenAPI Specification(下文简称OAS)定义了一个标准的、语言无关的 RESTful API 接口规范,它可以同时允许开发人员和操作系统查看并理解某个服务的功能,而无需访问源代码,文档或网络流量检查(既方便人类学习和阅读,也方便机器阅读)。正确定义 OAS 后,开发者可以使用最少的实现逻辑来理解远程服务并与之交互。
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
|
|
|
|
此外,文档生成工具可以使用 OpenAPI 规范来生成 API 文档,代码生成工具可以生成各种编程语言下的服务端和客户端代码,测试代码和其他用例。OpenAPI官方提供了各种语言的服务端和客户端的代码生成工具,比较著名的如[OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator),当然也有很多优秀的第三方开发者开发的工具。
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
> 参考阅读
|
2022-03-07 20:49:02 +08:00
|
|
|
|
>
|
|
|
|
|
> - [OpenAPI3 Specification官方文档(v3.0.3)](https://spec.openapis.org/oas/v3.0.3) Published 20 February 2020
|
|
|
|
|
> - [OpenAPI3 Specification官方文档(v3.1.0)](https://spec.openapis.org/oas/v3.1.0) Published 15 February 2021
|
|
|
|
|
> - [Awesome OpenAPI3](https://apis.guru/awesome-openapi3)
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
## 1. 项目背景
|
|
|
|
|
|
|
|
|
|
目前公司内部开发中的项目存在很多接口文档规范不统一、实现不一致的问题。据笔者的了解,起码目前存在以下的几种维护方式。
|
|
|
|
|
|
|
|
|
|
1. 使用[swag](https://github.com/swaggo/swag)工具在代码中加入注解,然后通过代码生成对应项目的文档
|
|
|
|
|
2. 一些使用grpc-gateway的grpc项目会使用其附带的[protoc-gen-openapiv2](https://github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2)工具对protobuf文件进行分析,生成对应的文档
|
|
|
|
|
3. 在yapi中手撸接口文档
|
|
|
|
|
4. 纯手动,自己维护一个自定义格式的HTML文档
|
|
|
|
|
|
|
|
|
|
其中前两种方式使用工具生成,后两种完全靠双手自行维护。除第二种方式外,其他三种方式都多少依赖维护者的自觉性,维护得好皆大欢喜,维护得不好就会引发一系列的连锁反应。
|
|
|
|
|
|
|
|
|
|
- 新人接手项目。目前维护的这个服务提供了哪些能力?接手新需求的时候我是要新增一个接口还是已经存在类似的接口,只需要扩展一下?如果没有完整的接口文档,只能硬啃现有代码。如果代码量比较巨大的话会非常痛苦,或者疑惑我应该信代码还是信文档?会不会是代码实现有失误的地方?总之满头问号
|
|
|
|
|
- 前后端对接、服务间对接。参数描述基本靠飞书,返回描述基本靠Chrome开发者工具。一个维护得足够久的接口没有人能够描述出来到底返回什么样的结构,需要传递哪些参数,怎么传,哪些必传,这些参数有哪些约束条件,或者一些已经废弃的参数可能早就无人使用,但是没人敢删除。到达某种状态后只能是懒得细扣代码,继续往上加。 总结:口头对接一时爽,事后复盘火葬场。
|
|
|
|
|
- 重构。项目到达一定的复杂程度、或者随着产品的不断迭代,接口可能会面临着重构。如果没有足够的测试代码且对接口行为没有完全十足的把握的话,重构无疑会存在着巨大的风险,一不小心在某个获取参数的地方导致行为不一致、或者返回的结构有差别的话就会导致整个下游服务的行为异常。
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
这也是OAS存在的理由。如果所有的服务都使用了某种方式能从代码中生产出对应可信赖的、接口描述与代码行为严格同步的OAS文档,上述的所有问题都会迎刃而解。且Schema First的开发流程也更符合Go语言[面向接口编程](https://en.wikipedia.org/wiki/Interface-based_programming)的理念。
|
|
|
|
|
|
|
|
|
|
此外也可以利用上社区内已经非常丰富的OAS生态相关工具,包括sdk生成、文档渲染工具等等。因为所有的文档都是标准的规范,甚至可以团队自己开发自己需要的工具,提升开发效率。一旦完善趋于完善,还可以做一些更有趣的事情,比如:
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
1. 公司内所有服务,可版本溯源的HTTP API接口文档查询工具。
|
|
|
|
|
2. 接通内管平台,对所有服务的接口管理。设定接口权限?联动网关层(如Istio)实现一个可视化的流量看板?
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
既然如上列举的各种优势,现在摆在眼前的问题只有一个,那就是实现代码和OAS文档同步带来的额外工作量和难度,如果同步文档的过程对开发者过于繁琐且耗时,那么必定在推广的时候困难重重。所以现在的情况就是寻找一个可以快速、简便地维护OAS文档的方法:
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
1. 找现成的开源方案
|
|
|
|
|
2. 自己造轮子
|
|
|
|
|
|
|
|
|
|
知己知彼,才能百战不殆。所以本项目的目的就是借鉴前人的设计和想法,总结好的设计方法,摒弃不好的设计方法。尽可能多的去了解别人是怎么做这件事情的,才能更好地去完成自己的。
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
以下内容会分别使用几种包含**从代码中同步文档**feature的框架(欢迎补充),用其实现一些简单的用例,然后分析这些框架的实现方式,和背后生成OAS的逻辑。随后会加上一些笔者的主观评价。
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
|
|
|
|
## 2. 参赛选手介绍
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
| 语言 | 框架 | Web功能完备 | OpenAPI版本 | Github Stars | 当前版本 | first release date |
|
|
|
|
|
|--------|--------------------------------------------------------------------------|-------------|-------------|--------------------------------------------------------------------------------|------------------------------------------------------------------------------------|--------------------|
|
|
|
|
|
| Go | [goa](https://github.com/goadesign/goa) | 是 | 3 |  |  | 2016-08-03 |
|
|
|
|
|
| Go | [swag](https://github.com/swaggo/swag) | 否 | 2 |  |  | 2017-11-30 |
|
|
|
|
|
| Go | [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) | 是 | 2 |  |  | 2016-07-11 |
|
|
|
|
|
| Go | [fizz](https://github.com/wI2L/fizz) | 是 | 3 |  |  | 2019-11-06 |
|
|
|
|
|
| Python | [Django REST framework](https://github.com/encode/django-rest-framework) | 是 | 3 |  |  | 2011-02-22 |
|
|
|
|
|
| Python | [FastAPI](https://github.com/tiangolo/fastapi) | 是 | 3 |  |  | 2018-12-16 |
|
|
|
|
|
| Rust | [Poem](https://github.com/poem-web/poem) | 是 | 3 |  |  | 2021-10-14 |
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
## 3. 详细对比
|
|
|
|
|
|
|
|
|
|
所有框架均会从以下几个方面进行讨论
|
|
|
|
|
|
|
|
|
|
1. 开发过程
|
2022-03-08 22:13:35 +08:00
|
|
|
|
2. OAS的实现方式: 探究这些工具是如何从代码生成对应sepc文件的,并会导航一些关键代码,方便读者进行深入探究。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
3. 优点:该框架的一些特色或者相比其他框架有优势的地方
|
|
|
|
|
4. 缺点:该框架的一些劣势,或者不使用的理由
|
|
|
|
|
|
2022-03-08 22:13:35 +08:00
|
|
|
|
> 所有评价均为个人意见,欢迎不同声音:D
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
|
|
|
|
### 1. Goa
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 1. 开发过程
|
|
|
|
|
|
|
|
|
|
1. 安装goa提供的代码生成工具
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
go install goa.design/goa/v3/cmd/goa@v3
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 创建一个design文件夹,用于描述API
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
mkdir -p goa_example/design
|
|
|
|
|
```
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
3. 使用goa提供的dsl描述API [desigin.go](go/goa_example/design/design.go)
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
4. 生成代码模板
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
goa gen goa_example/design
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
该步骤会生成gen文件夹中的所有文件,包括design文件中定义的所有接口信息、Model描述、验证方式、Protobuf描述(如果有)等所有相关信息的Go描述
|
|
|
|
|
|
|
|
|
|
5. (optional) 生成实现的一个example
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
goa example goa_example/design
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
该步骤会生成cmd文件夹,包括http server和 grpc server的启动代码
|
|
|
|
|
和项目根目录/{service_name}.go的文件,其中包含了各方法的默认实现(fmt.Println())
|
|
|
|
|
6. 完成代码实现
|
|
|
|
|
|
|
|
|
|
接下来只需要修改各方法中的具体实现为真实业务逻辑即可[service1.go](go/goa_example/service1.go)
|
|
|
|
|
|
|
|
|
|
#### 2. OAS的实现方式
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
OAS中的所有对象均使用框架自定的DSL实现,定义design.go文件的同时,gen程序可以获得文档中需要的所有描述信息。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
然后根据这些描述信息,代码生成所需要的各种类型和interface、包括OAS描述文件,让用户尽可能的少写重复代码,只去完成关键逻辑。
|
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 可同时生成HTTP和gRPC两种服务,对应的实现比较完整,基本可以描述所有类型的API,
|
2022-03-07 21:23:06 +08:00
|
|
|
|
2. 生成的代码质量比较高,提供的代码抽象比较好。基本上只需要完成业务逻辑的实现就可以。其中甚至包含了分别使用http和grpc请求远端的命令行请求工具
|
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 提供的DSL学习曲线很高,其基于查询调用栈的API设计,虽然调用非常灵活,但是会导致所有的函数均为灵活的不定长interface{}传参,除非熟练工,否则会因为没有任何代码提示导致使用体验非常差。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
2. Model的描述方式虽然定制能力比较高,但是定义过程相当繁琐,不够直观。
|
|
|
|
|
3. 使用时需要对OAS有足够的了解,否则不能很好地去合理设计API。
|
|
|
|
|
4. 社区内相对比较小众。(6年积累了star 4.5k)
|
2022-03-08 22:09:04 +08:00
|
|
|
|
5. 因为定制程度非常高,可能会导致灵活性较差,一些需要自定义http行为的特殊情况需要自己去Hack
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
### 2. swag
|
|
|
|
|
|
|
|
|
|
#### 1. 开发过程
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 安装swag命令行工具
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
go install github.com/swaggo/swag/cmd/swag@latest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
2. 创建一个main.go 先写一个简单的http服务
|
|
|
|
|
|
|
|
|
|
```golang
|
|
|
|
|
package main
|
|
|
|
|
import (
|
|
|
|
|
"time"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
func main() {
|
|
|
|
|
r := gin.New()
|
|
|
|
|
|
|
|
|
|
r.GET("/v2/object", GetObjects)
|
|
|
|
|
r.GET("/v2/object/:id", GetAnObject)
|
|
|
|
|
r.POST("/v2/object", CreateObject)
|
|
|
|
|
_ = r.Run(":8080")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Object struct {
|
|
|
|
|
ID int `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetAnObject(c *gin.Context) {
|
|
|
|
|
c.JSON(200, Object{
|
|
|
|
|
ID: 1,
|
|
|
|
|
Name: "hello",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func CreateObject(c *gin.Context) {
|
|
|
|
|
c.JSON(200, Object{
|
|
|
|
|
ID: 1,
|
|
|
|
|
Name: "hello",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GetObjects(c *gin.Context) {
|
|
|
|
|
c.JSON(200, []Object{
|
|
|
|
|
{ID: 1, Name: "hello"},
|
|
|
|
|
{ID: 2, Name: "hello"},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3. 按照swag文档中要求的格式添加注释. [main.go](./go/swag_example/main.go)
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
package main
|
|
|
|
|
// @Summary get an object
|
|
|
|
|
// @Description blabla...
|
|
|
|
|
// @Accept json
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param id path int true "Some ID"
|
|
|
|
|
// @Success 200 {object} Object "ok"
|
|
|
|
|
// @Failure 400 {object} APIError "We need ID!!"
|
|
|
|
|
// @Failure 404 {object} APIError "Can not find ID"
|
|
|
|
|
// @Router /testapi/get/{some_id} [get]
|
|
|
|
|
func GetAnObject(c *gin.Context) {
|
|
|
|
|
c.JSON(200, Object{
|
|
|
|
|
ID: 1,
|
|
|
|
|
Name: "hello",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
4. 执行swag init生成文档 [doc](go/swag_example/docs)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
swag init
|
|
|
|
|
|
|
|
|
|
tree docs
|
|
|
|
|
|
|
|
|
|
docs
|
|
|
|
|
├── docs.go
|
|
|
|
|
├── swagger.json
|
|
|
|
|
└── swagger.yaml
|
|
|
|
|
```
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 2. OAS的实现方式
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
命令行工具直接从源码的抽象语法树中获得到所有文件中的Comment信息,然后根据自定的规则把所有获得到的信息进行组合,然后生成文档。[核心代码](https://github.com/swaggo/swag/blob/6686f54ef730beeb719346330a4ca6bef3099cd6/parser.go#L1392),
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 与框架解耦,不影响原有的HTTP部分的逻辑,各种http框架均可轻松实现
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 也相当于是自定义的DSL,需要多查文档才能知道如何表达对应的对象信息,也需要对OpenAPI2.0规范有足够了解
|
|
|
|
|
2. 文档与代码逻辑完全脱离,文档与代码实现是否一致完全看开发人员的良心,经常碰到的情况是Comment年久失修
|
|
|
|
|
3. 写起来比较啰嗦,而且纯文本,体验差
|
|
|
|
|
4. 使用的是2.0规范,显得有点老了,仓库的Maintainer对支持3.0不太积极,相关[issue](https://github.com/swaggo/swag/issues/548)已经提了将近3年
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
### 3. grpc-gateway
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 1. 开发过程
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
基本的gRPC+gRPC-Gateway开发流程不再赘述,额外使用`protoc-gen-openapiv2`即可生成代码
|
|
|
|
|
工具会自动获取到proto文件中定义的message enum service等对象的注释,并添加至Spec文档中对应的description
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 2. OAS的实现方式
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 使用proto库提供的能力,可以获取到proto文件中的扩展信息。[核心代码](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/protoc-gen-openapiv2/internal/genopenapi/template.go#L2192)
|
|
|
|
|
2. 从扩展信息中获取对应的信息。[核心代码](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/internal/descriptor/registry.go#L625)
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 在Google的加持下,gRPC服务的用户众多。性能优异 + 跨语言支持 + 生态丰富
|
|
|
|
|
2. 对于OAS的基础功能(字段注释,结构体映射),增加文档几乎没有额外的开发成本。
|
|
|
|
|
3. **grpc + gateway模式**衍生出来的很多二次开发的框架均使用的是这套方案或类似方案。(kratos、go-zero等)
|
|
|
|
|
4. 相比上面的几种方案可以做到代码文档完全同步。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
1. 与gRPC强绑定,不适用与传统Web服务框架,
|
|
|
|
|
2. 支持的功能有限,某些基础功能不支持(为API增加Tag等?),protobuf的设计原则导致生成的(特指proto3)所有结构体中的字段均为可选字段,需要借助Envoy出品的[PGV protoc-gen-validate](https://github.com/envoyproxy/protoc-gen-validate)插件来增加一些必选参数、参数校验的功能。提供的能力只能说勉强够用。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
### 4. Fizz
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
该工具其实并不能算是一个独立的框架,更像是核心是Gin,然后进行二次包装的库。
|
|
|
|
|
虽然非常小众,GitHub star只有100多,但是笔者觉得这个库的设计非常巧妙,可以借鉴学习。
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 1. 开发过程
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
##### 1. 描述接口中的各种数据结构,tag中增加各种支持的json_schema信息
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// 描述数据模型
|
|
|
|
|
type Fruit struct {
|
|
|
|
|
Name string `json:"name" validate:"required" example:"banana"`
|
|
|
|
|
Origin string `json:"origin" validate:"required" description:"Country of origin of the fruit" enum:"ecuador,france,senegal,china,spain"`
|
|
|
|
|
Price float64 `json:"price" validate:"required" description:"Price in euros" example:"5.13"`
|
|
|
|
|
AddedAt time.Time `json:"-" binding:"-" description:"Date of addition of the fruit to the market"`
|
|
|
|
|
AddedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 描述请求参数
|
|
|
|
|
type FruitIdentityParams struct {
|
|
|
|
|
Name string `path:"name"`
|
|
|
|
|
APIKey string `header:"X-Api-Key" validate:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 描述请求参数
|
|
|
|
|
type ListFruitsParams struct {
|
|
|
|
|
Origin *string `query:"origin" description:"filter by fruit origin"`
|
|
|
|
|
PriceMin *float64 `query:"price_min" description:"filter by minimum inclusive price" validate:"omitempty,min=1"`
|
|
|
|
|
PriceMax *float64 `query:"price_max" description:"filter by maximum inclusive price" validate:"omitempty,max=15"`
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
##### 2. 使用一种稍微特殊的方式描述Hander函数
|
|
|
|
|
|
|
|
|
|
- 函数的参数: 第一个参数为gin的上下文信息,第二个参数(**可选**)为入参的数据(各种parameters和body),
|
|
|
|
|
- 返回值为: 第一个参数(**可选**)为返回值,第二个参数为error
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// CreateFruit add a new fruit to the market.
|
|
|
|
|
func CreateFruit(c *gin.Context, fruit *Fruit) (*Fruit, error) {
|
|
|
|
|
market.Lock()
|
|
|
|
|
defer market.Unlock()
|
|
|
|
|
n := strings.ToLower(fruit.Name)
|
|
|
|
|
if _, ok := market.fruits[n]; ok {
|
|
|
|
|
return nil, errors.AlreadyExistsf("fruit")
|
|
|
|
|
}
|
|
|
|
|
fruit.AddedAt = time.Now()
|
|
|
|
|
market.fruits[n] = fruit
|
|
|
|
|
return fruit, nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
##### 3. 编写fizz的路由相关函数
|
|
|
|
|
|
|
|
|
|
简单介绍:
|
|
|
|
|
|
|
|
|
|
- fizz.Summary: 为接口添加描述信息
|
|
|
|
|
- fizz.Response:为接口添加额外的可能的返回信息,一般用于接口可能返回多种status code的情况
|
|
|
|
|
- fizz.ResponseWithExamples: 同上,可以添加额外的示例返回信息。
|
|
|
|
|
- tonic.Handler: 封装第二步的特殊Handler,返回一个标准的`gin.HandlerFunc` 即:`func(*gin.Context)`
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Add a new fruit to the market.
|
|
|
|
|
grp.POST("", []fizz.OperationOption{
|
|
|
|
|
fizz.Summary("Add a fruit to the market"),
|
|
|
|
|
fizz.Response("400", "Bad request", nil, nil,
|
|
|
|
|
map[string]interface{}{"error": "fruit already exists"},
|
|
|
|
|
),
|
|
|
|
|
}, tonic.Handler(CreateFruit, 200))
|
|
|
|
|
|
|
|
|
|
// Remove a fruit from the market,
|
|
|
|
|
// probably because it rotted.
|
|
|
|
|
grp.DELETE("/:name", []fizz.OperationOption{
|
|
|
|
|
fizz.Summary("Remove a fruit from the market"),
|
|
|
|
|
fizz.ResponseWithExamples("400", "Bad request", nil, nil, map[string]interface{}{
|
|
|
|
|
"fruitNotFound": map[string]interface{}{"error": "fruit not found"},
|
|
|
|
|
"invalidApiKey": map[string]interface{}{"error": "invalid api key"},
|
|
|
|
|
}),
|
|
|
|
|
}, tonic.Handler(DeleteFruit, 204))
|
|
|
|
|
|
|
|
|
|
// List all available fruits.
|
|
|
|
|
grp.GET("", []fizz.OperationOption{
|
|
|
|
|
fizz.Summary("List the fruits of the market"),
|
|
|
|
|
fizz.Response("400", "Bad request", nil, nil, nil),
|
|
|
|
|
fizz.Header("X-Market-Listing-Size", "Listing size", fizz.Long),
|
|
|
|
|
}, tonic.Handler(ListFruits, 200))
|
|
|
|
|
```
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
##### 4. main函数将以上逻辑串起来
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
func main() {
|
|
|
|
|
router, err := NewRouter()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
srv := &http.Server{
|
|
|
|
|
Addr: ":4242",
|
|
|
|
|
Handler: router,
|
|
|
|
|
}
|
|
|
|
|
_ = srv.ListenAndServe()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewRouter returns a new router for the
|
|
|
|
|
// Pet Store.
|
|
|
|
|
func NewRouter() (*fizz.Fizz, error) {
|
|
|
|
|
engine := gin.New()
|
|
|
|
|
engine.Use(cors.Default())
|
|
|
|
|
|
|
|
|
|
fizz := fizz.NewFromEngine(engine)
|
|
|
|
|
|
|
|
|
|
// Override type names.
|
|
|
|
|
// fizz.Generator().OverrideTypeName(reflect.TypeOf(Fruit{}), "SweetFruit")
|
|
|
|
|
|
|
|
|
|
// Initialize the informations of
|
|
|
|
|
// the API that will be served with
|
|
|
|
|
// the specification.
|
|
|
|
|
infos := &openapi.Info{
|
|
|
|
|
Title: "Fruits Market",
|
|
|
|
|
Description: `This is a sample Fruits market server.`,
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
}
|
|
|
|
|
// Create a new route that serve the OpenAPI spec.
|
|
|
|
|
fizz.GET("/openapi.json", nil, fizz.OpenAPI(infos, "json"))
|
|
|
|
|
fizz.GET("/doc", nil, func(ctx *gin.Context) {
|
|
|
|
|
ctx.Status(200)
|
|
|
|
|
ctx.Header("Content-Type", "text/html")
|
|
|
|
|
ctx.String(200, rapidoc)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Setup routes.
|
|
|
|
|
routes(fizz.Group("/market", "market", "Your daily dose of freshness"))
|
|
|
|
|
|
|
|
|
|
if len(fizz.Errors()) != 0 {
|
|
|
|
|
return nil, fmt.Errorf("fizz errors: %v", fizz.Errors())
|
|
|
|
|
}
|
|
|
|
|
return fizz, nil
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 2. OAS的实现方式
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
比较核心的地方有两个
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. tonic.Handler 该方法使用reflect动态判断到传入的第一个参数,即第二步描述的Handler,根绝参数长度判断有无入参,根据返回值的长度判断有无返回值。如果有,则继续用reflect解析传入的struct,根据struct的tag描述出来各数据模型。如此的一个过程则基本上已经把一个接口的入参、返回能描述清楚。[关键代码](https://github.com/loopfz/gadgeto/blob/c4f8b2f64586099b9b281cbe99aa2f8b05e7d8b0/tonic/handler.go#L308)
|
2022-03-08 22:09:04 +08:00
|
|
|
|
|
|
|
|
|
另外,由于已经拿到了Handler的所有信息,包括入参的具体类型信息。所以还为我们提供了一个非常有用的额外能力:通过tag的描述,直接把各个位置的Parameters和RequestBody的值取到,然后把取到的Value设置到函数入参的指针里。所以我们可以在Handler中直接使用第二个参数中的值,无需自己再另行反序列化或者绑定操作。[关键代码](https://github.com/loopfz/gadgeto/blob/c4f8b2f64586099b9b281cbe99aa2f8b05e7d8b0/tonic/handler.go#L50)
|
|
|
|
|
2. fizz.OperationOption 库提供了一系列的OperationOption,可以为第一步的信息增加更多的对OpenAPI的支持。再配合上二次封装的Router,即可获取到整个OAS请求的全部信息。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. 开发流程很直观,比起纯粹的Gin项目开发额外需要改造的东西并不多,提供的有限的几个API上手也比较快
|
|
|
|
|
2. 可以完全利用Gin提供的所有API,不失灵活性。
|
|
|
|
|
3. 提供了 go-validator 和 一部分json_schema规则的校验。业务逻辑里不必再写取参数、参数校验部分的冗余代码,可以直接处理业务逻辑,且和接口文档严格保持一致。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. 知名度低,小众
|
|
|
|
|
2. 因为struct中字段的各种描述信息只能在tag中描述,会导致tag写的很长,比较丑
|
|
|
|
|
3. 参考代码信息,json_schema部分的支持不够完全,有些部分与go-validator共用一些字段可能会导致混乱
|
|
|
|
|
4. 框架绑定死了,基本不能解耦
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
### 5. Django REST Framework
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
Django项目是Python语言中最知名的Web框架,在2005年被开源以来,用户量非常庞大,目前Django社区依然非常活跃。
|
|
|
|
|
|
|
|
|
|
Django REST Framework(以下简称DRF)是著名开源组织[encode](https://www.encode.io)在2011年专门面向Django用户开发REST API的需求而诞生的一套工具。
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 1. 开发过程
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
创建python虚拟环境、Django项目初始化相关的流程由于比较冗长,涉及到的上下文信息比较多,这里就不再赘述,感兴趣可以去看[Django官方文档](https://docs.djangoproject.com/en/4.0/)和[Django REST Framework官方文档](https://www.django-rest-framework.org),我们现在只关注他是怎么描述Schema的。
|
|
|
|
|
|
|
|
|
|
在DRF中,描述数据model的结构叫做Serializer。官方提供了非常多的内置的序列化方法,包括各种可接受的数据类型,然后用户在定义Serializer的时候使用kwargs为各个参数字段添加约束信息。serializer.Serializer类本身提供了 request → serializer → response 数据流的转化能力。用户只需要继承这些能力即可。例如
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class SnippetSerializer(serializers.Serializer):
|
|
|
|
|
# int类型、只读、默认required=True
|
|
|
|
|
id = serializers.IntegerField(read_only=True)
|
|
|
|
|
# str类型、允许为空、最大长度限定100
|
|
|
|
|
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
|
|
|
|
|
# bool类型、非必填字段
|
|
|
|
|
linenos = serializers.BooleanField(required=False)
|
|
|
|
|
# 限定入参的值只能是提供的枚举中的值、并指定了默认值
|
|
|
|
|
language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
也提供了直接继承Django Model的能力
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class Snippet(models.Model):
|
|
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
title = models.CharField(max_length=100, blank=True, default='')
|
|
|
|
|
code = models.TextField()
|
|
|
|
|
linenos = models.BooleanField(default=False)
|
|
|
|
|
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
|
|
|
|
|
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
|
|
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
ordering = ['created']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SnippetSerializer(serializers.ModelSerializer):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = Snippet
|
|
|
|
|
fields = "__all__"
|
|
|
|
|
```
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 2. OAS的实现方式
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
如上面的例子,基于Serializer的描述可以获取到各种类型的request body信息,加上django-filter和django的router可以得到完整的API描述信息。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. 因为Serializer 与Django-ORM 的Model 可以结合在一起,大部分情况下,数据模型只需要定义一次,所有的地方都能得到约束,行为统一
|
|
|
|
|
|
|
|
|
|
如:上文Snippet类的title字段设置为max_length=100,那么接口入参的时候会校验长度,生成的文档中该参数也有对应的最大值,还有默认情况下数据库定义也会是varchar(20)
|
|
|
|
|
2. 经过了10年以上的考验,基本上碰得到的问题都能够找得到答案。本身也提供了各种各样丰富的API让用户可以完成各种场景的需求。
|
|
|
|
|
|
|
|
|
|
光论开发效率的话,可能是笔者知道的开发效率最高的web框架了。简单场景下,从零到一实现一张表的所有增删改查接口,一共只需要不到十行代码。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. Python语言本身导致的性能低下。虽然Django提供了几乎整个WEB周期所需要的全部工具,开发体验一流,但是仍然不适用于要求性能的场景,Django的主战场可能还是CMS或者是用于一些产品的MVP版本快速试错。
|
|
|
|
|
|
|
|
|
|
2. 隐晦性的声明太多,行为不够直观。历史包袱过重,未来发展的空间不大。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
### 6. FastAPI
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
[FastAPI](https://fastapi.tiangolo.com/)可以算是近两年python web框架中最炙手可热的明星项目了,上线仅3年多就已收获40000多star。得益于python3.5版本以来带来的async await异步关键字,和各种高性能的asyncio runtime的出现,性能上已经比传统的Flask等类似的微框架的表现有了极大的改观。
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 1. 开发过程
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
##### 1. 编写业务逻辑main.py
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Item(BaseModel):
|
|
|
|
|
id: Optional[int] = None
|
|
|
|
|
name: str = Field(description="The name of the item", regex="^[a-zA-Z0-9]*$")
|
|
|
|
|
price: float
|
|
|
|
|
is_offer: Optional[bool] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
def read_root():
|
|
|
|
|
return {"Hello": "World"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/items/{item_id}")
|
|
|
|
|
def read_item(item_id: int, q: Optional[str] = None):
|
|
|
|
|
return {"item_id": item_id, "q": q}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/items")
|
|
|
|
|
def create_item(item: Item) -> Item:
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
##### 2. 使用uvicorn启动服务
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
uvicorn main:app
|
|
|
|
|
```
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
#### 2. OAS的实现方式
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
通过Python提供的类型注解功能,程序在启动的时候,就能够得到所有入参内容、和返回结果,包括他们的类型信息。开发体验更像是直接写好处理逻辑的函数,然后框架把这些函数按照一定的规则用HTTP的形式实现。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. 所见即所得,开发接口的时候非常直观,效率很高,而且提供了十分丰富的其他功能使得参数可以复用。
|
|
|
|
|
2. 设计清晰易懂,用户层面不需要知道太多细节,不强制添加对应的类型注解(当然对应的也就无法得到足够的文档信息)
|
|
|
|
|
3. 可以直接利用python语言提供的强大生态资源。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
|
|
|
|
|
2022-03-09 21:07:59 +08:00
|
|
|
|
1. 除了语言与公司技术栈不太相符外,找不到明显的缺点。
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
### 7. Poem
|
|
|
|
|
|
|
|
|
|
#### 1. 开发过程
|
|
|
|
|
|
|
|
|
|
#### 2. OAS的实现方式
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
WIP
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 3. 优点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
WIP
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
|
|
|
|
#### 4. 缺点
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
WIP
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
|
|
|
|
## 4. FAQ
|
|
|
|
|
|
|
|
|
|
### 1. Swagger和OpenAPI的关系和区别?
|
|
|
|
|
|
|
|
|
|
简单来说
|
|
|
|
|
|
|
|
|
|
- OpenAPI: Specification(规范)
|
|
|
|
|
- Swagger: Tools for implementing the specification(实现规范的一系列工具)
|
|
|
|
|
|
|
|
|
|
Swagger最初是在2010年设计RESTful API的简单开源规范。还开发了包括Swagger UI、Swagger Editor和Swagger Codegen等开源工具,以更好地实现和可视化规范中定义的 API。由规范和开源工具组成的Swagger项目变得非常流行,创建了一个庞大的社区驱动工具生态系统。
|
|
|
|
|
|
|
|
|
|
在2015年,Swagger项目被SmartBear Software收购。Swagger规范被捐赠给Linux基金会并更名为OpenAPI Specification(OAS),是描述REST API的标准规范。
|
|
|
|
|
|
|
|
|
|
此后,Swagger已成为最受欢迎的工具套件,可在整个 API 生命周期中充分利用 OAS 的强大功能。
|
|
|
|
|
SmartBear Software 支持的 Swagger 工具是最流行的实现 OpenAPI 规范的工具之一,并将继续保持 Swagger 名称(Swagger Editor、Swagger UI、SwaggerHub 等)
|
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
> 参考阅读: <https://nordicapis.com/whats-the-difference-between-swagger-and-openapi/>
|
2022-03-07 21:23:06 +08:00
|
|
|
|
|
2022-03-07 20:49:02 +08:00
|
|
|
|
### 2. JSON Schema?
|
|
|
|
|
|
2022-03-07 21:23:06 +08:00
|
|
|
|
例如vscode中配置文件的代码补全和校验就是基于json schema完成的。
|
|
|
|
|
JSON Schema 是一种定义JSON格式的规范,相关生态已经比较完备,各种语言的校验工具基本都有实现。
|
2022-03-07 20:49:02 +08:00
|
|
|
|
|
2022-03-08 22:09:04 +08:00
|
|
|
|
OpenAPI中对Json对象的描述就是使用Json Schema或者其中的一部分子集、扩展来描述的
|
2022-03-07 21:23:06 +08:00
|
|
|
|
目前为止已经发布了多个版本的草案,其中
|
2022-03-08 22:09:04 +08:00
|
|
|
|
|
|
|
|
|
- OAS2 采用的是Draft 4的一个子集
|
|
|
|
|
- OAS3.0 采用的是Wright Draft 00(基于Draft 5的修改)
|
|
|
|
|
- OAS3.1 采用的是Draft 2020-12的超集 第一次完整兼容json_schema中描述的所有关键字
|
|
|
|
|
|
|
|
|
|
### 3. 各版本的区别有哪些?
|
|
|
|
|
|
|
|
|
|
其中OpenAPI2.0和OpenAPI3.0版本(目前应用最广泛的版本是3.0.3)的区别比较大。
|
|
|
|
|
|
|
|
|
|
最显著的是文档结构的组织,提升了描述的便利性和并提升了复用的可能性。区别如下图:
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
其他的一些改进还有,examples字段、Security Flow部分的改进、参数部分的改进、增加对callback的描述支持等。
|
|
|
|
|
|
|
|
|
|
OpenAPI3.1的显著改进是增加了对Json Schema的100%兼容、对类型数组的支持等。
|
|
|
|
|
|
|
|
|
|
> 参考阅读: <https://blog.stoplight.io/difference-between-open-v2-v3-v31>
|