gitbook/手把手带你写一个Web框架/docs/435582.md
2022-09-03 22:05:03 +08:00

22 KiB
Raw Permalink Blame History

23管理接口如何集成swagger自动生成文件

你好,我是轩脉刃。

不管你是前端页面开发还是后端服务开发你一定经历过前后端联调的场景前后端联调最痛苦的事情莫过于没有完善的接口文档、没有可以调用调试的接口返回值了所以一般都会采用形如Postman这样的第三方工具来进行接口的调用和联调。

但是这一节课我们要做的事情就是为自己的Web应用集成swagger使用swagger自动生成一个可以查看接口、可以调用执行的页面。

swagger

说到swagger可能有的同学还比较陌生我来简要介绍一下。swagger框架在2009年启动之前是Reverb公司内部开发的一个项目他们的工程师在与第三方调试REST接口的过程中为了解决大量的接口与文档问题就设计了swagger这个项目。

项目最终成型的方案是先设计一个JSON规则开发工程师把所有服务接口按照这种规则来写成一个JSON文件这个JSON文件可以直接生成一个交互式UI可以提供调用者查看、调用调试

swagger的应用是非常广泛的。非常多的开源项目在提供对外接口的时候都使用swagger来进行描述。比如目前最火的Kubernetes项目每次在发布版本的时候都会在项目根目录上带上符合swagger规则的JSON文件,用来向使用者提供内部接口。

swagger的产品有两类。

一个是前面说的JSON规则就是OpenAPI的文档它说明了我们要写一个接口说明文档的每个字段含义和要求。

OpenAPI的规则也是有版本的目前最新版本是3.0但是3.0版本目前市场上相应的配套支持还不成熟比如Golang版本的SDK库spec还不支持。目前市面上对OpenAPI2.0的支持还是最全的。所以我们的hade框架就使用swagger2.0版本。

swagger的另外一类产品是工具包括swagger-ui、swagger-editor和swagger-codegen。

swagger-editor提供一个开源网站在线编辑swagger文件。swagger-codegen提供一个Java命令行工具通过swagger文件生成client端代码。而swagger-ui通过提供一个开源网站将swagger接口在线展示出来并且可以让调用者查看、调试。我们的目标是生成一个可以查看接口进行调用调试的页面所以要将swagger-ui集成进hade框架。

命令设计

了解了swagger结合框架我们照例先思考下希望如何使用它。

按照swagger的定义我们应该在业务项目中维护一个JSON文件这个文件描述了这个业务的所有接口。但是你想过没有随着项目的接口数越来越大维护swagger的JSON描述文档本身就是一个很大很繁杂的工作量

由于每个接口在代码开发的时候我们都会有注释而更新代码的时候我们是会去更新注释的。所以能不能有一个方法通过代码的注释自动生成这个JSON文件呢

这个就是我们希望定义的一个swagger命令./hade swagger gen 能通过注释生成swagger.json文件。

但是考虑具体的实现设计怎么用Golang的代码注释生成swagger.json呢既然swagger.json是有一定的规则的那么注释的写法也是有一定规则的吧是的。目前有一个最流行的将Golang注释转化为swagger.json 的开源项目swag

swag项目

这个swag项目是MIT 协议目前已经有4.9k 个star了。它的用法和我们想要的一样生成swagger.json分三步

  • 在API接口中编写注释。注释的详细写法需要参考说明文档
  • 下载swag工具或者安装swag库
  • 使用工具或者库将指定代码生成swagger.json

步骤很简单不过第一步怎么写swag的注释说明文档是使用这个技术必须要学习的一个知识这个的学习确实是有些门槛的需要熟读对应的说明文档才能写出比较好的注释。这里我们用一个例子来讲解我在编写代码的时候常用的一些字段供你参考。

// Demo2  for godoc
// @Summary 获取所有学生
// @Description 获取所有学生,不进行分页
// @Produce  json
// @Tags demo
// @Success 200 {array} []UserDTO
// @Router /demo/demo2 [get]
func (api *DemoApi) Demo2(c *gin.Context) {
   demoProvider := c.MustMake(demoService.DemoKey).(demoService.IService)
   students := demoProvider.GetAllStudent()
   usersDTO := StudentsToUserDTOs(students)
   c.JSON(200, usersDTO)
}

type UserDTO struct {
   ID   int    `json:"id"`
   Name string `json:"name"`
}

观察注释。第一行 Demo2 for godoc 这个在swagger中并没有实际作用它是用来给godoc工具生成说明文档的。从第二行开始就是我们swaggo的注释语法了使用@符号加上关键字的方式来进行说明。
例子的关键字有这些:

  • Summary为接口增加简要说明
  • Description为接口增加详细说明
  • Produce说明接口返回格式
  • Tags为接口打标签可以为多个便于查看者查找
  • Success接口返回成功时候的说明
  • Router接口的路由调用

