无论你从事前端开发还是后端开发,或多或少都听说过Swagger。
Swagger Specification 是一种 API Specification(API 规范),2015 年,SmartBear Software 将 Swagger Specification 捐赠给 Linux Foundation,并改称为 OpenAPI Specification简称(OAS)
OAS 本质上是一种 RESTful API 的描述方式,允许用户使用这种方式定义所有的API。
参考:OpenAPI-Specification
通过OAS定义API,然后自动生成前后端的代码以及文档,可以一劳永逸的解决上述问题。
OAS可以书写为 JSON 或 YAML。很多配置文件(例如Nginx和大部分脚本语言的配置文件)都习惯使用 JSON ,但越来越多的地方(例如 Springboot),推荐使用YAML,YMAL能覆盖JSON的所有语义而且直至更多数据类型,语法更简洁。
参考:https://yaml.org/spec/1.2/spec.pdf
以Github的OpenAPI为例(Github的Restful API 完全兼容OpenAP 3.0规范):
参考:https://github.com/github/rest-api-description
openapi: 3.0.3
info:
version: 1.1.4
title: GitHub v3 REST API
description: GitHub's v3 REST API.
license:
name: MIT
url: https://spdx.org/licenses/MIT
servers:
- url: https://api.github.com
externalDocs:
description: GitHub v3 REST API
url: https://docs.github.com/rest/
tags:
- name: apps
description: Information for integrations and installations.
- name: activity
description: Activity APIs provide access to notifications, subscriptions, and timelines.
...
paths:
paths:
"/":
get:
summary: GitHub API Root
tags:
responses:
'200':
...
"/app":
get:
summary: Get the authenticated app
tags:
- apps
parameters: []
responses:
'200':
...
"/feeds":
get:
summary: Get feeds
tags:
- activity
parameters: []
responses:
'200':
...
components:
schemas:
simple-user:
title: Simple User
description: Simple User
type: object
properties:
login:
type: string
example: octocat
id:
type: integer
example: 1
avatar_url:
type: string
format: uri
example: https://github.com/images/error/octocat_happy.gif
...
详细配置可以参考 https://swagger.io/specification/
推荐使用SwaggerEditor,用来对yaml进行读取和编辑。右侧可以显示解析后的结果,还可以直接测试API的调用。
SwaggerEditor已经提供了Server和Client的代码生成,可以生成JavaBean以及API请求代码。但是都是Java版本的。如果想生成Kotlin代码可以使用
参考: OpenAPI Generator
OpenAPI Generator使用gardle生成目标代码,
参考: OpenAPI Generator Gradle Plugin
在工程中创建一个专门用于生成api的module,并为之定义build.gradle.kts
脚本
val kotlin_version = "1.4.30"
plugins {
kotlin("jvm")
// OpenAPI Generator
id("org.openapi.generator") version "5.0.0-beta2"
id("kotlinx-serialization")
}
dependencies {
implementation(fileTree("dir" to "libs", "include" to listOf("*.jar")))
// 依赖库 kotlin, retrofit, moshi 等
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// implementation(Dependency.moshi)
}
// API 名
val apiName = "github"
// 生成代码位置
val buildApiDir = "$buildDir/openApiGenerator/$apiName"
// 生成代码的package名
val basePackage = "com.my.openapigeneratorsample"
fun String.packageToDir() = replace('.', '/')
task<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("generate") {
doFirst {
delete(file(buildApiDir))
}
// OpenAPI Generator 相关设定
generatorName.set("kotlin") //生成kotlin
library.set("jvm-retrofit2")
// templateDir.set("$rootDir/template") // 设置模板用来做修正,后文介绍
inputSpec.set("$rootDir/api/api.github.com.yaml") //oas地址,取自github/openapi
outputDir.set(buildApiDir)
packageName.set(basePackage)
apiPackage.set("$basePackage.$apiName.api")
modelPackage.set("$basePackage.$apiName.model")
configOptions.set(
mapOf(
"dateLibrary" to "java8"
)
)
additionalProperties.set(
mapOf(
// 设置是否使用Rx或者Coroutine
"doNotUseRxAndCoroutines" to "true"
)
)
generateApiTests.set(false)
}
task<Copy>("copy") {
val dirFrom = "$buildApiDir/src/main/kotlin/${
basePackage.packageToDir()}/"
val dirInto = "$projectDir/src/main/java/${
basePackage.packageToDir()}/"
doFirst {
delete(file(dirInto))
}
dependsOn("generate")
from(dirFrom)
into(dirInto)
}
task("buildApi") {
dependsOn("generate", "copy")
}
命令行执行
./gradlew :api:buildApi
前面OSA的配置项中有tag
,用来对API进行分组,在paths
中也会标记当前API的tag。
生成代码时会根据tag
生成XXXApi
tags:
- name: actions
description: Endpoints to manage GitHub Actions using the REST API.
- name: activity
description: Activity APIs provide access to notifications, subscriptions, and timelines.
- name: apps
description: Information for integrations and installations.
- name: billing
description: Monitor charges and usage from Actions and Packages.
...
XXXApi
中根据paths
配置生成API请求方法:
interface ActionsApi {
/**
* Get GitHub Actions permissions for an organization
* Gets the GitHub Actions permissions policy for repositories and allowed actions in an organization. You must authenticate using an access token with the `admin:org` scope to use this endpoint. GitHub Apps must have the `administration` organization permission to use this API.
* Responses:
* - 200: Response
*
* @param org
* @return [Call]<[ActionsMinusOrganizationMinusPermissions]>
*/
@GET("orgs/{org}/actions/permissions")
fun actionsActionsPoliciesGetGithubActionsPermissionsOrganization(@Path("org") org: kotlin.String): Call<ActionsMinusOrganizationMinusPermissions>
/**
* Add repository access to a self-hosted runner group in an organization
* The self-hosted runner groups REST API is available with GitHub Enterprise Cloud. For more information, see \"[GitHub's products](https://docs.github.com/github/getting-started-with-github/githubs-products).\" Adds a repository to the list of selected repositories that can access a self-hosted runner group. The runner group must have `visibility` set to `selected`. For more information, see \"[Create a self-hosted runner group for an organization](#create-a-self-hosted-runner-group-for-an-organization).\" You must authenticate using an access token with the `admin:org` scope to use this endpoint.
* Responses:
* - 204: Response
*
* @param org
* @param runnerGroupId Unique identifier of the self-hosted runner group.
* @param repositoryId
* @return [Call]<[Unit]>
*/
@PUT("orgs/{org}/actions/runner-groups/{runner_group_id}/repositories/{repository_id}")
fun actionsAddRepoAccessToSelfHostedRunnerGroupInOrg(@Path("org") org: kotlin.String, @Path("runner_group_id") runnerGroupId: kotlin.Int, @Path("repository_id") repositoryId: kotlin.Int): Call<Unit>
...
}
API返回的Model定义来自OAS中的components.schemas
配置。paths
配置中会使用这些它们作为parameters
,当不匹配时,无法正常生成代码。
components:
schemas:
...
actor:
title: Actor
description: Actor
type: object
properties:
id:
type: integer
login:
type: string
display_login:
type: string
gravatar_id:
type: string
nullable: true
url:
type: string
format: uri
avatar_url:
type: string
format: uri
生成代码:
/**
* Actor
* @param id
* @param login
* @param gravatarId
* @param url
* @param avatarUrl
* @param displayLogin
*/
data class Actor (
@Json(name = "id")
val id: kotlin.Int,
@Json(name = "login")
val login: kotlin.String,
@Json(name = "gravatar_id")
val gravatarId: kotlin.String?,
@Json(name = "url")
val url: java.net.URI,
@Json(name = "avatar_url")
val avatarUrl: java.net.URI,
@Json(name = "display_login")
val displayLogin: kotlin.String? = null
)
OpenAPI Generator 并非完美,有时生成的代码有问题。
例如v5.0.0-beta2
生成的ApiClient.kt
中出现会如下错误(已发PR,估计v5.0.0会修复):
此时使用Template可以进行自动修正,避免手工劳动。
我们为ApiClient.kt
创建Template文件:libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache
...
fun createService(serviceClass: Class): S {
# REMOVE these lines
# var usedClient: OkHttpClient? = null
# this.okHttpClient?.let { usedClient = it } ?: run {usedClient = clientBuilder.build()}
# ADD this line
val usedClient: OkHttpClient = this.okHttpClient ?: clientBuilder.build()
return retrofitBuilder.client(usedClient).build().create(serviceClass)
}
...
关于Template,更多内容参考
https://openapi-generator.tech/docs/templating/
基于修正后的ApiClient.kt
,我们就可以直接拿来进行API请求了
val userApi: UserApi = ApiClient(authName = "Bearer", bearerToken = "[MY_TOKEN]")
.createService(UserApi::class.java)
fun getAllUser(callback: Callback<List<User>>, page: Int? = null, perPage: Int? = null) {
val call = userApi.getAllUser(page?.toString(), perPage?.toString())
call.enqueue(callback)
}
...
上面生成的API中使用的retrofit2.Call
返回结果。在Kotlin中我们更希望直接使用suspend
函数
build.gradle.kts
中稍作修改即可:
additionalProperties.set(mapOf(
// REMOVE this line
// "doNotUseRxAndCoroutines" to "true"
// ADD this line
"useCoroutines" to "true"
))
生成代码编程下面这样:
interface ActivityApi {
@GET("user/starred/{owner}/{repo}")
suspend fun activityCheckRepoIsStarredByAuthenticatedUser(@Path("owner") owner: kotlin.String, @Path("repo") repo: kotlin.String): Response<Unit>
@DELETE("repos/{owner}/{repo}/subscription")
suspend fun activityDeleteRepoSubscription(@Path("owner") owner: kotlin.String, @Path("repo") repo: kotlin.String): Response<Unit>
...
}
OpenAPI Generator能够通过多样性的配置生成开箱即用的客户端代码。此外它还可以生成服务端代码,保证了多端一致性。当API发生变更时,只要修改yaml文件就可以自动生成新的代码,大大降低开发负担。借助SwaggerEditor还可以对yaml进行简单测试。
现在越来越多的开放平台都支持了OpenAPI规范,作为客户端开发者,也可以活用OAS的周边工具管理和生成客户端代码,降低我们的开发成本。