图1. 名为openapidemo的应用程序的Swagger UI局部视图,如下所示。屏幕截图和API定义均由作者提供。渲染使用了go-openapi库。
目录• 概要
• 动机
• 生成器和OpenAPI版本2.0与3.0的对比
• 应用高层架构
• 项目目录结构
• 安装和使用Go-Swagger生成工具
• Swagger文件介绍
• 定义GET HostInfo
• 实现GET HostInfo处理程序
• 管理NBI的再生过程
• 实现一个简单的电话簿
◦ 定义电话簿API
◦ 实现电话簿API
• 实现一个与JSONPlaceholder服务器交互的客户端
◦ 指定JSONPlaceholder的部分API
◦ 实现一个JSONPlaceholder客户端
◦ 指定支持JSONPlaceholder的NBI
◦ 实现支持JSONPlaceholder的NBI
• 运行示例应用
• 参考资料
这篇文章解释了如何使用 Go 语言实现 RESTful 网络服务。它主要介绍如何使用 Swagger(即 OpenAPI 规范)文件指定北向接口 (NBI)。该文件将用于生成支持 API 端点 HTTP 方法的代码,以及 API 需要的数据类型。
我们还会讲解如何实现一个客户端和一个南向接口(SBI)。服务器将利用这个客户端与另一个服务器进行交流。大部分客户端的代码是从定义SBI API的Swagger文件中自动生成的。
本文中展示的代码来自一个小型但功能完备的 Go 应用,用于展示 swagger 功能并以便测试和探索。为了更好地理解代码片段的完整上下文,建议下载该应用的代码库并在您喜欢的 IDE 中进行查看。
代码遵循 MIT 开源许可,并可以从以下 Git 仓库获取:https://gitlab.com/adrolet/openapidemo。
这个演示应用程序已经可以编译和运行了。你只需要最新的 Go SDK,可选地,你可以使用 make
来简化你的工作。
在当今的大型软件系统中,我们通常有一组松散耦合的应用程序,它们通过某种协议互相传递命令和数据。一些常见的协议包括:REST,gRPC/Protobuf,和 GraphQL。REST消息的负载通常是JSON格式,但也可以使用XML或其他格式。
这样的应用(比如服务器和微服务)可以很容易地通过 shell 命令(CLI)启动,或者运行在 Docker 容器中,或者部署在 Kubernetes 集群里。
这篇文章展示了我们怎样利用OpenAPI Specification
来定义一个两个应用程序之间的接口,REST API,。
OpenAPI 规范最初被称为 Swagger。随着时间的推移,一个新的支持组织——在 Linux 基金会的支持下——成立了,以支持这个规范,[OpenAPI Initiative](https://www.openapis.org)
。其简短的演变历史可以在维基百科上找到,链接为 Wikipedia。
OpenAPI 如下定义规范:
什么是OpenAPI?
OpenAPI规范提供了一种描述HTTP API的标准方法。
这使得人们可以理解一个API是如何工作的,多个API如何一起工作,生成客户端代码、创建测试和应用设计标准等等。
这允许人们理解和应用更多功能,例如协同多个API工作、生成客户端代码、创建测试、应用设计标准等等。
Swagger 最初定义了一个 Swagger UI。这个网络界面工具提供了一个交互式的用户界面,用户可以在此“尝试”并使用 API 进行开发。如本文顶部的图 1 所示,这是该界面的一个示例。
本文通过一个名为 openapidemo
的小型 Go 程序来展示如何实现北向和南向接口(简称北向和南向)。这些接口的规范将定义在文件里,这些文件通常被称为 swagger
文件。然后,这些文件将用来生成每个 API 端点的底层 HTTP 代码以及表示客户端与服务器间交换的数据类型的 Go 类型。
这种方法的一个好处是,API以一种与任何实现语言无关的方式来定义。这些规范说明就成为客户端与服务器之间的契约。
有时,一个团队会开发一个服务器,并可能开发一个客户端作为参考实现或测试工具。任何外部团队如果对使用该服务器的服务感兴趣,也可以开发额外的客户端。此外,实现所用的语言可以由每个团队自行选择,API 规范语言是无关的。服务器和客户端所使用的实现语言不必相同。只要符合合约要求即可。从同一个规格文件生成代码可以确保客户端和服务器的数据模型实现保持同步。
生成工具和OpenAPI版本2.0和3.0在本文中,我们只讨论在 Go 语言中的实现。代码将由一个名为 swagger
的生成器程序生成。Swagger 的代码可以在以下 git 仓库找到:
https://github.com/go-swagger/go-swagger
假设你需要用其他语言编写系统中的某些部分(例如南向的客户端)。在这种情况下,你可以使用相同的规范文件,并通过其他生成器生成目标语言相应的代码,使句子更加流畅。
例如,你可以使用Codegen
或者它的某个分支。下面的各个项目都支持数十种服务器端语言,以及更多的客户端语言。
原来的 Swagger Codegen
是由 SmartBear 提供支持的。
Codegen 版本 2.X 的链接:https://github.com/swagger-api/swagger-codegen
Codegen 版本 3.X 的链接:https://github.com/swagger-api/swagger-codegen/tree/3.0.0
还有一个由社区支持的 Codegen
分支项目叫 OpenAPI Generator
。OpenAPI Generator
有更多的近期更新。
https://github.com/OpenAPITools/openapi-generator (GitHub 仓库链接)
目前(可能永远),go-swagger 项目支持的版本是 2.0,不支持 3.x。因为支持 3.0 版本需要大量的工作,而没有人有时间和精力去做这件事。这完全可以理解的。
关于 3.0 的支持可以在 GitHub 上 go-swagger 的问题 1122 中找到讨论:
支持 Open API 3.0 规范
:https://github.com/go-swagger/go-swagger/issues/1122
对于我创建的所有服务来说,2.0版本的限制从来不曾成为问题。因此,2.0版本对我来说很合适,也可能会很适合你。如果你的项目必须使用3.0版本,你可能需要考虑一下上面列出的其他生成器。我没有使用过它们的经验,所以你的使用体验可能会有所不同。
每个OpenAPI版本的详细规范可以在这里查看:
版本2.0:https://swagger.io/specification/v2/
版本3.x(或最新版本):https://spec.openapis.org/oas/latest.html
我们将通过一个叫做openapidemo的小程序学习如何提供服务和查询REST API。
这个应用程序包含3个组件,分别支持REST API的不同端点。
如下:
- 主机信息。
它只是提供运行该应用程序的主机的一些基本信息。(GET) - 电话簿。
一个简单的电话簿,可以存储有关人员、地址和电话号码的条目。我们可以查询所有条目、单个条目,并在电话簿中添加新的条目。(GET, PUT) - 用于JsonPlaceHolder外部服务的客户端。
JsonPlaceHolder是一个存储称为posts
的对象的服务平台。
我们可以获取帖子标题、特定用户的帖子,并模拟添加新的帖子。(GET, POST)
这里是我们服务器北向接口(Northbound接口)公开的端点。
-
GET http://<server:port>/openapidemo/主机信息
-
GET http://<server:port>/openapidemo/电话簿
-
PUT http://<server:port>/openapidemo/电话簿
-
GET http://<server:port>/openapidemo/电话簿/{first}/{last}
-
GET http://<server:port>/openapidemo/占位符帖子标题
-
GET http://<server:port>/openapidemo/占位符帖子按用户/{user}
- 发送 POST http://<server:port>/openapidemo/占位符帖子
swagger工具默认也添加了两个端点。
第一个端点提供NBI的swagger规范,格式为JSON。
第二个端点提供swagger UI,这是一个学习和尝试NBI的好地方。
- GET http://<server:port>/swagger.json
- GET http://<server:port>/openapidemo/docs
应用程序被组织为多个层次。
- 这里,在顶部,NBI 接受外部的 REST 呼叫。
- 这里,在底部,是一些数据提供者。包括操作系统呼叫、内部数据存储和一个 REST 客户端来访问另一个 Web 服务。
- 这里,在中间,是一组处理程序。它们提供了 NBI API 的行为,并作为顶部和底部层之间的粘合层。
图2如下展示了应用的高级架构。
图2. openapidemo的高层架构图。插图由作者提供。
项目文件夹结构应用的目录结构包括开发人员写的代码还有swagger
工具生成的代码。
在示例应用中,我们选择在路径 nbi/gen
和 sbis/X/gen
下生成的代码。在有多个客户端的应用程序中,X 是一个反映客户端可以访问的外部服务器名称的标识符。这种命名方式在我的团队中用于大型电信控制器项目时效果很好。你可以自由创建你自己的命名和结构方式。
这些非生成的目录包含了你的自定义代码。至少你需要一个包,例如这里的handlers
包,来实现每个HTTP方法的功能。你也可以添加其他包,比如自定义的日志包或数据库访问包。它可以根据你的需求简单或复杂。
你还需要将SBI生成的代码片段与其他代码结合起来,以便调用外部服务。在演示应用程序中,这是在sbis/jsonplaceholder/jphClient
包中进行的。
注意,包含主函数的 main.go
文件是由 swagger
生成的。服务器生成命令(将在下一节解释)会在 { -t 标志值}/cmd/{ -A 标志值}
生成主文件。例如,生成的文件路径为 nbi/gen/server/cmd/openapidemo-server/main.go
。
swagger文档表明你不应该修改生成的文件(但有一个例外可以修改)。每次生成服务器后,你的修改将被覆盖。实际上,有时你可能需要自定义main.go
文件,以及其他一些文件(如nbi/gen/server/restapi/server.go
),这可能是因为你想要自定义初始化或添加命令行参数。如果你决定这样做,你需要调整你的开发流程,以便在每次生成服务器后,通过手动或自动化脚本重新注入这些自定义内容。
克隆(或再生)之后,仓库结构看起来会是这样的。这里只展示了主要的目录和一些关键文件。
.
|-- LICENSE
|-- Makefile
|-- README.md
|-- docs # 图片和幻灯片。
|
|-- handlers # 连接NBI与底层的粘合代码。应用的核心部分。
|
|-- nbi
| |-- gen # 生成的代码。NBI桩代码和数据类型。
| | `-- server
| | |-- cmd
| | | `-- openapidemo-server
│ │ │ └── main.go # 包含应用程序主函数。
| | |-- models # 包含我们操作使用的数据类型。
| | `-- restapi
│ │ |-- operations # 包含HTTP请求和响应使用的数据类型。
| | `-- configure_openapidemo.go # 连接NBI和handlers的文件(唯一修改过的生成文件)。
| |-- nbi-swagger.yaml # 应用程序API的规范(NBI生成器输入)。
| `-- old-configs # 一个辅助目录用于管理重新生成的configure_openapidemo.go。
|
|-- sbis
| `-- jsonplaceholder # 与jsonplaceholder服务交互的客户端。
| |-- gen # 生成的桩代码和类型,用于构建客户端。
| | |-- client
│ │ │ └── operations
| | `-- models
| |-- jphClient # 手动编写的客户端代码,调用生成的代码。
| `-- jsonplaceholder-swagger.yaml # jsonplaceholder服务API的规范。
|
`-- testdata # 可用于测试NBI的JSON负载。
|-- README.md
|-- good-woman-quote.json
`-- lennon.json
注意,在RFC 9110, HTTP 语义中,像 GET、PUT、POST 和 DELETE 这样的术语被称为 方法
。在 Swagger 规范和生成的代码中,通常会看到 操作
这个词。请将 操作
和 (HTTP) 方法
视为同义词,这两个词在本文中可以互换使用。
该演示应用程序依赖于由两个swagger定义文件生成的代码。
如果你只是想原样使用这款应用,而不需要做任何修改的话,你只需要运行 go install
或 go run
命令(后面的解释会更详细)。仓库里已经包含了所有生成的文件和需要的手动配置。
总有一天,你可能会想自己写一个应用程序,或者修改演示应用的NBI(网络业务接口)或SBI(系统业务接口)进行实验。这将意味着你需要修改其中一个或两个文件nbi/nbi-swagger.yaml
和sbis/jsonplaceholder/jsonplaceholder-swagger.yaml
。一旦你修改了这些文件中的任何一个,某些Go文件就需要重新生成。
本文中,我们使用swagger
工具从swagger定义文件生成API的代码。
如果你正在读这篇文章,那我猜你是一名Go开发者,并且已经设置好了Go环境(例如,已经安装了Go SDK,设置了$GOPATH)。对你来说,最简单的方法就是使用go install
来获取swagger可执行程序。如果你不想自己动手编译生成器,本节底部的链接解释了其他选项(例如,预编译的可执行程序,Docker镜像)。
$ go install github.com/go-swagger/go-swagger/cmd/swagger@latest # 安装最新版本的swagger工具
这会将工具的代码复制到 $GOPATH/pkg/mod/github.com/go-swagger/go-swagger@{someVersion}
,并将其构建并保存到 $GOPATH/bin
。
使用openapidemo项目中的Makefile生成API代码是一个最简单的方式。
你可以运行命令 make generate
生成 NBI 和 SBI 的代码,或者如果你只想生成其中一个目标,你可以选择性地运行特定命令,比如 make generate-nbi
或 make generate-sbi
。在你继续阅读本文,了解生成命令的所有影响之前,请不要急于运行这些生成命令。
Makefile执行的命令如下所示:
使用 make
命令执行 swagger generate server
,生成支持NBI的应用程序(默认包括main.go文件)。
# 要查看 swagger generate server 的所有选项,请运行
$ swagger generate server -h
# swagger generate server -f 在 $(NBI_DIR)/nbi-swagger.yaml -t 在 $(NBI_SERVER_DIR) -A 服务名 $(SERVICE_NAME)
swagger generate server -f nbi/nbi-swagger.yaml -t nbi/gen/server -A openapidemo
子命令 generate server
用于生成服务器应用所需的所有文件。
-f
参数指定了 swagger NBI 规范文件的位置。
-t
参数指定了生成代码的目标目录。
-A
参数指定了要生成的应用名称。
有些人也会使用 --exclude-main
标志。使用这个标志的话,文件 nbi/gen/server/cmd/openapidemo-server/main.go
将不会被生成。开发者需要自己提供这个文件。当你熟悉了这一点后,这可以让你更好地控制应用程序的启动方式。在某些情况下,我用它来添加更多的 CLI 标志和自定义初始化行为。
生成用于连接到另一服务器所需的Go包,即示例应用程序中的客户端代码,make
命令会运行 swagger generate client
。也就是说,make
使用 swagger generate client
。
# 要查看 swagger 生成客户端的所有选项,请尝试运行
$ swagger generate client -h
# swagger generate client -f $(SBI_JSONPLACEHOLDER_DIR)/jsonplaceholder-swagger.yaml -t $(SBI_JSONPLACEHOLDER_DIR)/gen -A jsonplaceholder
swagger generate client -f $(SBI_JSONPLACEHOLDER_DIR)/jsonplaceholder/jsonplaceholder-swagger.yaml -t $(SBI_JSONPLACEHOLDER_DIR)/jsonplaceholder/gen -A jsonplaceholder
# 此命令将会生成 json 地址占位符的客户端代码
标志具有与服务器命令相同的含义。需要注意的是,-f 参数指向 SBI 规范文件。对于客户端,-A 标志仅反映在某些文件名和内容上。这个名称与服务器的名称完全无关。如果您需要多个客户端来支持您的应用程序,请使用唯一且具有意义的名称。通常,为每个客户端使用一个 Swagger 文件可以保持包结构的解耦和易于理解。
以下链接提供更多关于swagger
生成工具的信息。
-
go-swagger 产品文档页面:https://goswagger.io/go-swagger/
-
从源代码进行安装:https://goswagger.io/go-swagger/install/install-source/
- go-swagger 代码库:https://github.com/go-swagger/go-swagger
我们的服务器API描述保存在swagger文件里,文件路径为:nbi/nbi-swagger.yaml
在顶部,我们声明一组影响所有端点的字段集,除非在端点或操作/方法级别重新设定。这些字段集定义了文件/服务器的相关info
、支持的scheme
(通常是HTTP和/或HTTPS),以及服务器接受或返回的数据格式。
我们还声明了所有端点共用的路径前缀为basePath
。因此,所有示例应用的端点URL都将以此http://{hostIp}:{port}/openapidemo/
开头。例如,这个URL会作为所有端点的基础路径。
这里就是NBI swagger文件顶部的内容大概如下。
swagger: "2.0"
info:
title: OpenApi/Swagger 简单演示应用
description: |
一个简单的应用,演示如何使用 go-swagger 生成 NBI 和 SBI。
version: "0.0"
协议:
- http
支持:
- application/json
返回:
- application/json
basePath: "/openapidemo"
获取主机信息的GET请求
现在我们来定义我们第一个方法 GET HostInfo 接口。
这个简单的请求不需要输入,它会从主机操作系统获取几个信息。我们的目标是获取四个信息:主机名、CPU架构(例如amd64或x86)、操作系统名称(例如macOS,基于darwin内核)以及CPU的核心数。
我们通过在 paths
数组属性中定义一个端点路径条目,并在 definitions
数组属性中定义名为 HostInfo
的数据类型来明确这一点。
这两个新条目如下:
paths:
"/host-info":
get:
description: 获取运行此服务器的主机的主机名、CPU架构、操作系统名称和CPU数量
# operationId 影响生成代码中的名称
operationId: "GetHostInfo"
responses:
# 正常成功情况。如果需要,也可以添加其他成功的2xx代码
'200':
description: 返回主机名和服务器主机的CPU核心数
schema:
$ref: '#/definitions/HostInfo'
# 仅支持服务器错误。例如,如果这是用户错误(如无效路径或数据),则可以返回4xx错误
# 生成的swagger代码还将添加其自己的错误(例如404)
'500':
description: 返回表示发生错误的字符串
schema:
type: string
definitions:
HostInfo:
type: object
properties:
host-name:
type: string
architecture:
type: string
os-name:
type: string
num-cpus:
type: integer
新的入口路径是相对路径,OpenAPI 规范如下:
字段名必须以斜杠(/)开头。路径会追加到基础路径(basePath
),从而构建完整的URL。
/host-info
这个条目指定了 HTTP 方法,这里使用 GET 方法。还指定了 operationId
,它应该唯一且有意义,因为这个标识符将用于命名各种生成的代码部分,例如处理器函数、HTTP 参数对象以及 HTTP 返回代码枚举。description
字符串将在 Swagger UI 中展示。
response
部分列出了您可以在此端点方法中通过Go代码返回的所有HTTP状态码。需要注意的是,底层HTTP协议还可以返回其他状态码,比如404。
因为这是一个 GET 请求,会返回一些数据。为了正确解析响应,我们为每个返回码指定一个 schema
,这个 schema
表示返回的数据类型。这可以是一个基本类型,比如字符串或者数字,也可以是你定义的任何类型。
定义可以直接在schema
下定义,但通常建议将所有数据类型定义放在definitions
数组中。这样可以在整个swagger文件中重复利用。当某种类型在使用时未定义时,会使用带有JSON Pointer的$ref
来引用类型定义。$ref
指针开头的#
表示当前文件的根目录。
最后,在 definitions
数组中,我们添加了一个 HostInfo 类型。在这个情况下,它是一个简单的属性集合。在 Go 中,这会被表示为一个结构体。
你可以在这里学习如何在 Swagger 规范版本 2 中定义自己的数据类型,网址是:https://swagger.io/specification/v2/
如上所述,我们可以将这个文件输入给 go-swagger 生成器。这将生成一个叫做 HostInfo
的 Go 类型,即一个一般用于序列化成 JSON 格式的 Go 结构体。
type 主机信息 struct {
Architecture string `json:"architecture,omitempty"`
主机名 string `json:"host-name,omitempty"`
CPU数量 int64 `json:"num-cpus,omitempty"`
操作系统名称 string `json:"os-name,omitempty"`
}
获取HostInfo处理器
生成NBI之后,项目结构里会创建许多新的Go文件。
对我们来说,一个关键的文件是这个:nbi/gen/server/restapi/configure_openapidemo.go
。
这是swagger
应用唯一需要我们修改的文件。因此,如果文件不存在(例如第一次使用时),swagger
会自动生成该文件,但在文件已存在时再次生成时不会修改它。关于这一点,我们稍后再详细说明。
下面是从文件 configure_openapidemo.go
中提取的 configureAPI
函数生成的代码段。这段代码展示了在 swagger 文件中定义的所有 HTTP 方法的行,而不是我们现在正在了解的 GetHostInfoParams
方法。为了使代码更简洁,已删除所有注释。
func 配置API函数(api *operations.OpenapidemoAPI) http.Handler {
api.ServeError = errors.服务错误
api.使用SwaggerUI()
api.JSONConsumer = runtime.JSONConsumer()
api.JSONProducer = runtime.JSONProducer()
if api.AddPhoneBookEntryHandler == nil {
api.AddPhoneBookEntryHandler = operations.AddPhoneBookEntryHandlerFunc(func(params operations.AddPhoneBookEntryParams) middleware.Responder {
return middleware.NotImplemented("操作 '添加电话簿条目' 尚未实现")
})
}
if api.AddPostObjectHandler == nil {
api.AddPostObjectHandler = operations.AddPostObjectHandlerFunc(func(params operations.AddPostObjectParams) middleware.Responder {
return middleware.NotImplemented("操作 '添加帖子对象' 尚未实现")
})
}
if api.GetHostInfoHandler == nil {
api.GetHostInfoHandler = operations.GetHostInfoHandlerFunc(func(params operations.GetHostInfoParams) middleware.Responder {
return middleware.NotImplemented("操作 '获取主机信息' 尚未实现")
})
}
if api.GetPhoneBookHandler == nil {
api.GetPhoneBookHandler = operations.GetPhoneBookHandlerFunc(func(params operations.GetPhoneBookParams) middleware.Responder {
return middleware.NotImplemented("操作 '获取电话簿' 尚未实现")
})
}
if api.GetPhoneBookEntryHandler == nil {
api.GetPhoneBookEntryHandler = operations.GetPhoneBookEntryHandlerFunc(func(params operations.GetPhoneBookEntryParams) middleware.Responder {
return middleware.NotImplemented("操作 '获取电话簿条目' 尚未实现")
})
}
if api.GetPostTitlesHandler == nil {
api.GetPostTitlesHandler = operations.GetPostTitlesHandlerFunc(func(params operations.GetPostTitlesParams) middleware.Responder {
return middleware.NotImplemented("操作 '获取帖子标题' 尚未实现")
})
}
if api.GetPostsByUserHandler == nil {
api.GetPostsByUserHandler = operations.GetPostsByUserHandlerFunc(func(params operations.GetPostsByUserParams) middleware.Responder {
return middleware.NotImplemented("操作 '通过用户获取帖子' 尚未实现")
})
}
api.服务器关闭前 = func() {}
api.服务器关闭 = func() {}
return setupGlobalMiddleware(api.Serve(setupMiddlewares))
}
每个 HTTP 方法都用一个 if
块包裹起来。这个 if
包裹在我们的应用程序中无法使用,因此必须移除。请查阅 README.md 以了解这块代码的历史背景。
在 if
部分,我们看到格式通常是这样的:等等。
api.SomeHandler = operations.SomeOpHandlerFunc(
func(someInputDataType) middleware.Responder {
// 这个函数处理某种输入数据类型,并返回一个未实现的响应。
return middleware.NotImplemented("text")
}
)
这些代码块将 api
对象的 SomeHandler
属性设置为直接定义的函数。
如果你打开你最喜欢的IDE并分析这些代码块,分析在NBI operations
包中的生成代码,你会发现一种巧妙地利用类型定义函数和接口的方式。
你可能得暂停一下,去喝一杯咖啡仔细看看小字,但关键的是你要明白这一点。
内联函数(inline function)接受从HTTP请求中接收到的输入数据,即HTTP请求参数,并返回一个包含HTTP响应负载的对象。
更详细的解释是,operations.SomeOpHandlerFunc
是一个定义了函数签名的类型。它的存在是为了对内联函数进行类型转换(通常被错误地称为“强转”)。代码生成器在 operations
包中还定义了一个 SomeOpHandle
接口。该接口定义了名为 Handle
的方法签名。生成器在 operations.SomeOpHandlerFunc
类型上实现了 Handle
方法。ServeHTTP
Go 方法(为每个 HTTP 方法生成)会调用 Handle
方法。
如果看不懂最后一句,继续往下读。反正这些内容都是自动生成的。
这一代创建了一个内联函数,该函数简单地调用 middleware.NotImplemented
。这只是一个返回HTTP 501(未实现)状态码的简单中间件处理器。
这可以让你的服务器 API 在实现完整行为前就进行早期测试。
这个简单的处理函数需要替换为一个有用的处理函数,以提供我们期望从服务器获得的行为。在演示应用里,这些“真正的”处理函数都在开发者编写的 handlers
包中定义。
处理 GET HostInfo 方法的处理程序定义在如下文件中:handlers/handlers.go
。
以下展示的是获取主机信息的 GET 请求处理器的代码。
func GetHostInfo(params operations.GetHostInfoParams) middleware.Responder {
host, _ := os.Hostname()
numCpu := runtime.NumCPU()
arch := runtime.GOARCH
rtOs := runtime.GOOS
// 这两个值可能仅在 Go 已安装在主机上时才有效!
// 除非这些值是在编译时确定的
info := models.HostInfo{}
info.HostName = host
info.架构 = arch
info.操作系统名称 = rtOs
info.NumCpus = int64(numCpu)
return operations.NewGetHostInfoOK().WithPayload(&info)
}
在这个简单的 GET 请求里,我们不会从传入的参数中读取任何传入的数据。
这个处理程序的核心是调用os
和runtime
包来收集关于运行服务器的主机信息。收集到的数据将用于填充一个models.HostInfo
对象。如前一节所述,该HostInfo结构类型是根据我们的NBI swagger文件生成的。
operations.NewGetHostInfoOK
是 GetHostInfoOK
类型的构造函数。GetHostInfoOK
类型包含一个名为 WithPayload
的方法,此方法用于设置调用者期望 GET 方法返回的数据。它还包含一个名为 WriteResponse
的方法,用于将负载和状态码 200(如 swagger 文件所述)返回给发送 GET 请求的客户端。
现在我们有了处理程序的真正实现,需要在configure_openapidemo.go文件中调用它。
下面的代码示例展示了支持获取主机信息的功能所需的关键改动。
import (
...
// 导入了我们实际的处理函数
"gitlab.com/adrolet/openapidemo/handlers"
...
)
func configureAPI(api *operations.OpenapidemoAPI) http.Handler {
...
if api.AddPostObjectHandler == nil {
api.AddPostObjectHandler = operations.AddPostObjectHandlerFunc(func(params operations.AddPostObjectParams) middleware.Responder {
return middleware.NotImplemented("还未实现的操作 operations.AddPostObject")
})
}
// 更新的部分 - 注意这里移除了 `if` 语句的包裹
api.GetHostInfoHandler = operations.GetHostInfoHandlerFunc(func(params operations.GetHostInfoParams) middleware.Responder {
return handlers.GetHostInfo(params)
})
if api.GetPhoneBookHandler == nil {
api.GetPhoneBookHandler = operations.GetPhoneBookHandlerFunc(func(params operations.GetPhoneBookParams) middleware.Responder {
return middleware.NotImplemented("还未实现的操作 operations.GetPhoneBook")
})
}
...
}
上述代码仅展示了到目前为止本文所讨论的单一方法的改动。查看仓库的话,你会发现,configure_openapidemo.go
文件中所有服务器支持的方法都已进行了更新。
请注意,您还可以在此文件中进行更多自定义设置,以修改 HTTP 请求的处理方式。例如,您可以在 setupMiddlewares
和/或 setupGlobalMiddleware
函数中添加拦截处理器(链式处理器)。一个有趣的添加项可以是一个日志器,记录每个收到的请求的信息。这对于检测 DDoS 攻击或入侵非常有用。您还可以添加一个包装处理器来处理方法处理程序内部引发的 panic,返回错误响应,并继续运行程序。如果不想因坏数据而导致服务器崩溃,这会非常有用。
开发通常是一个逐步的过程,所以你可能会从定义NBI API的某些部分开始。随着项目的推进和试验的进行,你可能需要添加或删除端点和方法,或者调整某些数据类型。当你更新swagger文件时,你需要重新生成相关代码。
如之前所提到的,swagger
工具只会生成一次 configure_openapidemo.go
文件。如果文件已经存在,该工具不会重新生成。所以如果你修改了 API,你需要自己找到配置文件中需要更改的部分,并更新所有类型名称。一旦你对这个系统有一定熟悉度,这其实并不难。然而,这种方法仍然容易犯错,比如输入错误的类型名称或不遵循命名约定。
我认为更容易操作的一个替代方法是获取一个全新且完全反映了最新API定义的文件。为此,你需要从gen
目录中复制configure_openapidemo.go
文件,删除原始文件后调用生成器。这会给你一个最新的文件,但你所有的定制内容都会被丢失。为了减轻你的认知负担,Makefile中有一个内部目标叫做backup-nbi-config
。这个目标会将配置文件移动到非版本控制的目录nbi/old-configs
中,移动过程中,文件会被重命名,在配置文件名前加上一个数字。目标generate
和generate-nbi
会调用这个内部目标backup-nbi-config
。
openapidemo/nbi/old-configs % ll
-rw-r--r-- 1 adrolet staff 3488 2022年09月19日 configure_openapidemo_0.go
-rw-r--r-- 1 adrolet staff 4339 2022年02月12日 14时10分 configure_openapidemo_1.go
-rw-r--r-- 1 adrolet staff 3969 2022年02月12日 15时07分 configure_openapidemo_2.go
-rw-r--r-- 1 adrolet staff 3634 2022年02月14日 23时14分 configure_openapidemo_3.go
通过这样做,你可以重新生成配置文件,使用你的IDE中的“差异比较工具”更轻松地将自定义内容回退到最新的配置文件中(未更改的部分),然后手动处理新添加或修改的部分。我个人觉得这样做挺不错的。你可以根据自己的喜好选择合适的工作流程。
在 backup-nbi-config
中用于给文件编号的 shell 命令并不是非常稳健。所以,请小心使用。它期望所有数字都在从0到n的连续范围内,不应存在其他数字。如果文件夹变大,你可能需要移除最旧的文件,仅保留最近的几个文件。如果你这样做(这是个好主意),请确保将文件名末尾的数字重新从零开始编号,一直到库中文件数量减一。如果你喜欢这个机制,并且想改进该 shell 命令,欢迎提交 Pull Request!
我们现在已经看到了实现一个简单API方法所需的全部步骤。接下来,文章将扩展这些知识,包括带输入参数的GET请求和PUT和POST方法。最后,我们会讲如何生成和使用客户端代码来访问外部服务器。
接下来的部分将通过一个简单的电话簿例子来展示如何通过输入数据实现GET请求以及如何实现PUT请求。
电话簿使用 Go 语言中的映射(map)在内存中存储实现。电话簿中存储的条目包含:名字(FirstName)、姓氏(LastName)、电话号码(PhoneNumber)以及地址(Address)。因为它没有持久化存储,每次服务器重启后,内容都会重置为两个固定的条目。
电话簿的核心部分实现位于文件中:handlers/phone_book.go
。
电话簿可以使用三种HTTP方法,比如GET、POST和DELETE等:
请求 GET http://<server:port>/openapidemo/通讯录
请求 PUT http://<server:port>/openapidemo/通讯录
请求 GET http://<server:port>/openapidemo/通讯录/{first}/{last}
以下是我們正在添加的paths
和definitions
条目以支持上述内容:
paths:
"/phonebook":
get:
description: 获取完整的电话簿条目
请参阅 "/phonebook/{name}" 以获取单个条目
operationId: "GetPhoneBook"
responses:
# 正常成功情况。
'200':
description: 返回 PhoneBookEntry 数组。
schema:
type: array
items:
$ref: '#/definitions/PhoneBookEntry'
'500':
description: 返回一个字符串,表示出了什么问题。
schema:
type: string
put:
description: 向电话簿添加一个电话条目
operationId: "AddPhoneBookEntry"
parameters:
# 由于我们的数据是一个复杂类型,必须使用 "in: body"
# "in: formData" 仅适用于原始类型(如字符串、数字、布尔值)或
# 原始类型的数组(这意味着您不能使用一个 $ref 作为 items 的值)。
- in: body
# body 参数的名称仅用于文档目的
name: entry
description: "要添加到电话簿的数据"
schema:
$ref: '#/definitions/PhoneBookEntry'
required: true
responses:
# 正常成功情况。其他成功代码 2xx 也可以添加,如果需要的话。
'200':
description: "返回已添加的条目(即 'body' 中的数据)。"
schema:
$ref: '#/definitions/PhoneBookEntry'
# 要简化起见,我们仅支持服务器错误
# 如果这是用户错误(例如无效路径或数据),也可以是一个 4xx 错误。
'500':
description: 返回一个字符串,表示出了什么问题。
schema:
type: string
"/phonebook/{first}/{last}":
get:
description: 获取单个电话簿条目
operationId: "GetPhoneBookEntry"
parameters:
- in: path
name: first
description: "我们想要的联系人的名字"
type: string
required: true
- in: path
name: last
description: "我们想要的联系人的姓氏"
type: string
required: true
responses:
# 正常成功情况。
'200':
description: 返回单个 PhoneBookEntry。
schema:
$ref: '#/definitions/PhoneBookEntry'
'404':
description: 返回一个字符串,表示没有该名字与姓氏的联系人条目。
schema:
type: string
'500':
description: 返回一个字符串,表示出了什么问题。
schema:
type: string
definitions:
PhoneBookEntry:
type: object
properties:
FirstName:
type: string
LastName:
type: string
PhoneNumber:
type: string
Address:
$ref: '#/definitions/AddressEntry'
AddressEntry:
type: object
properties:
CivicNumber:
type: integer
Street:
type: string
City:
type: string
State:
type: string
Zip:
type: integer
PostalCode:
type: string
上面的片段有点长,不过这里有一些重点内容。
我们有两个API端点: /phonebook/
和 /phonebook/{first}/{last}
。第一个端点支持两种方法:一个GET请求用于检索电话簿中的所有条目,一个PUT请求用于添加一个新的条目。第二个端点在第一个的基础上增加了两个额外的部分,这些部分作为GET方法的输入参数,用于返回电话簿中的单个条目信息。
这些方法一切正常时返回状态码200并返回一些数据,500状态码用于表示任何错误。
访问 /phonebook/{first}/{last}
的 GET 请求也可以返回 404 状态码。这也用于告诉调用者请求的联系人条目(名字加姓氏)在电话簿里不存在。
/phonebook/
的GET请求结构与前面提到的GET HostInfo请求相同,因此我们不再详细讨论。
PUT /phonebook/
是我们第一个需要输入数据的方法。输入数据通过 swagger 所谓的参数传递进来。参数的值可以指定在以下位置:“query”,“header”,“path”,“formData” 和 “body”。Body 和 form 数据通常用于 PUT 和 POST 请求。
当我们对 /phonebook/
进行 PUT 操作时,我们传递的是一个复杂对象(即不是像字符串或整数等简单类型,也不是这些简单类型的数组)。我不记得是在哪里听说或者看到的了,但对于复杂对象来说,只能在 body
位置传递。
每个参数数组中的每个条目(这里只有一个条目)由具有以下属性的对象定义:
- in : 参数的位置,这里的值必须是
body
。 - name : 参数的名称。如果
in
是body
,则该名称将不被使用。 - description : 可选的描述。
- schema : 参数的数据类型的定义,通常是一个
$ref
。 - required : 表示该参数是否为必填项,或者是否可以由服务器默认提供。
有关参数、in
属性以及 body
和 format
格式位置参数的信息,请参阅:https://swagger.io/specification/v2/#parameter-object
请务必看看下方的“固定部分”,以了解“in”字段有哪些可能的值。
需要注意的是,要在服务器上创建一个新的对象,HTTP 标准建议通常使用 POST 方法。而 PUT 一般用于替换或更新已存在的条目。我们这里的目标是理解如何实现 HTTP 方法,因此在这个方面不必过于严格。
电话簿中的最后一种方法是 GET 请求 /phonebook/{first}/{last}
。此方法用于指定我们想要检索的条目中名字和姓氏的输入。因为这些名字是简单的字符串,所以这些参数可以直接放在路径中。在这种情况下,参数名称很重要。每个名称必须与端点路径中花括号内的字符串一一对应。
所有方法成功时会返回若干 PhoneBookEntry
(YAML 数组/Go 切片)或者单个的 PhoneBookEntry
。
生成器会把swagger文件中的数据类型定义转换成以下Go语言的结构体。需要注意的是,PhoneBookEntry
实际上包含了 AddressEntry
,这展示了构建复杂数据结构的能力。
// 来自 nbi/gen/server/models/电话簿条目.go
type PhoneBookEntry struct {
Address *AddressEntry `json:"Address,omitempty"`
FirstName string `json:"FirstName,omitempty"`
LastName string `json:"LastName,omitempty"`
PhoneNumber string `json:"PhoneNumber,omitempty"`
}
// 来自 nbi/gen/server/models/地址条目.go
type AddressEntry struct {
City string `json:"City,omitempty"`
CivicNumber int64 `json:"门牌号,omitempty"`
PostalCode string `json:"邮政编码,omitempty"`
State string `json:"State,omitempty"`
Street string `json:"Street,omitempty"`
Zip int64 `json:"邮政编码,omitempty"`
}
实现电话簿API功能
电话簿的核心部分,即与REST无关的部分,是在handlers/phone_book.go
文件中实现的。它定义了一个包级别的单例变量PhoneBookDb
,这个变量是一个自定义类型,也就是Go语言中的映射,键是一个字符串,值是PhoneBookEntry
对象,键是由名字和姓氏用短划线连接而成的字符串。PhoneBookDb
提供了三种核心方法:一种用于获取所有条目,一种用于获取单个条目,另一种用于添加条目。
在这篇文章中,我们将重点放在NBI中定义的电话簿相关HTTP方法的REST处理代码部分上。这里定义了三个处理程序。
func GetPhoneBook(params operations.GetPhoneBookParams) middleware.Responder{
list := PhoneBookDb.Entries() // 获取所有电话簿条目
return operations.NewGetPhoneBookOK().WithPayload(list)
}
func AddPhoneBookEntry(params operations.AddPhoneBookEntryParams) middleware.Responder{
entry := params.Entry // 要添加的 PhoneBookEntry 对象
// 一个可能的改进:
// 这里可以检测无效数据,并在失败时返回相应的错误码
PhoneBookDb.AddEntry(entry)
return operations.NewAddPhoneBookEntryOK().WithPayload(entry)
}
func GetPhoneBookEntry(params operations.GetPhoneBookEntryParams) middleware.Responder {
entry := PhoneBookDb.GetEntry(params.First, params.Last) // 查找特定的条目
if entry == nil 的话 {
// 404 错误
errMsg := fmt.Sprintf("未找到 %s-%s 对应的条目。", params.First, params.Last)
return operations.NewGetPhoneBookEntryNotFound().WithPayload(errMsg)
}
// 成功返回 (200)
return operations.NewGetPhoneBookEntryOK().WithPayload(entry)
}
获取电话簿从核心获取条目列表,并将其设为响应负载。这与 GET HostInfo 操作相似。
AddPhoneBookEntry 简单地从 HTTP 请求体中的条目传递给核心,然后请求将其添加到地址簿映射中。响应会包含新添加的条目。
GetPhoneBookEntry 尝试返回一个特定的联系人信息。为此,它使用从路径中由生成的代码解析出的两个名字:名字(First)和姓(Last)。
然后处理器请求核心系统检索带有这个名字和姓的条目。如果找到的话,将其放入响应的有效载荷中,并将状态码设置为200。如果没有找到条目,则返回状态码404的响应。在这种情况下,404状态码的响应中,有效载荷是一个提示信息,表示该键无法在电话簿中找到。
请记住,需要将文件 nbi/gen/server/restapi/configure_openapidemo_updated_configureAPI.go
中默认生成的处理器函数手动替换为刚刚提到的实际处理器函数调用。
现在我们知道了如何服务HTTP请求,我们将学习如何向其他服务器发送请求。这涉及生成客户端代码并将这些代码集成到我们的服务器逻辑中。
我们将通过这种方式展示这项能力,为此将与一个名为JSONPlaceholder的服务器互动。这个服务的呈现方式如下所示。
免费且可靠的测试接口,用于测试和原型设计。
适用于以下情况:
JSONPlaceholder 是一个免费的在线 REST API,当你需要一些假数据时,可以使用它。你可以在 GitHub 的 README 中找到它,在 CodeSandbox 的演示中,在 Stack Overflow 的代码示例中,或者用于本地测试目的。
更多详细信息可以在这个网址找到:https://jsonplaceholder.typicode.co。
该项目的代码库位于:https://github.com/typicode/jsonplaceholder。
JSONPlaceholder 是一个存储预设数据对象的服务器,这些数据对象被称为 posts
。一个 post
包含以下字段:id
(整数),title
(字符串),body
(字符串),和 userId
(整数)。JSONPlaceholder 还存储其他类型的对象,例如:评论、相册、照片、待办事项和用户。该服务器支持 http 和 https 方案,并支持以下操作方法:GET、PUT、POST、PATCH 和 DELETE。
如果你想学习如何写客户端,实现 post
对象上的 GET 和 POST 方法就足够了,特别是对于初学者来说。请务必注意不要将 POST
HTTP 方法和 post
对象混淆。如果需要适应其他方法和类型,相信也不会太难。
请注意,通过POST方式存储对象只是模拟的。POST的响应会返回您要存储的对象,但该对象实际上并没有被存储。因此,如果您在发送了POST请求后紧接着发出GET请求,将找不到您刚刚发送的对象。
指定JSONPlaceholder的部分APIJSONPlaceholder 并未提供其 API 的 Swagger 规范。相反,可以在 指南 页面上找到 API 的相关信息。通过查看该页面并做一些测试,我们大致可以了解它的用途。
由于没有现成的swagger文件定义,我们将自己动手写一个,涵盖我们学习过程中需要的两种方法。
- 获取 http://jsonplaceholder.typicode.com/posts (获取数据)
- 发送 POST 请求到 http://jsonplaceholder.typicode.com/posts (提交数据)
这些规定在文件中:sbis/jsonplaceholder/jsonplaceholder-swagger.yaml
。关键部分如下。
basePath: "/"
paths:
"/posts":
get:
description: |
获取用户帖子列表。
请不要将获取到的帖子对象与这里使用的HTTP操作GET混淆。
# operationId 影响生成代码中的各种名称
operationId: "GetPosts"
# 参数:
responses:
# 正常成功情况。网站未记录其他情况。
'200':
description: 返回 JSONPlaceholderPost 对象数组。
schema:
type: array
items:
$ref: '#/definitions/JSONPlaceholderPost'
post:
description: |
发布一个帖子对象,并在响应中查看。
发布时不提供帖子ID,响应中会返回一个ID,例如101。
POST 操作在服务器上进行模拟,您无法获取刚发布的对象。
operationId: "PostPost"
parameters:
# JSONPlaceholder 文档表明这是一个 body 参数。
- in: body
# body 参数的名称仅用于文档,实际操作中可以忽略。
name: post-object
description: "要创建的新 JSONPlaceholderPost 对象。"
schema:
$ref: '#/definitions/NewJSONPlaceholderPost'
required: true
responses:
# 正常成功情况。网站未记录其他情况。
'201':
description: 返回刚添加的帖子对象,并附带一个ID。
schema:
$ref: '#/definitions/JSONPlaceholderPost'
definitions:
JSONPlaceholderPost:
description: "用户发布的一条帖子。"
type: object
properties:
id:
type: integer
title:
type: string
body:
type: string
userId:
type: integer
NewJSONPlaceholderPost:
description: "与 JSONPlaceholderPost 类似,但不包含 id 属性。"
type: object
properties:
title:
type: string
body:
type: string
userId:
type: integer
这个 SBI swagger 文件定义了一个端点 /posts
,提供了两种方法:GET 和 POST。GET 方法返回一个 JSONPlaceholderPost
的 YAML 数组(Go 切片)。POST 方法用于存储新条目,需要输入一个 NewJSONPlaceholderPost
对象,返回一个新的 JSONPlaceholderPost
。
JSONPlaceholderPost 和 NewJSONPlaceholderPost 对象有三个相同的字段,但 JSONPlaceholderPost 对象多了一个 id
字段。这个字段只有在对象被服务器创建之后才有。
一旦生成,这两种类型在 Go 语言里看起来就像这样。
// 来自 sbis/jsonplaceholder/gen/models/json_placeholder_post.go,
type JSONPlaceholderPost struct {
Body string `json:"body,omitempty"` //omitempty表示可选字段
ID int64 `json:"id,omitempty"` //omitempty表示可选字段
Title string `json:"title,omitempty"` //omitempty表示可选字段
UserID int64 `json:"userId,omitempty"` //omitempty表示可选字段
}
// 来自 sbis/jsonplaceholder/gen/models/new_json_placeholder_post.go,
type NewJSONPlaceholderPost struct {
Body string `json:"body,omitempty"` //omitempty表示可选字段
Title string `json:"title,omitempty"` //omitempty表示可选字段
UserID int64 `json:"userId,omitempty"` //omitempty表示可选字段
} //表示一个新的JSONPlaceholderPost结构
实现一个 JSONPlaceholder 客户端工具
生成服务器 API 和客户端时,会创建不同的目录。
服务器的目标生成目录是 nbi/gen/server
,客户端的目标目录则是 sbis/jsonplaceholder/gen
。
nbi
└── gen
└── 服务器
├── cmd
│ └── openapidemo-server
├── models
└── restapi
└── 操作
sbis
└── jsonplaceholder
└── gen
├── 客户端
│ └── 操作
└── models
models
目录在两种情况下都存在。它包含了 API 使用的数据类型定义。operations
目录包含了处理 HTTP 方法的代码及其参数和响应的代码。
我们有名为cmd
的目录,它包含了主函数的代码。还有名为restapi
的目录,用于设置服务器端点和方法。
在客户端方面,我们并没有 cmd
或者 restapi
目录,但是 operations
目录中包含了一个名为 sbis/jsonplaceholder/gen/client/operations/operations_client.go
的文件。这个文件定义了一个名为 ClientService
的 Go 接口。ClientService
包含了用于执行 SBI swagger 文件中定义的每个 HTTP 方法的 Go 方法。
客户端还拥有一个额外生成的文件 sbis/jsonplaceholder/gen/client/jsonplaceholder_client.go
。该文件定义了一个名为 Jsonplaceholder
的对象,Jsonplaceholder
对象有两个属性:一个用于设置 HTTP 传输,另一个指向 ClientService
接口的实现对象。因此,我们可以通过 Jsonplaceholder
来配置 HTTP 的底层设置,而 ClientService
则知道如何调用 Jsonplaceholder
的 HTTP 方法并处理响应。
为了简化我们与客户端的交互(在这个例子中是handlers
包的行为代码),我们将编写一个Go代码文件来封装我们与外部服务器的交互。这个文件是sbis/jsonplaceholder/jphClient/jphClient.go
,用于封装与外部服务器的交互。
此文件有一个 New
函数用于创建 JSONPlaceholder 服务器的客户端实例,并且为每个我们打算在 JSONPlaceholder 上使用的 HTTP 方法提供对应的函数。此文件提供对 JSONPlaceholder 功能的最高层次抽象,并隐藏所有细节,让调用者无需关心这些细节。
如下代码片段展示了这段封装的代码。
import (
"github.com/go-openapi/strfmt"
httptransport "github.com/go-openapi/runtime/client"
"gitlab.com/adrolet/openapidemo/sbis/jsonplaceholder/gen/client"
"gitlab.com/adrolet/openapidemo/sbis/jsonplaceholder/gen/client/operations"
"gitlab.com/adrolet/openapidemo/sbis/jsonplaceholder/gen/models"
)
func New() *client.Jsonplaceholder {
jsonPlaceHolderHost := "jsonplaceholder.typicode.com"
// 创建传输层
// server-name/ip, basePath, schemes []string
transport := httptransport.New(jsonPlaceHolderHost, "/", nil)
// 使用传输创建 API 客户端
client := client.New(transport, strfmt.Default)
return client
}
// GetPosts 发送 HTTP GET 请求,并期望返回一个 Post 对象的列表
func GetPosts(client *client.Jsonplaceholder) (*operations.GetPostsOK, error){
params := operations.NewGetPostsParams()
ok, err := client.Operations.GetPosts(params)
if err != nil {
return nil, err
}
return ok, nil
}
// PostPost 发送 HTTP POST 请求以创建一个 Post 对象
func PostPost(postObj *models.NewJSONPlaceholderPost, client *client.Jsonplaceholder) (*operations.PostPostCreated, error){
params := operations.NewPostPostParams()
params.PostObject = postObj
ok, err := client.Operations.PostPost(params)
if err != nil {
return nil, err
}
return ok, nil
}
New
函数是一个构造器,它创建一个客户端实例,即一个Jsonplaceholder
实例,并设置该实例的Jsonplaceholder服务器URL地址,默认协议为http
协议。
GetPosts
函数为 GET 请求创建了一个简单的参数对象(此 GET 请求没有从调用方接收任何输入数据),然后调用了 Operations
接口中的 GetPosts
方法。通过调用 ok
变量的 GetPayload
方法,可以得到返回的 post
对象列表。由于我们在 swagger 文件中只指定了 200 状态码,因此我们只处理 GetPostsOK
类型的响应。在实际的应用程序中,这个函数还应该处理错误情况。
PostPost
函数创建一个空的参数对象,该对象将包含我们想要发送的帖子对象。然后我们调用 Operations
接口的 PostPost
方法来发送帖子数据到服务器。响应是一个 ok
对象,其中载荷被设置为参数中的帖子对象,并带有 ID
属性。请注意,响应类型为 PostPostCreated
,其状态码为 201,而不是 200,这表明请求成功创建了一个新资源。
为 JSONPlaceholder 指定 NBI 支持
在我们的演示应用中,我们选择在NBI上有一些方法来提供依赖于JSONPlaceholder服务器的功能。在最简单的情况下,这可能只是一个中继API,反映外部服务器API的一部分到演示API。你可能有商业上的理由来做这些。更有可能的是,你可能想利用外部数据来从你的服务器提供增值的服务。这些可能包括数据重新格式化、过滤,以及从多个来源聚合数据等。因此,虽然你的NBI可能会有所不同,但可能仍有一些相似之处。
如果两个服务器使用了相同的某些对象,你可以考虑仅使用其中一个API生成的代码。你可以考虑在NBI上展示由SBI规范生成的对象。这样做会使你的服务器与另一个服务器的规范紧密绑定,这对于希望保持API稳定并让用户满意的你来说是不好的。我建议你在NBI的Swagger文件中明确定义所有NBI对象,并在SBI的Swagger文件中定义所有SBI对象。为了帮助你更好地管理和随着时间的变化,给API打版本号是个好主意。一种方法是在URI路径中加入版本号,例如 scheme/server/v1/x
或 scheme/server/v2/y
。这个问题在互联网上有很多讨论,例如这里,这里,和这里。
为了演示NBI如何使用SBI并如何管理不同的但相关对象,演示应用程序实现了如下三种HTTP方法:
- POST http://<server:port>/占位帖子内容
- GET http://<server:port>/获取占位帖子标题
- GET http://<server:port>/根据用户获取占位帖子: {user}
以下展示了用于JSONPlaceholder服务器的NBI规范要求的内容。
paths:
"/place-holder-posts":
post:
description: 向占位符服务提交一个帖子
operationId: "AddPostObject"
parameters:
- in: body
name: post
description: "要添加到占位符服务中的帖子对象"
schema:
$ref: '#/definitions/NewPostObject'
required: true
responses:
'200':
description: "返回已添加的对象。"
schema:
$ref: '#/definitions/PostObject'
'500':
description: 返回错误信息字符串
schema:
type: string
"/place-holder-posts/titles":
get:
description: 从占位符服务获取帖子标题列表(包含ID)
operationId: "GetPostTitles"
responses:
'200':
description: 返回一个 PostTitle 数组。
schema:
type: array
items:
$ref: '#/definitions/PostTitle'
'500':
description: 返回错误信息字符串
schema:
type: string
"/place-holder-posts/byuser/{user}":
get:
description: 从占位符服务获取特定用户发布的帖子列表
operationId: "GetPostsByUser"
parameters:
- in: path
name: user
description: "要获取帖子的作者的用户ID"
type: integer
required: true
responses:
'200':
description: 返回一个 UserPost 数组。
schema:
type: array
items:
$ref: '#/definitions/UserPost'
'500':
description: 返回错误信息字符串
schema:
type: string
definitions:
PostObject:
# 类似于 jsonplaceholder SBI 中的 JSONPlaceholderPost 的对象
description: "用户发布的帖子内容"
type: object
properties:
id:
type: integer
title:
type: string
body:
type: string
userId:
type: integer
NewPostObject:
# 类似于 jsonplaceholder SBI 中的 NewJSONPlaceholderPost 的对象
description: "用于创建新帖子,与PostObject相同,但不包含ID属性"
type: object
properties:
title:
type: string
body:
type: string
userId:
type: integer
PostTitle:
description: "PostObject 的子集,仅返回标题和ID"
type: object
properties:
id:
type: integer
title:
type: string
UserPost:
description: |
PostObject 的子集,不包含已知的 userId 属性
type: object
properties:
id:
type: integer
title:
type: string
body:
type: string
如上所述的规范定义了三个端点,每个端点都有一个方法。第一个允许向 JSONPlaceholder 服务器 POST Post 对象。最后两个支持用于检索 Post 对象的 GET 方法,但带有某些过滤条件。所有方法都可以返回包含 200 成功状态码和一些数据的响应,或者返回包含 500 错误状态码和错误信息的响应。
/place-holder-posts
的 POST 请求接受一个 NewPostObject
(不包含 id 属性)作为输入,并返回一个 PostObject
(包含 id 属性)。这些类型的定义与 SBI 的定义一致,因此这是一个透传类型的 API。但是,拥有两种定义可以使我们的服务器 API 保持稳定,并且在 JSONPlaceholder API 发生变化时为我们提供内部补偿空间。
这是一个增值服务的例子,只返回标题和帖子ID,而不是完整的数据集。/place-holder-posts/titles
的 GET 请求返回包含标题和帖子ID的帖子列表,不过,它只会显示标题和帖子 ID。
最后一个例子是一个对 /place-holder-posts/byuser/{user}
的 GET 请求。这是一个带有路径参数的 GET 请求,这里传递的是用户的 ID。此方法返回的是请求用户的帖子列表。返回的对象是 UserPost,与 SBI 帖子对象类似,但缺少了 userId
属性,因为我们已经知道用户 ID。此方法最好支持 404 响应,以便在用户未知的情况下给出明确的反馈。在演示环境中,我没有实现这一点。请参考上面提到的电话簿示例。
这些方法生成的 Go 数据类型是:
// 源自 nbi/gen/server/models/post_object.go
// 定义了一个 PostObject 结构体
type PostObject struct {
Body string `json:"body,omitempty"` (omitempty 表示如果值为空,则不写入 JSON)
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"userId,omitempty"` (用户ID)
}
// 源自 nbi/gen/server/models/new_post_object.go
// 定义了一个 NewPostObject 结构体
type NewPostObject struct {
Body string `json:"body,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"userId,omitempty"` (用户ID)
}
// 源自 nbi/gen/server/models/post_title.go
// 定义了一个 PostTitle 结构体
type PostTitle struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
}
// 源自 nbi/gen/server/models/user_post.go
// 定义了一个 UserPost 结构体
type UserPost struct {
Body string `json:"body,omitempty"`
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
}
实现 JSONPlaceholder 支持的 NBI
注:NBI 在此可能是一个特定的术语或缩写,具体含义不明。如果没有更多背景信息,建议保持原样。
下面我们将介绍如何实现 JSONPlaceholder 的支持功能,其中会涉及到 NBI 的内容。
既然我们知道有哪些数据会进入我们的服务器,以及需要发送给JSONPlaceholder服务器的数据,我们来看看这两个API是如何绑定在一起的。这三个NBI方法的处理程序已经实现,就像其他所有的处理程序一样,它们也都在这个文件handlers/handlers.go
中。NBI方法(例如某种特定的方法)。
import (
...
"github.com/go-openapi/runtime/middleware"
"gitlab.com/adrolet/openapidemo/nbi/gen/server/restapi/operations"
"gitlab.com/adrolet/openapidemo/nbi/gen/server/models"
"fmt"
"gitlab.com/adrolet/openapidemo/sbis/jsonplaceholder/jphClient"
jphModels "gitlab.com/adrolet/openapidemo/sbis/jsonplaceholder/gen/models"
)
func AddPostObject(params operations.AddPostObjectParams) middleware.Responder{
client := jphClient.New()
// 将NBI对象转换为发送到SBI的等效POST对象
postObject := params.Post
postObj := &jphModels.NewJSONPlaceholderPost{}
postObj.UserID = postObject.UserID
postObj.Title = postObject.Title
postObj.Body = postObject.Body
postResponse, err := jphClient.PostPost(postObj, client)
if err != nil {
return operations.NewAddPostObjectInternalServerError().WithPayload(err.Error())
}
// 将SBI响应对象转换为NBI对象
sbiPost := postResponse.Payload
po := &models.PostObject{}
po.Body = sbiPost.Body
po.ID = sbiPost.ID
po.Title = sbiPost.Title
po.UserID = sbiPost.UserID
return operations.NewAddPostObjectOK().WithPayload(po)
}
func GetPostTitles(params operations.GetPostTitlesParams) middleware.Responder{
client := jphClient.New()
getResponse, err := jphClient.GetPosts(client)
if err != nil {
fmt.Println(err.Error())
return operations.NewGetPostTitlesInternalServerError().WithPayload(err.Error())
}
// 读取SBI中的帖子标题并将它们转换为NBI中的帖子标题结构
titles := make([]*models.PostTitle, 0, len(getResponse.Payload))
for _, aPost := range getResponse.Payload {
if aPost.UserID == params.User {
title := &models.PostTitle{ID:aPost.ID, Title:aPost.Title}
titles = append(titles, title)
}
}
return operations.NewGetPostTitlesOK().WithPayload(titles)
}
func GetPostsByUser(params operations.GetPostsByUserParams) middleware.Responder{
client := jphClient.New()
getResponse, err := jphClient.GetPosts(client)
if err != nil {
fmt.Println(err.Error())
return operations.NewGetPostsByUserInternalServerError().WithPayload(err.Error())
}
// 读取SBI中的帖子,并将属于指定用户的帖子转换为NBI中的帖子结构
titles := make([]*models.UserPost, 0, len(getResponse.Payload))
for _, aPost := range getResponse.Payload {
if aPost.UserID == params.User {
title := &models.UserPost{ID:aPost.ID, Title:aPost.Title, Body:aPost.Body}
titles = append(titles, title)
}
}
return operations.NewGetPostsByUserOK().WithPayload(titles)
}
AddPostObject
函数接受要 POST 的对象作为其 params
参数。然后我们定义一个新的对象 postObj
,它是符合 SBI 规范的对象。因为 NBI 和 SBI 对象遵循相同的规范,我们可以直接将 params
中的每个属性复制到 SBI 对象中。
此 SBI 对象会被传递给 jphClient.PostPost
函数,此函数会与 JSONPlaceholder 服务器交互并返回该 POST 对象,此时该对象的 ID
属性已经被设定。
如果检测到错误,我们就会返回一个错误信息,并返回 500 状态码。
成功时,我们将SBI响应对象的属性复制到NBI对象。然后将该NBI对象返回给调用了演示程序的客户端。
GetPostTitles
函数简单地用我们刚创建的 SBI 客户端调用了 GetPosts
函数。
如果检测到错误,将会返回错误信息和 500 状态码。
如果成功,我们会遍历响应中的所有 post 元素,并创建一个 PostTitle
的切片。将响应元素中的属性复制到 PostTitle
。最后,通过 operations.NewGetPostTitlesOK
将切片返回给调用方。
此功能的价值在于,它能从完整的对象列表中提取摘要对象列表。实际上,你增加的价值可能重要得多。
GetPostsByUser
函数和 GetPostTitles
函数有相同的结构。区别在于,在处理响应时的循环中,它会判断是否需要返回该条目,即看传递的 UserID
是否匹配。此外,返回对象中的属性也不相同。
记住,需要手动将文件 nbi/gen/server/restapi/configure_openapidemo_updated_configureAPI.go
中默认生成的处理程序替换为前面刚刚讨论的实际处理程序的调用。
恭喜,你已经读了足够的用例来掌握如何在内部和外部服务器上处理HTTP请求。你现在应该能够将这些知识应用到你自己的应用程序的大多数甚至所有可能需要的情况中。
运行演示程序学习的最佳方式是动手实验。建议你复制或下载GitHub上的代码,编译并试试运行。然后你可以进行修改,看看哪些地方可以改进。试试看,这样更符合原句的意思。
这里简单说一下如何做这件事。
你可以用两种方法来运行演示应用程序服务器:使用 go run
或 go install
,然后你可以按自己喜欢的方式使用编译后的程序。
为了使这更容易,仓库包含一个Makefile文件,其中定义了两个目标,用于启动服务器。
如果你想使用,请确保你的主机上已安装了 GNU make
可执行程序。
使用 go run
命令:
$ make run
编译并运行最新代码。不会安装到 /Users/adrolet/go/bin。关于安装,请参阅 'make install'。
go run gitlab.com/adrolet/openapidemo/nbi/gen/server/cmd/openapidemo-server --port 8888 --host 127.0.0.1
你可以按下 Ctrl+C 来杀死服务器。
你也可以编译服务器端,然后运行它。下面的示例假设环境变量$PATH中包含$GOPATH/bin。
$ make install
编译应用程序,并将可执行文件安装在 /Users/adrolet/go/bin,
go install gitlab.com/adrolet/openapidemo/nbi/gen/server/cmd/openapidemo-server
$ openapidemo-server --port 8888 --host 127.0.0.1 # 启动服务器监听端口8888并绑定到IP地址127.0.0.1
如果你想从其他主机访问这个应用,就用外网IP作为--host
,然后随便选一个空闲端口作为--port
。
一旦你的服务器运行起来,你可以通过shell会话轻松使用curl
命令发送HTTP请求。
如果你更喜欢使用某种用户界面形式,演示应用嵌入了 Swagger UI。Swagger UI 是最简单的方式,无需安装即可运行 API。只需访问:(下面的文档和 swagger.json 链接在你使用与浏览器相同的主机和默认的 ip 和端口运行演示应用时会工作)。
http://127.0.0.1:8888/openapidemo/docs (本地服务器的API文档页面)
或者,你可以使用出色的Postman产品。Postman拥有类似IDE的图形界面(GUI),可以配置并记住一些设置。如果你打算做严肃的REST测试,你应该试试它。
Swagger生成器还实现了一个端点,通过该端点你可以下载你服务器的Swagger规范(JSON格式)。只需执行GET请求:
/swagger.json
http://127.0.0.1:8888/swagger.json
作为示例,这是使用 curl
调用 GET HostInfo 方法的方法。结果通过 jq
工具进行了美化输出。这是可选的,但如果你没有安装的话,安装起来应该很简单。将输出通过命令 python3 -m json.tool
美化处理也会得到相同的输出。
$ curl -s http://127.0.0.1:8888/openapidemo/host-info | jq
# 使用curl命令获取并解析主机信息
{
"架构": "amd64",
"主机名": "Alain-Drolets-iMac.local",
"CPU数量": 4,
"操作系统名称": "darwin"
}
我建议你也读一下 README.md 文件。它包含大多数 NBI 方法的命令示例,也有简洁明了的应用程序结构和功能介绍。
参考文献OpenApiDemo — 本文中用到的应用程序代码
https://gitlab.com/adrolet/openapidemo
RFC 9110,HTTP 语义学文档
< https://www.rfc-editor.org/rfc/rfc9110.html >
REST(表述资源、通过超文本传输协议 (HTTP) 的无状态服务的软件架构样式)
https://en.wikipedia.org/wiki/REST(关于 REST 的维基百科页面)
这两个术语gRPC和协议缓冲
https://en.wikipedia.org/wiki/GRPC (gRPC和协议缓冲的维基百科页面)
GraphQL,一种强大的数据查询语言
https://graphql.org
Swagger API项目的历程
https://en.wikipedia.org/wiki/Swagger_(software)
OpenAPI组织 — 主页面
https://www.openapis.org
Swagger/OpenAPI 规格
版本号 2.0: https://swagger.io/specification/v2/
版本号 3.x 或最新版本: https://spec.openapis.org/oas/latest.html
Swagger UI:
https://swagger.io/tools/swagger-ui/ 访问 Swagger UI 官方网站
Postman
https://www.postman.com
go-swagger 生成工具简介
go-swagger 产品文档: https://goswagger.io/go-swagger/
安装简介: https://goswagger.io/go-swagger/install/
从源代码安装: https://goswagger.io/go-swagger/install/install-source/
Git 仓库地址: https://github.com/go-swagger/go-swagger
支持 Open API 3.0 规范: https://github.com/go-swagger/go-swagger/issues/1122
Swagger Codegen — 一个由 SmartBear 支持的原始工具。
Codegen 版本 2.X https://github.com/swagger-api/swagger-codegen
Codegen 版本 3.X https://github.com/swagger-api/swagger-codegen/tree/3.0.0
OpenAPI Generator — 社区支持的 Codegen 分叉
https://github.com/OpenAPITools/openapi-generator
JSONPlaceholder 网站
首页: https://jsonplaceholder.typicode.com
使用说明: https://jsonplaceholder.typicode.com/guide/
Git 代码库: https://github.com/typicode/jsonplaceholder
JSON 指示符(JSON Pointer)
https://datatracker.ietf.org/doc/html/rfc6901
**关于API版本的信息
https://www.postman.com/api-platform/api-versioning/
https://www.xmatters.com/blog/api-versioning-strategies
https://restfulapi.net/versioning/
共同学习,写下你的评论
评论加载中...
作者其他优质文章