具体对应的swagger-ui界面是这样的

我们对照注释和界面很容易就看出每个注释的最终显示效果。不过这里再啰嗦解释下比较复杂的Success注释。

在这个例子中是这样使用Success注释的

// @Success 200 {array} UserDTO

在成功的时候返回UserDTO结构的数组这里swaggo会自动去项目中寻找UserDTO结构来生成swagger-ui中的返回结构说明。

不过这里能这么写是因为恰好UserDTO是和API放在同一个namespace下如果你的返回结构放在不同的namespace下需要在注释中注明返回结构的命名空间。比如

// @Success 200 {array} model.Account

同时,这个返回结构还支持返回对象嵌套,比如下面这个例子:

// 返回了一个JsonResult对象其中这个对象的data字段是Order结构
@success 200 {object} jsonresult.JSONResult{data=proto.Order} "desc"

type JSONResult struct {
    Code    int          `json:"code" `
    Message string       `json:"message"`
    Data    interface{}  `json:"data"`
}

type Order struct { //in `proto` package
    Id  uint            `json:"id"`
    Data  interface{}   `json:"data"`
}

它返回了一个JSONResult对象这个JSONResult对象中的一个字段data是Order结构。

命令实现

现在注释已经标记好了我们再回到生成JSON文件的命令 ./hade swagger gen

这个命令通过swaggo准备好的命令行工具swag或者类库来生成JSON文件。由于我们的框架已经集成了命令行工具所以不会选择额外使用swag工具而是在我们的命令中集成swaggo类库swag/gen

这个类库最核心的结构就是Config结构。来看这个swagger gen命令的具体实现代码写在framework/command/swagger.go中

// swaggerGenCommand 生成具体的swagger文档
var swaggerGenCommand = &cobra.Command{
   Use:   "gen",
   Short: "生成对应的swagger文件, contain swagger.yaml, doc.go",
   Run: func(c *cobra.Command, args []string) {
      container := c.GetContainer()
      appService := container.MustMake(contract.AppKey).(contract.App)
      outputDir := filepath.Join(appService.AppFolder(), "http", "swagger")
      httpFolder := filepath.Join(appService.AppFolder(), "http")
      conf := &gen.Config{
         // 遍历需要查询注释的目录
         SearchDir: httpFolder,
         // 不包含哪些文件
         Excludes: "",
         // 输出目录
         OutputDir: outputDir,
         // 整个swagger接口的说明文档注释
         MainAPIFile: "swagger.go",
         // 名字的显示策略,比如首字母大写等
         PropNamingStrategy: "",
         // 是否要解析vendor目录
         ParseVendor: false,
         // 是否要解析外部依赖库的包
         ParseDependency: false,
         // 是否要解析标准库的包
         ParseInternal: false,
         // 是否要查找markdown文件这个markdown文件能用来为tag增加说明格式
         MarkdownFilesDir: "",
         // 是否应该在docs.go中生成时间戳
         GeneratedTime: false,
      }
      err := gen.New().Build(conf)
      if err != nil {
         fmt.Println(err)
      }
   },
}

结合这个具体实现我们来看这个Config结构的关键字段SearchDir、OutputDir和MainAPIFile这几个字段的含义必须完全理解才能设置正确其他的几个字段如果不理解直接使用默认值就行。

第一个 SearchDir 表示要swaggo去哪个目录遍历代码的注释来生成swagger的JSON文件。对于我们的hade框架所有接口文件都存放在app/http文件夹中所以要遍历的就是这个文件夹了。

第二个关键字段 OutputDir表示要输出的swagger文件的存放地址。我们在app/http目录下创建一个swagger目录来存放要输出的swagger文件。这里再补充一点前面说swagger最终会生成JSON文件但是你运行一次swagger gen 会发现这个生成目录下除了有swagger.json这个文件还有两个文件swagger.yaml 和 docs.go。

对于额外生成的这两个文件swagger.yaml 是YAML格式的接口说明文档里面的内容和swagger.json其实是一样的。而docs.go 是“接口说明文档”的代码它是为go项目直接引入接口说明文档生成swagger-ui用的

也就是说生成的docs.go我们的框架只需要import它就能从这个文件的变量doc中直接获取到“接口说明文档”不需要用读取文件的方式读取swagger.json 或者swagger.yaml。下一节课我们会使用它来让框架启动服务的时候自动启动swagger-ui。

第三个关键字段 MainAPIFile表示整个swagger接口的说明文档。这是什么意思呢在最终生成的swagger-ui界面上的头部你会看到对当前swagger接口的整体说明包括作者、接口版本、接口licence等信息。

