API 本身的含義指應用程式接口,包括所依賴的庫、平台、作業系統提供的能力都可以叫做 API。我們在讨論微服務場景下的 API 設計都是指 WEB API,一般的實作有 RESTful、RPC等。API 代表了一個微服務執行個體對外提供的能力,是以 API 的傳輸格式(XML、JSON)對我們在設計 API 時的影響并不大。
API 設計是微服務設計中非常重要的環節,代表服務之間互動的方式,會影響服務之間的內建。 通常來說,一個好的 API 設計需要滿足兩個主要的目的:
平台獨立性。 任何用戶端都能消費 API,而不需要關注系統内部實作。API 應該使用标準的協定和消息格式對外部提供服務。傳輸協定和傳輸格式不應該侵入到業務邏輯中,也就是系統應該具備随時支援不同傳輸協定和消息格式的能力。
系統可靠性。 在 API 已經被釋出和非 API 版本改變的情況下,API 應該對契約負責,不應該導緻資料格式發生破壞性的修改。在 API 需要重大更新時,使用版本更新的方式修改,并對舊版本預留下線時間視窗。
實踐中發現,API 設計是一件很難的事情,同時也很難衡量設計是否優秀。根據系統設計和消費者的角度,給出了一些簡單的設計原則。
使用成熟度合适的 RESTful API
RESTful 風格的 API 具有一些天然的優勢,例如通過 HTTP 協定降低了用戶端的耦合,具有極好的開放性。是以越來越多的開發者使用 RESTful 這種風格設計 API,但是 RESTful 隻能算是一個設計思想或理念,不是一個 API 規範,沒有一些具體的限制條件。
是以在設計 RESTful 風格的 API 時候,需要參考 RESTful 成熟度模型。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiIXZ05WZj91YpB3IwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxSP9EVWyYUbhpXSU1UMo1mY2x2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwQDOyUTNxYTMxMDOwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
根據自己的應用場景選擇對應的成熟度模型,一般來說系統成熟度模型在 Level 2左右。
避免簡單封裝
API應該服務業務能力的封裝,避免簡單封裝讓API徹底變成了資料庫操作接口。例如标記訂單狀态為已支付,應該提供形如POST /orders/1/pay這樣的API。而非PATCH /orders/1,然後通過具體的字段更新訂單。
因為訂單支付是有具體的業務邏輯,可能涉及到大量複雜的操作,使用簡單的更新操作将業務邏輯洩漏到系統之外。同時系統外也需要知道訂單狀态 這個内部使用的字段。
更重要的是,破壞了業務邏輯的封裝,同時也會影響其他非功能需求。例如,權限控制、日志記錄、通知等。
關注點分離
好的接口應該做到不多東西,不少東西。 怎麼了解呢?在使用者修改密碼和修改個人資料的場景中,這兩個操作看起來很類似,然後設計API的時候使用了一個通用的/users/1/udpateURI。
然後定義了一個對象,這個對象可能直接使用了User這個類:
{
"username": "使用者名",
"password": "密碼"
}
這個對象在修改使用者名的時候, password是不必要的,但是在修改密碼的操作中,一個password字段卻不夠用了,可能還需要
confirmPassword。
于是這個接口變成:
{
"username": "使用者名",
"password":"密碼",
"confirmPassword":"重複密碼"
}
這種類的複用會給後續維護的開發者帶來困惑,同時對消費者也非常不友好。合理的設計應該是兩個分離的 API:
// POST /users/{userId}/password
{
"password":"密碼",
"confirmPassword":"重複密碼"
}
// PATCH /users/{userId}
{
"username":"使用者名",
"xxxx":"其他可更新的字段"
}
對應的實作,在 Java 中需要定義兩個 DTO,分别處理不同的接口。這也展現了面向對象思想中的關注點分離。
完全窮盡,彼此獨立
API 之間盡量遵守完全窮盡,彼此獨立 (MECE) 原則,不應該提供互相疊加的 API。例如訂單和訂單項這兩個資源,如果提供了形如 PUT /orders/1/order-items/1 這樣的接口去修改訂單項,接口 PUT /orders/1 就不應該具備處理某一個 order-item 的能力。
這樣的好處是不會存在重複的 API,造成維護和了解上的複雜性。如何做到完全窮盡和彼此獨立呢?
簡單的方法是使用一個表格設計 API,标出每個 URI 具備的能力。
API設計表格
API設計表格
資源 URL 設計來源于 DDD 領域模組化就非常簡單了,聚合根作為根 URL,實體作為二級 URI 設計。聚合根之間應該徹底沒有任何聯系,實體和聚合根之間的責任應該明确。
産生這類問題的根源還是缺乏合理的抽象。如果存在 API 中可以通過使用者組操作使用者,通過使用者的 URI 操作使用者屬于的使用者組,這其中的問題是缺少了成員這一概念。使用者組下面的本質上并不是使用者,而是使用者和使用者組的關系,即成員。
版本化
一個對外開放的服務,極大的機率會發生變化。業務變化可能修改 API 參數或響應資料結構,以及資源之間的關系。一般來說,字段的增加不會影響舊的用戶端運作。但是當存在一些破壞性修改時,就需要使用新的版本将資料導向到新的資源位址。
版本資訊的傳輸,可以通過下面幾種方式
URI 字首
Header
Query
比較推薦的做法是使用 URI 字首,例如/v1/users/表達擷取 v1 版本下的使用者清單。
常見的反模式是通過增加 URI 字尾來實作的,例如/users/1/updateV2。這樣做的缺陷是版本資訊侵入到業務邏輯中,對路由的統一管理帶來不便。
使用 Header 和 Query 發送版本資訊則較為相似,不同之處在于,使用 URI 字首在 MVC 架構中實作相對簡單,隻需要定義好路由即可。使用 Header 和 Query 還需要編寫額外的攔截器。
合理命名
設計 API 時候的命名涉及多個地方:URI、請求參數、響應資料等。通常來說最主要,也是最難的一個是全局命名統一。
其次,命名需要注意這些:
盡可能和領域名詞保持一緻,例如聚合根、實體、事件等
RESTful 設計的 URI 中使用名詞複數
盡可能不要過度簡寫,例如将 user 簡寫成usr
盡可能使用不需要編碼的字元
用領域名詞來對 API 設計命名不是一件特别難的事情。識别出的領域名詞可以直接作為 URI 來使用。如果存在多個單詞的連接配接可以使用中橫線,例如/orders/1/order-items
安全
安全是任何一項軟體設計都必須要考慮的事情,對于 API 設計來說,暴露給内部系統的 API 和開放給外部系統的 API 略有不同
。
内部系統,更多的是考慮是否足夠健壯。對接收的資料有足夠的驗證,并給出錯誤資訊,而不是什麼資訊都接收,然後内部業務邏輯應該邊界值的影響變得莫名其妙。
而對于外部系統的 API 則有更多的挑戰。
錯誤的調用方式
接口濫用
浏覽器消費 API 時因安全漏洞導緻的非法通路
是以設計 API 時應該考慮響應的應對措施。針對錯誤的調用方式,API 不應該進入業務處理流程,及時給出錯誤資訊;對于接口濫用的情況,需要做一些限速的方案;對于一些浏覽器消費者的問題,可以在讓 API 傳回一些安全增強頭部,例如:X-XSS-Protection、Content-Security-Policy 等。
API 設計評審清單
URI 命名是否通過聚合根和實體統一
URI 命名是否采用名詞複數和連接配接線
URI 命名是否都是單詞小寫
URI 是否暴露了不必要的資訊,例如/cgi-bin
URI 規則是否統一
資源提供的能力是否彼此獨立
URI 是否存在需要編碼的字元
請求和傳回的參數是否不多不少
資源的 ID 參數是否通過 PATH 參數傳遞
認證和授權資訊是否暴露到 query 參數中
參數是否使用奇怪的縮寫
參數和響應資料中的字段命名統一
是否存在無意義的對象包裝 例如{“data”:{}’}
出錯時是否破壞約定的資料結構
是否使用合适的狀态碼
是否使用合适的媒體類型
響應資料的單複是否和資料内容一緻
響應頭中是否有緩存資訊
是否進行了版本管理
版本資訊是否作為 URI 的字首存在
是否提供 API 服務期限
是否提供了 API 傳回所有 API 的索引
是否進行了認證和授權
是否采用 HTTPS
是否檢查了非法參數
是否增加安全性的頭部
是否有限流政策
是否支援 CORS
響應中的時間格式是否采用ISO 8601标準
是否存在越權通路