OpenAPI Specification
OpenAPI 规范(OAS)为 RESTful API 定义了一套标准的、跨语言的接口,以便人类和计算机均无需通过源码、文档或网络流量检测探知和理解服务器提供的能力。OAS 可用于制作文档生成器、代码生成器、测试工具等等。OpenAPI 以 json 对象形式呈现,因此它基本遵循 json schema 规范,可以用 json 或 yaml 格式编写。
在 OpenAPI 规范的基础上,Swagger 提供了一套开源工具用于为服务端和客户端设计、制作 api 文档以及代码,包含 Swagger Editor、Swagger UI、Swagger Codegen。基于 Swagger,开发流程可能是这样的:设计 api,使用 Swagger Codegen 制作服务端代码,稍后再实现业务逻辑;使用 Swagger Codegen 生成客户端脚本库;使用 Swagger UI 制作交互式文档界面等等。
OpenAPI 文档的基础数据包含:
- openapi: OAS 版本号,影响解析策略。
- info: RESTful API 的元信息,包含标题、描述、联系方式、许可证、版本号、服务条款链接。
- servers: 服务器信息。当 servers 未指定时,将使用 url 为 '/' 的服务器对象。
- paths: RESTful API 的路径。单个路径包含相同配置时会造成冲突,这时会由工具决定选用哪一个。路径下挂操作对象。
- components: 在 OAS 中作为可重用的部件,被引用时才生效。
- security: RESTful API 的安全机制,可以在操作对象层级进行改写。
- tags: 标签列表,即针对操作对象打标签。
- externalDocs: 扩展文档。
penapi: 3.0.0
info:
title: Sample API
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
version: 0.1.9
servers:
- url: https://api.example.com/v1
description: Production server (uses live data)
- url: https://sandbox-api.example.com:8443/v1
description: Sandbox server (uses test data)
paths:
/users:
get:
summary: Returns a list of users.
description: Optional extended description in CommonMark or HTML.
security:
- OAuth2: [read] # <------ 使用 oauth2 认证
responses:
'200': # status code
description: A JSON array of user names
content:
application/json:
schema:
type: array
items:
type: string
components:
securitySchemes:
BasicAuth:
type: http
scheme: basic
security:
- BasicAuth: []
OAS 使用 end point、operation object 描述 api。以上 schema 中,/users 是一个 end point,get 是一个 operation object。end point 下可以包含如下的 operation object:put、post、delete、options、head、patch、trace。operation object 可以配置 tags、summary、description、externalDocs、operationId、parameters、requestBody、responses、callbacks、deprecated、security、servers 属性。典型如下:
paths:
/users/{id}: # 正则格式
get:
tags:
- Users
summary: Gets a user by ID.
description: A detailed description of the operation.
Use markdown for rich text representation,
such as **bold**, *italic*, and [links](https://swagger.io).
operationId: getUserById
parameters:
- name: id
in: path
description: User ID
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/User' # 使用 $ref 引用 components 中定义的实体
externalDocs:
description: Learn more about user operations provided by this API.
url: http://api.example.com/docs/user-operations/
components:
schemas:
User:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
required:
- id
- name
后端接入 swagger2
java 中介入 swagger2 可按以下方式进行:
- pom.xml 添加 springfox-swagger2、springfox-swagger-ui 依赖。
- 使用 @Configuration、@EnableSwagger2 注解编写 Swagger 配置类。
- 在 Controller 中使用 Swagger 注解接口。
- 访问 swagger-ui.html 查看接口文档。
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
// configuration
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket customDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
Contact contact = new Contact("团队名", "www.my.com", "my@my.com");
return new ApiInfoBuilder()
.title("文档标题")
.description("文档描述")
.contact(contact) // 联系方式
.version("1.1.0") // 版本
.build();
}
}
// controller
@RestController
@Api(tags="接口所在的类")
public class HelloController {
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ApiOperation(value = "接口名", notes = "接口描述", httpMethod = "POST")
@ApiImplicitParams({
@ApiImplicitParam(name = "length",value = "参数1", required = true, paramType = "path"),
@ApiImplicitParam(name = "size",value = "参数2", required = true, paramType = "query"),
@ApiImplicitParam(name = "page",value = "参数3", required = true, paramType = "header"),
@ApiImplicitParam(name = "total",value = "参数4", required = true, paramType = "form"),
@ApiImplicitParam(name = "start",value = "参数5",dataType = "string", paramType = "body")
})
public String index(){
return "Hello World!";
}
}
生成前端接口
这里以 openapi-generator 为例,说明一下怎么根据 swagger-ui 生成前端服务层调用代码和实体类接口。通过后端应用接入 swagger2 之后,访问 /v2/api-docs 即可查看复合 OAS 规范的全量接口信息,openapi-generator 就是在这份接口数据的基础上,通过解析并使用 nunjucks 模板生成 js 脚本的。下图即为 /v2/api-docs 数据内容。
openapi-generator 的核心代码见于 ServiceGenerator 类,该类的作用即是解析 /v2/api-doc 数据内容并使用模板生成服务层调用及接口文件。除了解析 OAS 数据外,它基于以下流程实现:
- 基于 config.requestLib 配置为真生成服务层调用基类,文件名为 base.js 或 base.ts。
- 基于 config.type 配置为 'ts' 生成实体类接口,文件名为 typings.d.ts。
- 基于 config.serviceType 生成服务层调用脚本,类或函数形式。
class ServiceGenerator {
// 生成服务层调用代码和实体类接口文件
genFile() {
// 基于 openapi-generator 内置的 template/base.njk 生成服务层调用基类
this.genRequestLib();
// 基于 openapi-generator 内置的 template/interface.njk 或外部模板生成实体类接口
if (this.config.type === 'ts') {
debug('[GenSDK] gen interface.');
this.genFileFromTemplate('typings.d.ts', 'interface', {
namespace: this.config.namespace,
list: this.getInterfaceTP(),// 获取 /v2/api-doc 数据内容中的 definitions
});
}
// 基于 openapi-generator 内置的 template/interface.njk 或外部模板生成实体类接口
this.getServiceTP()// 获取 /v2/api-doc 数据内容中的 paths(经 ServiceGenerator.constructor 处理)
.filter(tp => {
tp.list = tp.list.filter(
item =>
!this.config.filter ||
this.config.filter.some(f => {
return f instanceof RegExp
? f.test(item.path)
: typeof f === 'function'
? f(item)
: true;
})
);
return tp.list.length;
})
.map(tp => {
debug('[GenSDK] generate service:', tp.className);
this.genFileFromTemplate(
this.getFinalFileName(`${tp.className}.${this.config.type}`),
'service',
{
namespace: this.config.namespace,
...tp,
}
);
});
}
protected genFileFromTemplate(fileName: string, type: 'interface' | 'service', params: any) {
try {
const template = this.getTemplate(type);
this.writeFile(fileName, nunjucks.renderString(template, params));
} catch (error) {
console.warn('[GenSDK] file gen fail:', fileName, 'type:', type);
throw error;
}
}
protected getTemplate(type: 'interface' | 'service') {
const configFilePath =
type === 'interface' ? this.config.interfaceTemplatePath : this.config.templatePath;
try {
if (configFilePath) {
this.mkdir(path.dirname(configFilePath));
if (fs.existsSync(configFilePath)) {
return fs.readFileSync(configFilePath, 'utf8');
}
}
const fileContent = fs.readFileSync(
path.join(
__dirname,
'template',
type === 'service' ? `${type}.${this.config.serviceType}.njk` : `${type}.njk`
),
'utf8'
);
if (configFilePath) {
fs.writeFileSync(configFilePath, fileContent, 'utf8');
}
return fileContent;
} catch (error) {
console.warn(`[GenSDK] get {${type}} template fail:`, configFilePath);
throw error;
}
}
}
在 ServiceGenerator 类的基础上,openapi-generator 支持基于接口、文件获取 OAS 数据并生成前端脚本。
openapi-generator url http://xxx/v2/api-docs -c true # 基于接口
openapi-generator config ./xxx.js # 基于文件
后记
写作本文的目的本意在于探究怎样通过后端脚本生成前端 service 层代码。我曾接触过的 midway 前端框架可以生成 hsf 服务中使用的代理层文件(实现与后端服务的参数对接和类型转换),前端也能根据 swagger 文档生成 service 层代码。也许,midway 中生成的代理文件也是基于 hsf 平台上标准化的接口文档。在这次探寻过程上,我发现了一个新的思考点:既可以根据 OAS 生成 service 脚本,也可以根据 OAS 提供 mock 服务。OAS 规范也使人思忖:在团队能力较弱或应用场景较小的情况下,探讨逻辑流程的紧要程度高于制定输入输出文档;在应用场景达到规模化或团队能力可信赖的情况下,规范化的输入输出文档高于逻辑实现。“路漫漫而修远兮,吾将上下而求索”。
参考
Swagger Codegen 高效开发客户端对接服务端代码
开源小工具 - swagger API访问代码生成器(js/typescript)