这些信息也都是使用注释来自动生成的而这一部分注释就是存放在这个MainApiFile所指向的Go文件中。

我们的项目就固定将这个文件命名为swagger.go存放在app/http/swagger.go 文件中。这个文件只是增加注释不增加任何的业务逻辑。其中的每个注释的关键字说明也是参考swaggo的说明文档

// Package http API.
// @title hade
// @version 1.1
// @description hade测试
// @termsOfService https://github.com/swaggo/swag

// @contact.name yejianfeng1
// @contact.email yejianfeng

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @BasePath /
// @query.collection.format multi

// @securityDefinitions.basic BasicAuth

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization

// @x-extension-openapi {"example": "value on a json format"}

package http

现在按照前面的说明我们设置好了Config结构在swaggerGenCommand 命令逻辑的最后只需调用一次Build方法就能按照Config配置来生成 docs.go、swagger.json、swagger.yaml这三个文件了。

err := gen.New().Build(conf)

记得将这个二级命令 ./hade swagger gen 挂载到一级命令 ./hade swagger并且挂载到framework/command/kernel.go 中。这个步骤,前面几章中都已经重复做过了,这里就不赘述了。

挂载好之后,我们尝试运行一下:

可以看到,日志信息打印非常详细,包括去哪个目录查找、最终生成哪些文件。

查看app/http/swagger目录确实最终生成了docs.go、swagger.json、swagger.yaml 这三个文件:

启动swagger-ui

有了swagger生成文件了应该怎么使用它呢还是先设想我们希望的是能在启动服务的时候同时启动一个swagger-ui页面给接口使用人员来查看服务接口并且他们可以直接在这个页面进行接口调用。

到这里相信你已经想到了,在启动服务的时候,增加一个打开swagger-ui页面路由就可以达到我们的目的了。还是查看swaggo这个项目官方文档中有一段如何将swaggo结合Gin来生成路由的方法说明正好我们框架的路由用的Gin所以就考虑使用这个方法来开辟一个swagger-ui路由。

swaggo开发了一个gin-swagger 中间件来为Gin框架增加路由设置。怎么使用它呢看官方文档的这个例子

package main

import (
   "github.com/gin-gonic/gin"
   docs "github.com/go-project-name/docs"
   swaggerfiles "github.com/swaggo/files"
   ginSwagger "github.com/swaggo/gin-swagger"
   "net/http"
)
// @BasePath /api/v1

// PingExample godoc
// @Summary ping example
// @Schemes
// @Description do ping
// @Tags example
// @Accept json
// @Produce json
// @Success 200 {string} Helloworld
// @Router /example/helloworld [get]
func Helloworld(g *gin.Context)  {
   g.JSON(http.StatusOK,"helloworld")
}

func main()  {
   r := gin.Default()
   docs.SwaggerInfo.BasePath = "/api/v1"
   v1 := r.Group("/api/v1")
   {
      eg := v1.Group("/example")
      {
         eg.GET("/helloworld",Helloworld)
      }
   }
   r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
   r.Run(":8080")
}

gin-swagger原理分析

我们着重注意下这几行:

package main

import (
   ...
   docs "github.com/go-project-name/docs"
   swaggerfiles "github.com/swaggo/gin-swagger/swaggerFiles"
   ginSwagger "github.com/swaggo/gin-swagger"
   ...
)
...

func main()  {
  ...
   r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
   ...
}

首先import的三个文件分别是做什么用的必须要理解清楚。

swagger-ui是一个HTML + JSON接口的页面那么里面分别有动态内容和静态内容静态内容包括HTML、JS、CSS、png 等动态内容包括swagger JSON化的结构。也就是说我们现在的目标是要创建一个包含HTML+JSON的服务,如何做呢?

一种方法当然是将这些静态文件直接放在项目中,在服务启动的时候,使用读取文件的方式并返回这些静态文件提供服务。但是这就要求在发布的时候,这些静态文件,必须同时被带着上线,它们成为了服务必须的一部分,特别是作为一个类库提供的时候,如果要求使用者必须带着库的静态文件,是非常不方便的。

而这里gin-swagger采用了另外一种更为极致的做法将这些静态文件代码化嵌入到go代码中比如让一个变量返回HTML的内容我们在提供获取HTML页面的服务时直接将变量返回就可以了。

这里代码第6行的github.com/swaggo/gin-swagger/swaggerFiles这个库就做了这个事情它将swagger-ui的所有HTML、JS、CSS、png 文件都变化成为了go文件并且作为HTTP服务提供出来。其中的swaggerfiles.Handler 就是实现了net/http 的 HandlerFunc 接口

而另外一部分动态JSON接口返回的是具体的swagger JSON化的内容。这个怎么获取呢

