如何实现 OpenAPI 多语言 SDK 开发?

阿里妹导读:由于每个网关所对应的后端情况不同,因此没有一套元数据可以适用于所有的网关。阿里云通过重新定义一门 DSL 语言 —— Darabonba 来支持不同风格的 OpenAPI,同时支持多语言的 SDK、Code Sample 目标生成。本文将从技术原理和解决方案分享相关的探索和实践。

如今 OpenAPI 已经成为完成系统之间集成的重要桥梁,OpenAPI 的可用性以及用户在使用时的体验就变得越来越重要,阿里云前架构师曾说过:"阿里云的本质是一家卖 API 的公司。API 有没有做好,是关乎生死的大事"。但是从日常来自用户的反馈中我们总结了以下比较通用的几点 OpenAPI 体验问题:
  • 云产品 OpenAPI 没有提供 SDK 或者 SDK 语言不全;

  • 部分云产品的 SDK 使用风格差异过大,导致使用成本增加;

  • API 文档缺失或者不够清晰,不具备指导意义;

  • 没有场景化 Code Sample 或提供 CodeSample 无法运行。

多语言 SDK 生成

说起生成多语言 SDK,大家第一时间想起的一定是当前业界内生成多语言 SDK 的通用方案——Swagger,开发者通过 Swagger 定义的 OpenAPI 标准并配合模板的方式来生成多语言 SDK。不过问题并没有因此而得到完美的解决。首先模版的生成方式相对生硬,虽然实现起来容易,但维护起来却不那么灵活;其次,大量 OpenAPI 并不是 RESTful 风格的,这就导致很多产品现存的 OpenAPI 在文档、SDK 等场景下,无法使用上 Swagger 这样强大的生态工具链。既然无法沿用 Swagger 规范元数据的方法来解决这个问题,我们就需要对我们的工作重新进行抽象。在笔者看来,之所以没有一套元数据可以适用于所有的网关主要还是因为每个网关所对应的后端情况不同,就像机器语言或者汇编语言会因为架构的不同而有所不同,但是其本质还是描述如何通过操作寄存器、内存里的数据来完成一个程序,高级语言就是通过 AST 兼容了各平台的这些不同最后解决了这些问题。而对于 OpenAPI 来说也是同样的道理,所以我们通过重新定义一门 DSL 语言 Darabonba 来描述各种各样的 OpenAPI。

通过 Darabonba 对 OpenAPI 进行描述,其本质就是统一了元数据,只是这个元数据并不是 JSON 或者 Yaml 这样的方式来描述的,而是通过 DSL 代码来描述。Darabonba 的编译器则会将 Darabonba 的 DSL 代码转化为 AST,通过 OpenAPI 描述转化而来的 AST 不仅包含了 OpenAPI 的信息,而且还包含整个 OpenAPI 的流程性描述,所以我们只需要通过 AST 开发对应的各语言SDK就可以生成多语言的 SDK了。Darabonba 具体的设计思路和理念可参考文章:Darabonba:支持任意 OpenAPI 网关的多语言 SDK 方案,这里就不再赘述。

模块化设计

在通过元数据向生成 SDK 的过程中,仅仅通过对 OpenAPI 的数据模型和请求/响应描述是不够的,还需要各种参数处理,签名生成,文件上传,流操作等各种复杂的方法,以往通过模板生成 SDK 的时会选择维护一个各语言的核心模块来封装这些方法,但是随着支持的 OpenAPI 越来越多核心库中的方法也是越来越多,就会产生以下的问题:
  • 客户使用或者开发者接入核心库的 SDK 开发者来说成本会越来越大;

  • 核心库的维护人员的维护成本也会越来越高,随着方法越来越多也需要不断的对核心库进行重构,遇到不兼容性改动的可能性也会越来越高;

  • 所有方法杂糅在一个核心库中,在修改时容易牵一发动全身,需要大量的测试用例保障。

在通过 Darabonba 生成的 SDK 时也会遇到同样的问题,Darabonba 作为一门 DSL 语言主要能力在于描述 OpenAPI ,为了保障生成的 SDK 具备完整的功能同样需要很多实现很多核心方法,而在总结以往维护核心库的中遇到的问题以后,我们选择了现在在高级语言中非常常见的模块化开发理念,并提供了相应的命令行工具 Darabonba CLI 和 Darabonba 模块仓库。

接口模块

