引言
为了将自己的服务提供出去供第三方使用,往往需要把服务以标准的 OpenAPI 形式暴露,并提供多语言版本的 SDK(软件开发工具包),比如常见的环信即时通讯服务、飞书开放服务、微信开放服务等都是基于 SDK 来让开发者调用和接入的。那么如何把我们自己写的 HTTP 服务转换成 OpenAPI 的标准格式,再包装成便捷的 SDK 呢?最近我基于 Swagger 和 OpenAPI Generator 两个利器进行了一些实践。
Swagger
随着前后端分离开发模式的普及,前端已经全权承担了静态资源的管理,而后端也只用专心于提供接口,前后端之间通过传输载体(通常为 JSON)进行交互。既然需要交互,那么前端必然需要知道后端的接口地址、入参、出参、参数位置等定义,才能准确包装好请求体(通常为 HTTP)。
那后端如何提供接口定义呢?常用的方式可以通过写文档 或 Swagger UI。写文档的方式比较机械,如果代码里的接口定义发生修改,则要同步记得更改文档。相比之下 Swagger 会友好得多,Swagger 是一个可以生成、描述、调用、可视化 Restful 风格服务的框架,它里面的组件很多,但我们最常用到的,就是在我们的代码里面引入 Swagger 的库或者插件,然后通过在 HTTP 服务上打注解(或写注释),然后一键生成 swagger.json 文档,该文档是符合 OpenAPI 规范的,并且会启动 Swagger UI 服务,提供可视化的页面供前端查看。下面是使用 Golang 语言提供的一个 HTTP 服务,服务上方被打上了 Swagger 可以识别的注释,并引入了 github.com/swaggo/gin-swagger 库来生成 Swagger 文档和页面:
import ( "demo/request" "demo/response" ) // @Summary 创建用户 // @Users user // @Accept json // @Produce json // @Param object body request.CreateUserRequest true "创建用户参数" // @Success 200 {object} response.Response[CreateUserResponse] // @Router /users [post] func (h *UserHandler) Create(ctx *gin.Context) { // parse request ... // validate request ... // invoke service ... // return response ... }
可以看到我们只需向 gin 服务的入口函数上写上这个 HTTP 接口的定义即可,需要说明的是 @Param 和 @Success 由于用到了 request 和 response 包下的结构体,Swagger 要求必须引入这些结构体所在的包才能完成解析。CreateUserRequest, CreateUserResponse, Response 结构体的定义如下:
package request type CreateUserRequest struct { Username string `json:"Username"` Password string `json:"Password"` Role string `json:"Role"` }
package response type Response[T any] struct { Code int `json:"Code"` Message string `json:"Message"` Data T `json:"Data"` } type CreateUserResponse struct { UserId string `json:"UserId"` Status int `json:"Status"` }
需要说明的是,因为每个响应结果除了要返回数据本身,往往还要返回 Code 和 Message 标识本次请求在业务语义上结果,因此这里引入了 Response[T any] 泛型类进行装饰,那么相应的,上面 Swagger 注释也需要写成这样 response.Response[CreateUserResponse]。最后我们还需要使用安装命令 swag 命令行工具来生成 Swagger 文档,像下面这样:
# install go get -u github.com/swaggo/swag/cmd/swag go install github.com/swaggo/swag/cmd/swag@latest # copy swag from $GOPATH to /usr/local/bin cp $GOPATH/bin/swag /usr/local/bin # generate swagger.json swag init -g ./cmd/server/main.go -o ./cmd/server/doc --parseDependency
执行完上面生成命令后,swagger.json 便会出现在我们的目录下:
. ├── cmd │ ├── server │ │ ├── docs │ │ │ ├── docs.go │ │ │ ├── swagger.yaml │ │ │ └── swagger.json │ │ └── main.go ...
gin-swagger 注释的语法还有很多,以及如何提供 Swagger UI 静态页面服务,都可以参考这里,本文不再赘述。
OpenAPI Generator
其实以前我用 Swagger 也就到上面为止了,更多的是去使用 Swagger UI 查看接口,至于 swagger.json 为何物完全不关心,另外如果要调用 HTTP 接口,直接基于 HTTP 通信库填好URL 地址、Header、入参、令牌就好了,为什么还需要 SDK 呢?其实直接调用 HTTP 通信库一点问题都没有,但这样会增大接入方的研发成本,他需要手工拼接好接口的参数,字段还不能有拼写错误,另外如果参数有签名还要关心用什么签名算法,因此 SDK 就是为了给接入方减负出现的,因为它不再要求接入方关注过多细节,而通过编程语言的函数封装、函数签名、结构体/类都约束好了,如果开发者接口或者字段拼写有错误,那么在程序编译的时候就会报出来,风险相当于从服务器端上移到客户端了。
那我们如何才能更方便的生成 SDK 呢?上面提到的 swagger.json 就很重要了,正因为它是遵循标准的 OpenAPI 规范生成的,因此只要将这份文档作为输入,传递给 OpenAPI Generator 工具,便可以生成各种语言的 SDK 代码了! 这或许就是制定标准和规范的伟大之处吧,能够让同样的数据在多种不同组织开发的组件间共享,最后形成一种生态。
OpenAPI Generator 可以简单理解为就是一个可以基于标准的 OpenAPI 描述文档生成多语言 SDK 的工具,它基于 Swagger Codegen 并且功能方面更为丰富强大。OpenAPI Generator 的安装方式有很多种,官方文档有非常详细的说明。安装好后我们就可以像下面这样从一份 swagger.json 文件导出 SDK 啦~
openapi-generator generate \ -i swagger.json \ # 输入文件 -g go \ # 编程语言 -o sdk/ # 输出目录
OpenAPI Generator 默认生成的 SDK 里包含的一些命名有的地方会比较奇怪,比如用包名+下划线拼接成的文件名,然后文件都是平铺开的没有划分目录,但这些都可以通过修改对应语言的默认模版来定制化,官方文档同样有很多介绍,本文不做赘述。
使用 SDK
有了上面导出的 SDK,我们就可以像下面这样在客户端引入它然后方便地进行调用啦~ 下面是一个 Golang SDK 的示例:
func ListUsers() error { // 定义配置 config := openapi.NewConfiguration() config.Host = "my-service.com:9090" config.Scheme = "http" config.AddDefaultHeader("caller-uid", "admin") config.AddDefaultHeader("caller-token", "18a87dfs12sdf9381aww0f2383") // 创建客户端对象 apiClient = openapi.NewAPIClient(config) // 调用服务 bizResponse, httpResponse, err := apiClient.UserApi.UsersGetExecute(openapi.ApiUsersGetRequest{ Role: "normal-user", Status: "Logined", }) if err != nil { return err } if httpResponse == nil || httpResponse.Status != "200 OK" { return fmt.Errorf("error occurred") } // 打印调用结果 fmt.Println(bizResponse) } //bizResponse: //{ // Code: 0, // Message: "success", // Data: { // TotalCount: 20, // Users: [{ // Id: 1, // Name: "alice", // Role: "normal-user", // Status: "Logined" // }, { // Id: 1, // Name: "bob", // Role: "normal-user", // Status: "Logined" // }] // } //}
结语
上面便是从服务器端代码到最终多语言的 SDK 的生成路径,总结起来就是下面这样:
- 为服务端代码的 HTTP 方法加上 Swagger 注释
- 由 swag 工具生成 swagger.json 文档
- 将 swagger.json 作为输入参数传递给 openapi-generator 工具生成对应语言的 SDK
可以看出,要想获得一份 OpenAPI SDK,我们不再需要手工编写繁琐的请求发送和解析逻辑,只用按照 Swagger 注释规范填写好接口的签名信息即可~ 这种方式将 SDK 与原始接口相绑定,所见即所得,减少了两侧定义不一致的风险出现,导出 SDK 更加快速高效,又遵循了标准的 OpenAPI 规范。