2022-03-08 22:13:35 +08:00
2022-03-08 22:09:04 +08:00
2022-03-08 22:13:35 +08:00

[[TOC]]

OpenAPI3 Framworks Compare

本文会横向对比几种支持OpenAPI文档生成的工具/框架。

OpenAPI Specification下文简称OAS定义了一个标准的、语言无关的 RESTful API 接口规范,它可以同时允许开发人员和操作系统查看并理解某个服务的功能,而无需访问源代码,文档或网络流量检查(既方便人类学习和阅读,也方便机器阅读)。正确定义 OAS 后,开发者可以使用最少的实现逻辑来理解远程服务并与之交互。

此外,文档生成工具可以使用 OpenAPI 规范来生成 API 文档代码生成工具可以生成各种编程语言下的服务端和客户端代码测试代码和其他用例。OpenAPI官方提供了各种语言的服务端和客户端的代码生成工具比较著名的如OpenAPI Generator,当然也有很多优秀的第三方开发者开发的工具。

参考阅读

1. 项目背景

目前公司内部开发中的项目存在很多接口文档规范不统一、实现不一致的问题。据笔者的了解,起码目前存在以下的几种维护方式。

  1. 使用swag工具在代码中加入注解,然后通过代码生成对应项目的文档
  2. 一些使用grpc-gateway的grpc项目会使用其附带的protoc-gen-openapiv2工具对protobuf文件进行分析生成对应的文档
  3. 在yapi中手撸接口文档
  4. 纯手动自己维护一个自定义格式的HTML文档

其中前两种方式使用工具生成,后两种完全靠双手自行维护。除第二种方式外,其他三种方式都多少依赖维护者的自觉性,维护得好皆大欢喜,维护得不好就会引发一系列的连锁反应。

  • 新人接手项目。目前维护的这个服务提供了哪些能力?接手新需求的时候我是要新增一个接口还是已经存在类似的接口,只需要扩展一下?如果没有完整的接口文档,只能硬啃现有代码。如果代码量比较巨大的话会非常痛苦,或者疑惑我应该信代码还是信文档?会不会是代码实现有失误的地方?总之满头问号
  • 前后端对接、服务间对接。参数描述基本靠飞书返回描述基本靠Chrome开发者工具。一个维护得足够久的接口没有人能够描述出来到底返回什么样的结构需要传递哪些参数怎么传哪些必传这些参数有哪些约束条件或者一些已经废弃的参数可能早就无人使用但是没人敢删除。到达某种状态后只能是懒得细扣代码继续往上加。 总结:口头对接一时爽,事后复盘火葬场。
  • 重构。项目到达一定的复杂程度、或者随着产品的不断迭代,接口可能会面临着重构。如果没有足够的测试代码且对接口行为没有完全十足的把握的话,重构无疑会存在着巨大的风险,一不小心在某个获取参数的地方导致行为不一致、或者返回的结构有差别的话就会导致整个下游服务的行为异常。

这也是OAS存在的理由。如果所有的服务都使用了某种方式能从代码中生产出对应可信赖的、接口描述与代码行为严格同步的OAS文档上述的所有问题都会迎刃而解。且Schema First的开发流程也更符合Go语言面向接口编程的理念。

此外也可以利用上社区内已经非常丰富的OAS生态相关工具包括sdk生成、文档渲染工具等等。因为所有的文档都是标准的规范甚至可以团队自己开发自己需要的工具提升开发效率。一旦完善趋于完善还可以做一些更有趣的事情比如:

  1. 公司内所有服务可版本溯源的HTTP API接口文档查询工具。
  2. 接通内管平台,对所有服务的接口管理。设定接口权限?联动网关层(如Istio)实现一个可视化的流量看板?

既然如上列举的各种优势现在摆在眼前的问题只有一个那就是实现代码和OAS文档同步带来的额外工作量和难度如果同步文档的过程对开发者过于繁琐且耗时那么必定在推广的时候困难重重。所以现在的情况就是寻找一个可以快速、简便地维护OAS文档的方法

  1. 找现成的开源方案
  2. 自己造轮子

知己知彼,才能百战不殆。所以本项目的目的就是借鉴前人的设计和想法,总结好的设计方法,摒弃不好的设计方法。尽可能多的去了解别人是怎么做这件事情的,才能更好地去完成自己的。