Darabonba 其核心能力是描述 OpenAPI,缺少复杂逻辑实现的能力,为了弥补这个能力 Darabonba 设计了接口模块的概念。与 Java 中的 interface 接口类型定义类似,Darabonba 的接口模块即是只在 Darabonba 编写的DSL 代码中只定义方法体的集合而并不实现其具体逻辑,真正的逻辑则是由各语言分别实现。例如 Darabonba 中常用的 Console 模块:
/** * Console val with log level into stdout * @param val the printing string * @return void * @example \[LOG\] tea console example */static function log(val: string): void;

我们只需要在模块中申明模块包含 log 方法并描述它的出参入参即可,而各语言则通过自身语言的特性来实现该方法即可,其具体实现可参考 Console 模块源码。在编写好生成接口模块以后可以通过 Darabonba 提供的 Darabonba CLI 执行 dara publish 将模块发布到 Darabonba 模块仓库,就可以在 Darabonba 代码中使用了。下面就是我们通过引入 Console 模块来打印字符串的一段代码:
import Console;
static async function main(args: [ string ]) throws : void {  Console.log("hello world!");}

通过接口模块的设计理念将以往核心库中的方法根据功能拆分成一个个包含了特定功能的基础模块,不仅使得生成的 SDK 更好用逻辑更清晰,同时也做到了足够的抽象避免很多在生成 SDK 过程中重复造轮子的工作。目前 Darabonba 官方提供了包含了常用方法的 Util 模块、文件上传所使用的 FileForm 以及 XML 模块等,同时开发者也可以编写与自己业务逻辑相关的接口模块并发布到 Darabonba 模块仓库。

OpenAPI 模块

在 Darabonba 的模块化设计中不止有接口模块,事实上每一个 Darabonba 的项目都是一个模块,所以基于一组 OpenAPI 描述编写的 Darabonba 代码就是一个 OpenAPI 模块。开发者在完成了 OpenAPI 描述的 Darabonba 代码编写以后,同样可以通过 Darabonba CLI 将描述 OpenAPI 的 Darabonba 模块发布到模块仓库中,这样使用 SDK 的用户就可以通过模块的详情页面查看 SDK 中包含的方法及各语言 SDK 的安装说明等信息了。
基于一组 OpenAPI 发布的 Darabonba 模块不仅可以帮助用户更好的了解这组 OpenAPI,更可以在这个基础上实现 OpenAPI 接口的 Code Sample 编写,进而实现多语言的 Code Sample 统一生成。

Code Sample 自动生成

可以说对于简单的 API 调用普通的 API 文档就足够了,但是随着现在 OpenAPI 在系统与系统集成之间使用的越来越广泛,其复杂度也随之提高,以往单纯使用 API 文档的方式已经不足以让客户顺利的使用 OpenAPI 了。从阿里云目前的工单情况来看,SDK 相关的客户咨询至少有一半是因为没有 Code Sample 造成的,其中更是有1/4的客户是直接要求为 SDK 提供 Code Sample。

这种情况下,能够提供给用户可运行、可调试的 Code Sample 示例就成了文档中必不可少的一部分,但是如何能够编写全语言的 Code Sample 并且保障其可运行,却是一个极大的问题。如果通过人力来维护,很容易就出现语言不全,或是编写的代码没有维护的问题,阿里云中遇到最多的问题就是 OpenAPI 在迭代,而 Code Sample 却忘记迭代了造成了提供出去的 Code Sample 无法使用从而被用户诟病。

