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 成熟度模型。
RESTful 成熟度模型。
根據自己的應用場景選擇對應的成熟度模型,一般來說系統成熟度模型在 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設計表格
資源 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標準
- 是否存在越權訪問
更多精彩洞見,請關注微信公眾號:ThoughtWorks洞見
文/ThoughtWorks少個分號






