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

阿里技術發表於2020-09-08

如何實現 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 模組倉庫。

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

介面模組

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 多語言 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 方案

相關文章