通过引用模块仓库中 Darabonba 模块编写的 CodeSample 则可以避免这样的问题,首先多语言的自动生成,节约了大量的维护成本,而且风格统一利于用户理解;同时 Darabonba 编译时采用类型的强校验,一旦 OpenAPI 的参数或者返回结果出现了不兼容的更新,CodeSample 则会生成失败从而通知到开发者更新相关示例来解决这个问题。下面是阿里云语音服务 SDK 相关的 CodeSample 示例,大家也可以点击示例链接尝试生成:
import Dyvmsapi;import RPC;import Console;
/** * 使用AK&SK初始化账号Client * @param accessKeyId * @param accessKeySecret * @param regionId * @return Client * @throws Exception */ static function createClient (accessKeyId : string , accessKeySecret : string) throws : Dyvmsapi{    var config = new RPC.Config{};    // 您的AccessKey ID    config.accessKeyId = accessKeyId;    // 您的可用区ID    config.accessKeySecret = accessKeySecret;    return new Dyvmsapi(config);}
/** * @param args * @throws Exception */static async function main(args: [string]) throws : void {    var client = createClient("accessKeyId","accessKeySecret");    var request = new Dyvmsapi.QueryCallDetailByCallIdRequest{        // 通话的唯一识别ID。        callId = "100625930001^10019107xx",        // 产品ID。        // 11000000300006:语音通知。        // 11010000138001:语音验证码。        // 11000000300005:语音IVR。        // 11000000300004:语音双呼。        // 11000000300009:语音SIP。        // 11030000180001:智能外呼。        prodId = 11000000300004L,        // 指定通话发生的时间,格式为Unix时间戳,单位毫秒。会查询这个时间点对应的一整天的记录。queryDate = 1577255564    };    var response = client.queryCallDetailByCallId(request);    Console.log(response.code);}

Test Cases 自动生成

在 OpenAPI 公布上线以后,如何能够保障 OpenAPI 持续可用就是一个非常重要的问题,如果没有一个保障机制,很可能会出现 OpenAPI 出了问题就无法及时发现,客户的投诉也就随之而来。而且 OpenAPI 和 SDK 也会遇到更新升级等情况,如何能保障这些更新升级不会给正在使用 OpenAPI 的客户造成问题也是 OpenAPI 提供方遇到的一个非常大的挑战。为了解决这些挑战, 就需要 OpenAPI 提供方编写 Test Cases 作为日常的持续集成来检验 OpenAPI 的可用性,但是目前多语言的 SDK 的 Test Cases 大多存在以下的问题:
  • 需要大量的人力去维护 Test Cases ,而且无法保障所有语言的 SDK 都拥有 Test Cases;

  • OpenAPI 的 Test Cases 少且更新频率低,造成了对 OpenAPI 的覆盖面低而无法起到有效的保障作用;

  • 各语言 SDK 的由各语言的开发同学分别维护,所以用例不同步导致不同语言之间的测试结果有所差异。

而Darabonba 的多语言生成能力则可以解决以上所有问题,只需要引用 SDK 在模块仓库中对应的 Darabonba 模块与 Darabonba 官方提供的断言模块 Assert 模块编写对应的 Darabonba Test Cases 即可为各语言 SDK 生成其对应的 Test Cases。通过 Darabonba 自动化生成 Test Cases 不仅可以解决人力不足 Test Cases 很难覆盖各语言 SDK 的情况,而且生成的各语言 Test Cases 标准一致,也可以解决各语言 Test Cases用例不同步造成的测试差异问题。下面是一段通过 Darabonba 编写的测试用例:
import Assert;import Dyvmsapi;import RPC;
static function createClient (accessKeyId : string , accessKeySecret : string) throws : Dyvmsapi{    var config = new RPC.Config{};    config.accessKeyId = accessKeyId;    config.accessKeySecret = accessKeySecret;    return new Dyvmsapi(config);}
static async function TestNumberEqual() throws : void {var client = createClient("accessKeyId","accessKeySecret");    var request = new Dyvmsapi.QueryCallDetailByCallIdRequest{        callId = "100625930001^10019107xx",        prodId = 11000000300004L,        queryDate = 1577255564    };    var response = client.queryCallDetailByCallId(request);    Assert.equal(response.code, 'OK', 'queryCallDetailByCallId is failed!');}
Darabonba 的主要能力是支持到不同风格的 OpenAPI,同时支持多语言的 SDK、Code Sample 目标生成。最终的目的仍然是打通从 OpenAPI 定义到文档、到 SDK、CLI 等 OpenAPI 使用场景下的一致性。提供给用户更统一、专业、一致的使用体验。同时也大幅降低 OpenAPI 提供者用来支持用户的成本,通过自动化的方式,节省精力的同时,还可减少人为参与时导致的错误。

参与贡献

Darabonba 的目标是让用户得到极致的 OpenAPI 体验,所以我们也需要更多的人参与到我们的开源项目来,大家可以按以前的方式参与 Darabonba 的贡献:
  • 参与其他语言生成器的生成,目前 Darabonba 只支持了比较常用的六门语言,还需要更多生成器的支持。

  • 编写更多底层工具模块,使 Darabonba 能够生成更便捷的 SDK。

  • 参与 Darabonba 编译器及 CLI 工具的建设中来。

  • 编写更多 OpenAPI 元数据转换到 Darabonba 的工具。

相关阅读

[1]Darabonba 上手文档
https://github.com/aliyun/darabonba#%E6%96%87%E6%A1%A3
[2]Darabonba CodeSample 示例
https://darabonba.api.aliyun.com/tutorial/demos
[3]Darabonba:支持任意 OpenAPI 网关的多语言 SDK 方案
阿里技术
阿里技术

分享阿里巴巴的技术创新、实战案例、经验总结,内容同步于微信公众号“阿里技术”。

专栏二维码
工程SDKOpenAPI开发工具
暂无评论
暂无评论~