1. 問題
演進 REST API 是一項困難的問題——存在許多可供選擇的方案。本文將討論其中一些方案。
2. 什麼是合同?
在其他任何事情之前,我們需要回答一個簡單的問題:API 與客户端之間的合同是什麼?
2.1. URI 是合同的一部分嗎?
首先,讓我們考慮 REST API 的 URI 結構——這是否是合同的一部分? 客户端是否應該記住、硬編碼並依賴 API 的 URI?
如果是這樣,那麼客户端與 REST 服務之間的交互將不再由服務本身驅動,而是由 Roy Fielding 稱之為 出站信息所驅動:
一個 REST API 應該以沒有任何先驗知識,除了初始 URI(書籤)和適合目標受眾的標準媒體類型進入… 這裏的失敗意味着出站信息正在驅動交互,而不是超文本。
因此,URI 不是合同的一部分! 客户端只需要知道一個 URI——API 的入口點。 其他 URI 應該在消費 API 時進行發現。
2.2. 媒體類型是合同的一部分嗎?
關於用於表示資源的媒體類型信息——這些是否是客户端和服務器之間的合同的一部分?
為了成功地消費 API,客户端必須事先了解這些媒體類型。 事實上,這些媒體類型的定義代表了整個合同。
因此,REST 服務應該將重點放在這裏:
一個 REST API 應該花費幾乎所有的描述性努力來定義用於表示資源和驅動應用程序狀態的媒體類型(s),或者定義現有標準媒體類型的擴展關係名稱和/或超文本標記化,以便支持現有標準媒體類型。
因此,媒體類型定義是合同的一部分,並且是客户端消費 API 的先驗知識。 這也是標準化發揮作用的地方。
我們現在對合同有了大致的瞭解,接下來我們來解決版本問題。
3. 高級選項
接下來,我們討論一下 REST API 版本化的高級方法:- URI 版本化 – 使用 URI 空間中的版本指示符進行版本化媒體類型版本化 – 使用資源的表示形式進行版本化當我們引入 URI 空間中的版本時,資源的表示形式被認為是不可變的。 因此,當 API 中需要引入更改時,需要創建一個新的 URI 空間。
例如,假設一個 API 發佈了以下資源 – 用户和權限:
http://host/v1/users
http://host/v1/privileges現在,我們考慮一下 users API 中的一個破壞性變更需要引入第二個版本:
http://host/v2/users
http://host/v2/privileges當我們版本化 Media Type 並擴展語言時,我們會基於此標頭進行 內容協商。 REST API 將使用自定義 供應商自定義 MIME 類型,而不是諸如 application/json 這樣通用的 MIME 類型。 我們將對這些 MIME 類型進行版本化,而不是 URI。
例如:
===>
GET /users/3 HTTP/1.1
Accept: application/vnd.myname.v1+json
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.myname.v1+json
{
"user": {
"name": "John Smith"
}
}我們可以查閲這篇關於“REST API 自定義媒體類型”的文章,以獲取更多相關信息和示例。
這裏需要理解的關鍵是,客户端不會對響應的結構做出任何假設,除了在媒體類型中明確定義的範圍。
因此,通用媒體類型並不是理想的選擇。它們不提供足夠的語義信息,並迫使客户端需要使用額外的提示來處理資源的實際表示。
例外情況是使用其他方式來唯一標識內容的語義——例如 XML 模式。
4. 優點與缺點
現在我們對客户與服務之間的合同中“部分”的內容以及 API 版本選項的高層概述都有了清晰的概念,接下來我們來討論每種方法的優點和缺點。
首先,在 URI 中引入版本標識符會導致 URI 佔用空間非常大。 這是因為任何已發佈 API 中的任何破壞性更改都將引入整個 API 的全新表示樹。 隨着時間的推移,這成為維護的負擔,並且也給客户端帶來問題——因為客户端現在有更多選項可供選擇。
在 URI 中引入版本標識符也極其不靈活。 無法簡單地演化單個資源或整體 API 的小部分。
正如我們之前提到的,這是一種“全或無”的方法。如果 API 的一部分移動到新版本,則整個 API 也必須隨之移動。 這也使得從 v1 升級到 v2 的客户端變得是一項重大任務——這導致升級速度較慢,舊版本的停用期也更長。
HTTP 緩存在版本化方面也構成了主要問題。
從代理緩存的角度來看,每種方法都有各自的優點和缺點。 如果 URI 被版本化,則緩存需要保持每個資源的多個副本——一個用於每個 API 版本。 這給緩存帶來了負載,並降低了緩存命中率,因為不同的客户端將使用不同的版本。
此外,某些緩存失效機制將不再起作用。 如果媒體類型被版本化,則客户端和服務器都需要支持 Vary HTTP 標頭 以指示多個版本正在進行緩存。
然而,從客户端緩存的角度來看,版本化媒體類型的方法比 URI 包含版本標識符的方法需要稍微多一些工作,因為當其鍵是一個 URL 而不是一個媒體類型時,緩存某物更容易。
讓我們以定義一些目標(直接來自 API 演化)結束這一部分:
- 保持兼容性更改在名稱中
- 避免新主要版本
- 使更改向後兼容
- 考慮向前兼容性
5. 可能的API變更
接下來,我們考慮一下REST API中可能出現的變更類型,這些變更將在下面介紹。
- 表示格式變更
- 資源變更
5.1. 添加到資源的表示
媒體類型的文檔格式應考慮到向前兼容性。具體而言,客户端應忽略它不理解的信息(JSON在這方面比 XML 更好)。
現在,向資源的表示中添加信息將不會破壞現有的客户端,如果這些客户端已正確實現。
為了繼續我們之前的示例,在 用户 資源的表示中添加 金額 將不會引入破壞性變更。
{
"user": {
"name": "John Smith",
"amount": "300"
}
}5.2. 刪除或更改現有表示形式
刪除、重命名或對現有表示形式進行總體重新結構化都將導致客户端出現破壞性變更。這是因為客户端已經理解並依賴於舊格式。
這就是內容協商(Content Negotiation)發揮作用的地方。對於此類更改,我們可以添加一個新的供應商 MIME 媒體類型。
讓我們繼續使用之前的示例。假設我們想要將 name 字段中的 name 字段分解為 firstname 和 lastname 字段:
===>
GET /users/3 HTTP/1.1
Accept: application/vnd.myname.v2+json
<===
HTTP/1.1 200 OK
Content-Type: application/vnd.myname.v2+json
{
"user": {
"firstname": "John",
"lastname": "Smith",
"amount": "300"
}
}因此,這對於客户端來説確實代表着不兼容的變化——客户端需要請求新的表示形式並理解新的語義。然而,URI 空間將保持穩定,不會受到影響。
5.3. 主要語義變更
這些變更涉及資源本身的含義、它們之間的關係,或者它們在後端映射的內容。
這類變更可能需要引入新的媒體類型,或者需要發佈一個新的資源,緊鄰舊資源並利用鏈接指向它。
雖然這聽起來有點像在 URI 中再次使用版本標識符,但關鍵的區別在於,新的資源獨立於其他任何資源發佈,不會在根級別分叉整個 API。
REST API 應該遵循 HATEOAS 約束。根據這一約束,大部分 URI 應該由客户端通過發現來獲取,而不是硬編碼。更改這樣的 URI 不應被視為不兼容的變更。新的 URI 可以替換舊的 URI,客户端將能夠重新發現 URI 並繼續正常工作。
值得注意的是,儘管使用 URI 中的版本標識符存在上述所有問題,但它在某種程度上仍然是 RESTful 的。
6. 結論
本文旨在概述 演進 RESTful 服務 這一複雜且多樣的問題。我們討論了兩種常見的解決方案,各自的優缺點,以及如何在 REST 的背景下進行思考。
本文的結論是,支持第二種解決方案——版本化媒體類型,同時考察對 RESTful API 的潛在影響。
7. 進一步閲讀
通常,這些閲讀資源會在文章中進行鏈接,但在此處,好的資源實在太多了。