大數據

如何實現 OpenAPI 多語言 SDK 開發?

image.png
如今 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 模塊倉庫。

image.png

接口模塊

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 的安裝說明等信息了。

image.png

基於一組 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 方案

Leave a Reply

Your email address will not be published. Required fields are marked *