是通过前面我们说的swaggo 生成的几个文件中的doc.go 文件来获取的在例子中就是第5行引入的 github.com/go-project-name/docs 库它的原理就是生成doc 全局变量,并且通过 ReadDoc() 方法来提供JSON的内容读取。

现在有了动态JSON接口和静态文件服务接口如何集成到Gin的Engine里呢

要一个中间件就行了就是第7行 import中引入的 github.com/swaggo/gin-swagger 库。它通过创建一个Gin的中间件将动态和静态的请求都承接起来静态请求就请求到swaggo/files库动态请求就请求到docs库中。

所以在路由中,我们*创建一个路由/swagger/any就可以获取swagger-ui 并且读取swaggo 创建的doc.go 文件内容了

r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))

如何集成

gin-swagger的原理理解清楚了如何集成进入我们框架呢

gin-swagger本质就是一个Gin中间件。集成Gin的中间件我们只需要拷贝这个中间件源码并且将中间件的 github.com/gin-gonic/gin 替换为hade框架的gin地址github.com/gohade/hade/framework/gin就可以了。

所以把这个中间件放在framework/middleware/gin-swagger 下。存放完之后的目录截图,你可以对比检查一下:

把gin-swagger中间件处理完就剩下将路由存放到我们的app业务路由中去了。

这里再设计一个小细节。毕竟我们其实并不希望线上服务也提供这么一个swagger路由也就是说只希望swagger-ui在测试和开发环境使用所以可以在配置文件app.yaml 中有这么一个配置项:

swagger: true

来表示是否开启这个swagger路由。
那在应用路由中如何获取到这个配置呢之前应用路由中的参数只有一个gin.Engine。我们需要为gin.Enigne增加一个获取服务容器的接口GetContainer()。来修改framework/gin/hade_engine.go

// GetContainer 从Engine中获取container
func (engine *Engine) GetContainer() framework.Container {
   return engine.container
}

现在就是真正的万事俱备了我们来改造应用路由app/http/route.go。

  • 首先要引入gin-swagger提示的三个import。

这里我将最后一个docs对应的import放在了同级目录的app/http/swagger.go文件中。

我是这么考虑的,docs.go是我们用命令行生成的而生成的时候swagger的全局说明配置是放在swagger.go中的,所以这两个文件关系更为紧密,比较适合放在一起。

app/http/swagger.go文件信息

// Package http API.
// @title hade
// @version 1.1
// @description hade测试
// @termsOfService https://github.com/swaggo/swag
...
package http

import (
    _ "github.com/gohade/hade/app/http/swagger"
)

回到app/http/route.go中。我们引入了另外两个库并且先判断app.swagger配置项是否为true如果为true则开启swagger路由。

app/http/route.go代码实现你应该能很容易写出来。要点刚才都详细讲过了

package http

import (
   ...
   ginSwagger "github.com/gohade/hade/framework/middleware/gin-swagger"
   "github.com/gohade/hade/framework/middleware/gin-swagger/swaggerFiles"
   ...
)

// Routes 绑定业务层路由
func Routes(r *gin.Engine) {
   container := r.GetContainer()
   configService := container.MustMake(contract.ConfigKey).(contract.Config)

   ...

   // 如果配置了swagger则显示swagger的中间件
   if configService.GetBool("app.swagger") == true {
      r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
   }

   ...
}

到这里我们就完美将swagger-ui集成进入我们服务了。

验证

最后做一下验证。先用 ./hade swagger gen 命令生成我们要的docs.go文件

再使用 ./hade build self 将生成的docs.go 打包编译进./hade命令启动服务 ./hade app start

浏览器打开地址 http://localhost:8888/swagger/index.html 可以看到整个swagger-ui界面

并且点击某个接口的 execute 按钮,可以真实地调用这个接口,返回返回数据,进行调试。非常方便:

验证完成!

今天所有代码都保存在GitHub上的geekbang/23分支了。附上目录结构供你对比查看。

小结

这一节课我们其实就做了一件事情将swagger融合进入hade框架。

我们依赖swag项目和gin-swagger中间件成功地将swagger放到hade框架中之后使用一个配置能同时启动hade后端服务和swagger前端调试工具自动生成一个可以查看接口、可以调用执行的页面。相信在实际工作中开发过后端接口的同学就知道这个工具是有多实用。

当然熟练使用swagger以及熟练编写swagger的代码注释需要对swagger的规则和swag的注释定义有一定了解这个需要你花时间去掌握。但是相信我虽然写swagger注释有一些繁琐但是它能节省大量你和前端同学联调的时间。

思考题

我之前在一个项目中使用swagger的JSON文件自动生成了项目的接口word说明文档。不知道你在实际工作中是如何使用swagger的呢能分享一下你/你们公司使用swagger的一些经历么

欢迎在留言区分享你的思考。感谢你的收听,如果觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。我们下节课见~