以下内容会分别使用几种包含从代码中同步文档feature的框架(欢迎补充)用其实现一些简单的用例然后分析这些框架的实现方式和背后生成OAS的逻辑。随后会加上一些笔者的主观评价。

2. 参赛选手介绍

语言 框架 Web功能完备 OpenAPI版本 Github Stars 当前版本 first release date
Go goa 3 stars Release 2016-08-03
Go swag 2 stars Release 2017-11-30
Go grpc-gateway 2 stars Release 2016-07-11
Go fizz 3 stars Release 2019-11-06
Python Django REST framework 3 stars Release 2011-02-22
Python FastAPI 3 stars Release 2018-12-16
Rust Poem 3 stars Release 2021-10-14

3. 详细对比

所有框架均会从以下几个方面进行讨论

  1. 开发过程
  2. OAS的实现方式: 探究这些工具是如何从代码生成对应sepc文件的并会导航一些关键代码方便读者进行深入探究。
  3. 优点:该框架的一些特色或者相比其他框架有优势的地方
  4. 缺点:该框架的一些劣势,或者不使用的理由

所有评价均为个人意见,欢迎不同声音:D

1. Goa

1. 开发过程

  1. 安装goa提供的代码生成工具

    go install goa.design/goa/v3/cmd/goa@v3
    
  2. 创建一个design文件夹用于描述API

    mkdir -p goa_example/design
    
  3. 使用goa提供的dsl描述API desigin.go

  4. 生成代码模板

    goa gen goa_example/design
    

    该步骤会生成gen文件夹中的所有文件包括design文件中定义的所有接口信息、Model描述、验证方式、Protobuf描述(如果有)等所有相关信息的Go描述

  5. (optional) 生成实现的一个example

    goa example goa_example/design
    

    该步骤会生成cmd文件夹包括http server和 grpc server的启动代码 和项目根目录/{service_name}.go的文件其中包含了各方法的默认实现(fmt.Println())

  6. 完成代码实现

    接下来只需要修改各方法中的具体实现为真实业务逻辑即可service1.go

2. OAS的实现方式

OAS中的所有对象均使用框架自定的DSL实现定义design.go文件的同时gen程序可以获得文档中需要的所有描述信息。 然后根据这些描述信息代码生成所需要的各种类型和interface、包括OAS描述文件让用户尽可能的少写重复代码只去完成关键逻辑。

3. 优点

  1. 可同时生成HTTP和gRPC两种服务对应的实现比较完整基本可以描述所有类型的API
  2. 生成的代码质量比较高提供的代码抽象比较好。基本上只需要完成业务逻辑的实现就可以。其中甚至包含了分别使用http和grpc请求远端的命令行请求工具

4. 缺点

  1. 提供的DSL学习曲线很高其基于查询调用栈的API设计虽然调用非常灵活但是会导致所有的函数均为灵活的不定长interface{}传参,除非熟练工,否则会因为没有任何代码提示导致使用体验非常差。
  2. Model的描述方式虽然定制能力比较高但是定义过程相当繁琐不够直观。
  3. 使用时需要对OAS有足够的了解否则不能很好地去合理设计API。
  4. 社区内相对比较小众。(6年积累了star 4.5k)
  5. 因为定制程度非常高可能会导致灵活性较差一些需要自定义http行为的特殊情况需要自己去Hack

2. swag

1. 开发过程

  1. 安装swag命令行工具

    go install github.com/swaggo/swag/cmd/swag@latest
    
  2. 创建一个main.go 先写一个简单的http服务

     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

    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

    swag init
    
    tree docs
    
    docs
    ├── docs.go
    ├── swagger.json
    └── swagger.yaml
    

2. OAS的实现方式

命令行工具直接从源码的抽象语法树中获得到所有文件中的Comment信息然后根据自定的规则把所有获得到的信息进行组合然后生成文档。核心代码

3. 优点

  1. 与框架解耦不影响原有的HTTP部分的逻辑各种http框架均可轻松实现

4. 缺点

  1. 也相当于是自定义的DSL需要多查文档才能知道如何表达对应的对象信息也需要对OpenAPI2.0规范有足够了解
  2. 文档与代码逻辑完全脱离文档与代码实现是否一致完全看开发人员的良心经常碰到的情况是Comment年久失修
  3. 写起来比较啰嗦,而且纯文本,体验差
  4. 使用的是2.0规范显得有点老了仓库的Maintainer对支持3.0不太积极,相关issue已经提了将近3年

