淺析 Open API 設(shè)計規(guī)范
背景
最近由于業(yè)務(wù)需求,我參與研發(fā)的云產(chǎn)品 CSB 需要對外開放 Open API,原本不是什么難事,因為阿里云內(nèi)部的 Open API 開放機(jī)制已經(jīng)非常成熟了,根本不需要我去設(shè)計,但這次的需求主要是針對一些獨立部署的場景,需要自行設(shè)計一套規(guī)范,那就意味著,需要對 Open API 進(jìn)行一些規(guī)范約束了,遂有此文。
Open API 和前端頁面一樣,一直都是產(chǎn)品的門面, Open API 不規(guī)范,會拉低產(chǎn)品的專業(yè)性。在云場景下,很多用戶會選擇自建門戶,對接云產(chǎn)品的 Open API,這對我們提出的訴求便是構(gòu)建一套成熟的 Open API 機(jī)制。
站在業(yè)務(wù)角度,有一些指導(dǎo)原則,指導(dǎo)我們完善 Open API 機(jī)制:
前端頁面使用的接口和 Open API 提供的接口是同一套接口 任意的前端頁面接口都應(yīng)該有對應(yīng)的 Open API
站在技術(shù)角度,有很多的 API 開放標(biāo)準(zhǔn)可供我們參考,一些開源產(chǎn)品的 Open API 文檔也都非常完善。一方面,我會取其精華,另一方面,要考慮自身產(chǎn)品輸出形態(tài)的特殊性。本文將圍繞諸多因素,嘗試探討出一份合適的 Open API 開放規(guī)范。
Open API 設(shè)計考慮因素
一個完善的 Open API 規(guī)范到底應(yīng)該規(guī)范哪些東西?
站在設(shè)計角度,需要考慮:命名規(guī)范,構(gòu)成規(guī)范,路徑規(guī)范,出入?yún)⒁?guī)范,數(shù)據(jù)類型規(guī)范,統(tǒng)一返回值規(guī)范,錯誤碼規(guī)范,分頁規(guī)范。
站在團(tuán)隊角度,團(tuán)隊中的后端初級中級開發(fā)以及前端研發(fā)是否有足夠的經(jīng)驗,領(lǐng)悟并落地好制定的 API 規(guī)范。同時,伴隨著人員流動,這份 Open API 規(guī)范是否可以很好地被傳承下去。
站在行業(yè)角度,需要考慮提供 Open API 的產(chǎn)品所在的市場是否已經(jīng)成熟,API 風(fēng)格可能已經(jīng)有了對應(yīng)的規(guī)范。
站在產(chǎn)品角度,每個產(chǎn)品適合的 API 風(fēng)格是不同的,下文會著重探討這一角度。
總之,Open API 的設(shè)計是很難形成定論的一個東西,我在介紹自身產(chǎn)品最終采用的 Open API 規(guī)范之前,會先來聊一下大家耳熟能詳?shù)囊恍└拍?,例?restful。
restful 規(guī)范之爭
有人的地方就會有江湖。
有代碼的地方也是如此。
如果你在碼圈混,一定聽說過 restful 規(guī)范:
增刪改查應(yīng)分別聲明為:POST、DELETE、PUT、PATCH、GET
不應(yīng)該出現(xiàn)動詞,動詞統(tǒng)一由 HTTP Method 表示
體現(xiàn)出“資源”的抽象
利用 pathVariable,queryParam,header,statusCode 表達(dá)很多業(yè)務(wù)語義
restful 規(guī)范看似美好,但如果你真正嘗試過落地,一定會遇到一些類似的問題:
以用戶登錄接口為例,此類接口難以映射到資源的增刪改查 以查詢最近 7 個小時內(nèi)的接口請求錯誤率為例,衍生到諸如 graphQL 這類復(fù)雜的查詢場景,往往需要 json 結(jié)構(gòu),GET 是無法實現(xiàn)這一點的,只有 POST 才可以傳遞
基于此,restful 規(guī)范逐漸有了反對的聲音:
強(qiáng)行讓所有的事物都“資源”化一下,有悖于開發(fā)常識,接口不一定都能夠通過簡單的增刪改查來映射 復(fù)雜的查詢語義不一定能夠用 GET 表達(dá)
restful 風(fēng)格的擁躉者,不乏對這些反對言論進(jìn)行抨擊,社區(qū)中不免有“拒絕 restful 風(fēng)格的主要是低水平不思進(jìn)取的架構(gòu)師和前后端程序員們,不會設(shè)計是人的問題,不是規(guī)范的問題”此類的言論。同時對 restful 進(jìn)行了升華:復(fù)雜參數(shù)的檢索問題,在 restful 語義中本就應(yīng)當(dāng)歸類為 post,因為該行為并不是對資源的定位(GET),而是對資源的檢索(POST)
這顯然刺激了 restful 風(fēng)格反對者的神經(jīng),不屑道:呵,愚蠢的 restful 原教旨主義者呀。
不知道你是 restful 的擁躉者還是反對者?亦或是,中立者。
restful 之爭暫時到此為止,這番爭論純屬虛構(gòu),看官不必計較。無論你如何看待 restful,下面我的論述,你都可以作為一個中立者,否則效果減半。
ROA 與 RPC
API 設(shè)計并不只有 restful 一種規(guī)范,在更大的視角中,主流的 API 設(shè)計風(fēng)格其實可以分為
面向資源的設(shè)計,即 ROA(Resource oriented architecture) 面向過程的設(shè)計,即 RPC(Remote Procedure Call)
restful 便是 ROA 風(fēng)格的典型例子,而 RPC 風(fēng)格則相對而言不太容易被大家熟知,但實際上可能大多數(shù)的系統(tǒng)的接口是 RPC 風(fēng)格的,只不過 RPC 風(fēng)格這個概念不太為人所知。
以用戶模塊的 CRUD 為例,對比下兩個風(fēng)格:
ROA 風(fēng)格
創(chuàng)建用戶(POST)
Request:
POST /users
{"name": "kirito", "age": 18}
Response:
HTTP 201 Created
{"id": 1, "name": "kirito", "age": 18}
查詢用戶(GET)
Request:
GET /users/1
Response:
HTTP 200 OK
{"id": 1, "name": "kirito", "age": 18}
查詢用戶列表(GET)
Request:
GET /users
Response:
HTTP 200 OK
{[{"id": 1, "name": "kirito", "age": 18}], "next": "/users?offset=1"}
創(chuàng)建/修改用戶(PUT)
Request:
PUT /users/1
{"name": "kirito", "age": 19}
Response:
HTTP 200 OK
{"id": 1, "name": "kirito", "age": 19}
修改用戶(PATCH)
Request:
PATCH /users/1
{"age": 20}
Response:
HTTP 200 OK
{"id": 1, "name": "kirito", "age": 20}
刪除用戶(DELETE)
Request:
DELETE /users/1
Response:
HTTP 204 No Content
ROA 風(fēng)格和 restful 規(guī)范說明的是一回事,為方便其與 RPC 風(fēng)格接口的對比,特此說明上面示例的一些值得關(guān)注的點:
使用 HTTP 響應(yīng)碼(200,201,204),完成 HTTP 語義與業(yè)務(wù)語義的映射,異常流也出現(xiàn) 404,401 等情況(出于篇幅考慮,本文未做異常流的介紹) PATCH 部分修改資源,請求體是修改部分的內(nèi)容;PUT 創(chuàng)建/修改資源,請求體是新資源全部的內(nèi)容 id 是資源定位符,而 age、name 則為屬性
RPC 風(fēng)格
創(chuàng)建用戶(POST)
Request:
POST /user/createUser
{"name": "kirito", "age": 18}
Response:
HTTP 200 OK
{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 18}}
查詢用戶(POST)
Request:
POST /user/getUser
{"id": 1}
Response:
HTTP 200 OK
{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 18}}
查詢用戶列表(POST)
Request:
POST /user/listUsers
Response:
HTTP 200 OK
{"code": 0, "message": "", "data": {"user": [{"id": 1, "name": "kirito", "age": 18}], "next": "/user/listUsers?offset=1"}}
修改用戶(POST)
Request:
POST /user/modifyUser
{"id": 1, "name": "kirito", "age": 19}
Response:
HTTP 200 OK
{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 19}}
修改用戶名稱(POST)
Request:
POST /user/modifyUserAge
{"id": 1, "age": 20}
Response:
HTTP 200 OK
{"code": 0, "message": "", "data": {"id": 1, "name": "kirito", "age": 20}}
刪除用戶(DELETE)
Request:
POST /user/deleteUser
{"id": 1}
Response:
{"code": 0, "message": ""}
RPC 風(fēng)格不像 restful 一類的 ROA 風(fēng)格存在一些約定俗成的規(guī)范,每個業(yè)務(wù)系統(tǒng)在落地時,都存在差異,故此處只是筆者個人的經(jīng)驗之談,但愿讀者能夠求同存異:
user 為模塊名,不需要像 ROA 風(fēng)格使用復(fù)數(shù)形式 使用明確的動賓結(jié)構(gòu),而不是將 CRUD 映射到 HTTP Method,HTTP Method 統(tǒng)一使用 POST,查詢場景也可以使用 GET 返回值中攜帶 code、message 和 data,來映射響應(yīng)狀態(tài)及響應(yīng)信息,一般可以自行定義 code 的狀態(tài)碼,本文使用 0 標(biāo)識請求成功,message 僅在業(yè)務(wù)響應(yīng)失敗時有意義,data 代表業(yè)務(wù)響應(yīng)結(jié)果
如何選擇 RPC 和 ROA,則需要根據(jù)產(chǎn)品自身的業(yè)務(wù)情況進(jìn)行決策。有如下的指導(dǎo)原則:
有復(fù)雜業(yè)務(wù)邏輯的 API ,無法使用簡單的增、刪、改、查描述時宜使用 RPC 風(fēng)格。 如果業(yè)務(wù)所屬行業(yè)標(biāo)準(zhǔn)要求 restful 風(fēng)格 API 或 ROA 能夠滿足業(yè)務(wù)需求,宜使用 ROA 風(fēng)格。
AWS 主要采用 RPC 風(fēng)格,Azure、Google 主要采用 ROA(restful)風(fēng)格,阿里云 OpenAPI 同時支持 RPC 和 ROA,以 RPC 為主。
盡管規(guī)范是無罪的,但在 ROA 風(fēng)格在實踐過程中,我還是見識過不少“坑”的:
要求資源先行,即先設(shè)計資源,后設(shè)計接口,對軟件開發(fā)流程要求較高 錯誤的 ROA 設(shè)計案例 1:tomcat 等應(yīng)用服務(wù)器在處理 DELETE 方法的 HTTP 請求時,默認(rèn)不允許攜帶 request body,需要顯式開啟,導(dǎo)致刪除失敗。(此案例為設(shè)計者的問題,復(fù)雜的刪除場景,不應(yīng)當(dāng)映射成 DELELE,而應(yīng)改成 POST,DELETE 不應(yīng)當(dāng)攜帶 request body) 錯誤的 ROA 設(shè)計案例 2:restful 路徑中攜帶的參數(shù),可能會引發(fā)正則匹配的問題,例如誤將郵箱作為路徑參數(shù),或者多級路徑匹配的沖突問題(此案例為設(shè)計者的問題,復(fù)雜的查詢場景,不應(yīng)當(dāng)映射成 GET,而應(yīng)改成 POST,path 中只應(yīng)該出現(xiàn)資源定位符,而不應(yīng)當(dāng)攜帶屬性) 響應(yīng)碼為 404 時,較難區(qū)分是真的 path 不存在,還是資源不存在 不利于對接網(wǎng)關(guān)等需要配置路由轉(zhuǎn)發(fā)的場景
CSB 的 Open API 規(guī)范希望滿足以下的需求:
后端開發(fā)設(shè)計接口時,有明確的設(shè)計思路,不至于因為一個接口到底用 POST 還是 GET 實現(xiàn)而糾結(jié),不用花費太多時間在資源的抽象上(這并不是說明資源是不需要被設(shè)計的) 前端開發(fā)對接接口時,能夠較快地與后端協(xié)同,并且利于前端接口的封裝 用戶對接 Open API 時,整體風(fēng)格一致,模塊清晰
綜上,在設(shè)計風(fēng)格選擇上,我計劃采取 RPC 的設(shè)計規(guī)范??偨Y(jié)一下 RPC 風(fēng)格的優(yōu)勢:
API 設(shè)計難度較低,容易落地 阿里云大多數(shù)成熟的 IAAS 層產(chǎn)品使用 RPC 規(guī)范 適合復(fù)雜業(yè)務(wù)場景
一個詳細(xì)的 RPC 接口文檔示例
創(chuàng)建服務(wù)
請求參數(shù)
| 序號 | 字段中文名 | 字段英文名 | 數(shù)據(jù)類型 | 必填 | 說明 |
|---|---|---|---|---|---|
| 1 | 名稱 | name | string | 是 | 顯示名稱 |
| 2 | 協(xié)議 | protocol | string | 是 | 枚舉值:http/grpc/webservice |
| 3 | 負(fù)載均衡 | lb | string | 是 | 枚舉值:random/roundrobin |
| 4 | 上游類型 | upstreamType | string | 是 | 枚舉值:fixed/discovery |
| 5 | 節(jié)點列表 | nodes | array | 否 | upstreamType=fixed 時必填,示例:[{"host": "1.1.1.1","port": "80","weight": "1"}] |
| 6 | 來源id | originId | string | 否 | |
| 7 | 服務(wù)名稱 | serviceName | string | 否 | 注冊中心中的名稱,upstreamType=discovery 時必填 |
| 8 | 服務(wù)描述 | description | string | 否 | |
| 9 | 網(wǎng)關(guān)id | gatewayId | string | 是 |
返回參數(shù)
| 序號 | 字段中文名 | 字段英文名 | 數(shù)據(jù)類型 | 說明 |
|---|---|---|---|---|
| 1 | 響應(yīng)碼 | code | int | 0 標(biāo)識成功;1 標(biāo)識失敗 |
| 2 | 響應(yīng)信息 | message | string | |
| 3 | 響應(yīng)結(jié)果 | data | string | 返回服務(wù) id |
請求示例
POST /service/createService
Request:
{
"name": "httpbin",
"protocol": "http",
"lb": "random",
"upstreamType": "fixed",
"nodes": [
{
"host": "httpbin.org",
"port": "80",
"weight": "1"
}
],
"gatewayId": "gw-1qw2e3e4"
}
Response:
{
"code": 0,
"message": "",
"serviceId": "s-1qw2e3e4"
}
API 命名規(guī)范
API 應(yīng)使用拼寫正確的英文,符合語法規(guī)范,包括單復(fù)數(shù)、時態(tài)和語言習(xí)慣
不能出現(xiàn)多個含義相近但功能無實際差別的 API,如同時存在 /user/getUser 和 /user/describeUser
語言習(xí)慣:禁止使用拼音
如下常見場景的命名規(guī)則是固定的
日期時間類型的參數(shù)應(yīng)命名為 XxxxTime。例如:CreateTime 常用操作名稱規(guī)范
create:創(chuàng)建 modify:變更 delete:刪除 get:獲取單個資源詳情 list:獲取資源列表 establishRelation:建立資源關(guān)系 destroyRelation:銷毀資源關(guān)系
總結(jié)
以本文推崇的一條規(guī)范為例:"所有接口全部使用 POST",這不是為了遷就低水平不思進(jìn)取的架構(gòu)師和前后端程序員們(我在社區(qū)論壇上看到的言論),而是為了提高開發(fā)效率,降低溝通成本,降低運維和錯誤定位成本,把瞎折騰的成本,投入到了其他比如業(yè)務(wù)架構(gòu)設(shè)計,測試體系,線上監(jiān)控,容災(zāi)降級等領(lǐng)域上。
接口規(guī)范也并非我總結(jié)的那樣,只有 RPC 和 ROA,也有一些言論將 GraphQL 單獨歸為一類 API 設(shè)計風(fēng)格,用于復(fù)雜查詢場景,有興趣的同學(xué)可以參考 es 的 API 文檔。
綜上,我計劃采用 RPC 的 API 設(shè)計風(fēng)格。
參考資料
kong:https://docs.konghq.com/gateway/2.8.x/admin-api/
google restful api design:https://cloud.google.com/apis/design?hl=zh-cn
https://www.zhihu.com/question/336797348