3. grpc-gateway

1. 开发过程

基本的gRPC+gRPC-Gateway开发流程不再赘述额外使用protoc-gen-openapiv2即可生成代码 工具会自动获取到proto文件中定义的message enum service等对象的注释并添加至Spec文档中对应的description

2. OAS的实现方式

  1. 使用proto库提供的能力可以获取到proto文件中的扩展信息。核心代码
  2. 从扩展信息中获取对应的信息。核心代码

3. 优点

  1. 在Google的加持下gRPC服务的用户众多。性能优异 + 跨语言支持 + 生态丰富
  2. 对于OAS的基础功能字段注释结构体映射增加文档几乎没有额外的开发成本。
  3. grpc + gateway模式衍生出来的很多二次开发的框架均使用的是这套方案或类似方案。(kratos、go-zero等)
  4. 相比上面的几种方案可以做到代码文档完全同步。

4. 缺点

  1. 与gRPC强绑定不适用与传统Web服务框架
  2. 支持的功能有限,某些基础功能不支持(为API增加Tag等)protobuf的设计原则导致生成的(特指proto3)所有结构体中的字段均为可选字段需要借助Envoy出品的PGV protoc-gen-validate插件来增加一些必选参数、参数校验的功能。提供的能力只能说勉强够用。

4. Fizz

该工具其实并不能算是一个独立的框架更像是核心是Gin然后进行二次包装的库。 虽然非常小众GitHub star只有100多但是笔者觉得这个库的设计非常巧妙可以借鉴学习。

1. 开发过程

1. 描述接口中的各种数据结构tag中增加各种支持的json_schema信息
// 描述数据模型
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
// 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)
// 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))

2. OAS的实现方式

比较核心的地方有两个

  1. tonic.Handler 该方法使用reflect动态判断到传入的第一个参数即第二步描述的Handler根绝参数长度判断有无入参根据返回值的长度判断有无返回值。如果有则继续用reflect解析传入的struct根据struct的tag描述出来各数据模型。如此的一个过程则基本上已经把一个接口的入参、返回能描述清楚。关键代码

    另外由于已经拿到了Handler的所有信息包括入参的具体类型信息。所以还为我们提供了一个非常有用的额外能力通过tag的描述直接把各个位置的Parameters和RequestBody的值取到然后把取到的Value设置到函数入参的指针里。所以我们可以在Handler中直接使用第二个参数中的值无需自己再另行反序列化或者绑定操作。关键代码

  2. fizz.OperationOption 库提供了一系列的OperationOption可以为第一步的信息增加更多的对OpenAPI的支持。再配合上二次封装的Router即可获取到整个OAS请求的全部信息。

3. 优点

WIP

4. 缺点

WIP

5. Django REST Framework

1. 开发过程

2. OAS的实现方式

WIP

3. 优点

WIP

4. 缺点

WIP

6. FastAPI

1. 开发过程

2. OAS的实现方式

WIP

3. 优点

WIP

4. 缺点

WIP

7. Poem

1. 开发过程

2. OAS的实现方式

WIP

3. 优点

WIP

4. 缺点

WIP

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 等)

参考阅读: https://nordicapis.com/whats-the-difference-between-swagger-and-openapi/

2. JSON Schema?

例如vscode中配置文件的代码补全和校验就是基于json schema完成的。 JSON Schema 是一种定义JSON格式的规范相关生态已经比较完备各种语言的校验工具基本都有实现。

OpenAPI中对Json对象的描述就是使用Json Schema或者其中的一部分子集、扩展来描述的 目前为止已经发布了多个版本的草案,其中

  • 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)的区别比较大。

最显著的是文档结构的组织,提升了描述的便利性和并提升了复用的可能性。区别如下图: v2 vs v3

其他的一些改进还有examples字段、Security Flow部分的改进、参数部分的改进、增加对callback的描述支持等。

OpenAPI3.1的显著改进是增加了对Json Schema的100%兼容、对类型数组的支持等。

参考阅读: https://blog.stoplight.io/difference-between-open-v2-v3-v31

Description
No description provided
Readme 156 KiB
Languages
Go 70.8%
Python 19.4%
Rust 8.2%
HTML 1.6%