原文鏈接:Web API design best practices - Azure Architecture Center | Microsoft Docs
現在網絡上已經有了很多服務商的公開API,可以讓各類客户端調用,那麼怎樣才是一個設計優良的web API呢?一般來講應該具備以下標準:
- 平台無關性:使用API的可以是任何客户端,它們不用關心API是怎麼實現的。這就要求了交互時使用到的協議要標準化,並且要存在一種機制,能確保客户端和服務提供方在數據格式上達成一致。
- 服務演化: web API可以自行更新迭代自己的功能,使用它的客户端不用做出任何修改就能繼續使用這些API。服務端提供的所有功能要具備可發現性,使得客户端能充分使用到它們。
下面來説説設計web API時要考慮的一些關鍵問題。
什麼是REST?
在2000年,Roy Fielding提出使用表述性狀態轉移(Representational State Transfer ,簡稱REST)來設計網絡服務的構建方法。REST是一種基於超媒體來構建分佈式系統的架構風格,它不應關注底層服務如何,也不用跟HTTP綁定,不過大部分REST API的實現還是基於了HTTP協議。讓我們先關注下如何使用HTTP來設計REST API接口。
在HTTP上使用REST的好處是它是一個有公開標準的協議,不需要這些API的提供方或使用方依賴任何特定實現方案,服務方和使用方可以用任意語言、工具包來提供REST服務實現,或創建HTTP請求以及解析HTTP響應報文。
基於HTTP設計RESTful API的主要原則有:
- REST API的核心是資源,可以是客户能訪問到的任意物體、數據或服務
-
每種資源都要有一個獨一無二的URI來作為唯一標識符定位到該資源。比如一種客户訂單可以這樣描述:
https://adventure-works.com/orders/1 -
客户通過交換資源表述來與服務交互。許多web API使用JSON作為數據轉換格式。例如,一個針對上面URI的GET請求可能得到如下內容:
{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1} - REST API使用一套統一的接口,來幫助解耦調用端和服務實現。在HTTP上構建REST API時,統一接口使用標準的HTTP動詞來執行資源的操作,經常使用到的操作有GET,POST,PUT,PATCH和DELETE。
- REST API是無狀態的。由於發出的HTTP請求是獨立且無序的,因此沒辦法在請求間保持這種臨時會話狀態。能存儲信息的地方只能是API資源自身,而每個請求應當是原子操作。這樣的約束要求客户和特定服務器間不能保留任何關聯,從而使得服務具備了高度的可伸縮性。任意的服務器可以處理任何的客户請求。但是,其他因素可能會限制這種可伸縮性,比如很多服務都要寫數據到後端存儲中,而這種單一存儲就很難擴展。關於這種數據存儲該如何擴展的策略方法,可以參考Horizontal, vertical, and functional data partitioning.
-
REST API的重要驅動核心是其表述內容中包含的超媒體鏈接。舉個例子,下面展示了一個包含訂單信息的JSON格式內容,它包含了一些鏈接,用來獲得或更新與該訂單關聯的客户數據。
{ "orderID":3, "productID":2, "quantity":4, "orderValue":16.60, "links": [ {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" }, {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" } ] }在2008年,Leonard Richardson提出了一個web API成熟度模型:
- Level 0:僅有一個URI,用來處理所有POST請求 .
- Level 1:不同的資源提供單獨的URI請求路徑.
- Level 2:使用HTTP的method來定義在資源上的不同操作.
-
Level 3:使用了超媒體(HATEOS,下面會提到)
圍繞資源設計API
web API需要暴露一些業務實體, 拿電商系統來舉例,主要的實體基本上就是客户和訂單了。可以發送一個包含了訂單信息的HTTP POST請求來創建一個訂單,而響應報文應當告知這個訂單是否創建成功。請記得,提供出來的這些資源操作URI應儘量使用名詞(資源名)來描述,而非動詞(操作動作)。
https://adventure-works.com/orders // Good https://adventure-works.com/create-order // Avoid一個所謂的資源不一定要是一個真實的物理實體,比如對於訂單來説,它可能在內部實現上用到了數張關係型數據庫中的表,但是對於客户而言,它就是一個單獨的實體。不要創建一些僅僅是把數據庫對象做了個簡單鏡像展示的API。REST的目標是描述實體和可在其上執行的操作,而客户不應接觸到內部實現。
實體通常會有其集合形態(一組訂單、一些顧客)。相比與其中的單一實體,一個集合也是一種單獨的資源,也應提供其獨有的訪問URI。例如下面這個URI就表示了一組訂單:
https://adventure-works.com/orders
使用HTTP GET調用該URI,就會得到一組數據,而其中每一個元素也應有其單獨的訪問URI,通過HTTP GET方法訪問它們,就能得到每一個元素的詳細信息。
採用一個統一的命名規範來定義這些URI。通常提供的內容是集合時應該使用名字的複數形式。一個很好的做法是在集合和元素間提供層級結構,比如使用/customers來代表客户集合,而/customers/5則指向ID是5的這個單獨客户。這樣做會讓你的web API很直觀。並且很多web API框架提供了參數URI路徑的支持,所以你可以使用/customers/{id}來做資源路由。
另外需要考慮的是不同資源間的關係,和你應該如何暴露出這種聯繫。例如我們知道/customers/5/orders應該代表了顧客5的所有訂單,但是如果路徑是從訂單開始,列出其關聯的所有客户的URI就可能是/orders/99/customer。然而,這種模式的過分擴展實現起來會很繁雜笨重。一個更好的方案是,在HTTP的響應報文體中提供關聯資源的導航鏈接。我們會在下面章節展開詳細探討。
在一些複雜系統中,像/customers/1/orders/99/products這樣的提供給客户端多級關係路徑訪問的URI,看起來似乎很方便,但是這樣的複雜級別會讓維護變得困難,並且難以在將來調整資源間的層級關係。因此,好的做法是讓資源URI儘量簡單明瞭,一旦應用程序有了對資源的引用,就應該可以使用這個引用來查找與該資源相關的項。比如前面查找客户1所有訂單的查詢URI可以替換為/customers/1/orders,然後通過/orders/99/products來得到訂單中的所有商品。
注意:不要使用比/collection/item/collection更復雜的查詢URI。
另外要考慮每次請求對服務器的負載影響。請求越多,負載越高。因此,不要提供太多過於細小瑣碎的資源接口。這樣的接口可能需要客户端執行多次請求才能得到想到的數據。可以考慮把一些相關數據組合到一個資源中,以使得一次查詢請求便能滿足需求。不過,你還是得做好權衡,來避免拿到過多的無用數據。另外,檢索的數據過大,還會讓請求變得緩慢,併產生額外的帶寬成本。跟多關於性能錯誤模式的介紹,可參看Chatty I/O 和 Extraneous Fetching.
要避免讓web API和底層數據庫產生依賴關係,例如,當你使用了關係數據庫存儲數據,web API並不需要把所有的表都暴露為資源集合,這樣設計很不好,合適的做法是,把web API當成是數據庫的抽象,或者可以考慮使用一個映射層來建立數據庫和web API的映射關係。這樣客户端就可以與底層數據庫隔離開來,從而在底層數據表變化時不受到影響。
最後,可能不太容易做到將所有web API實現的操作找到合適的資源描述來建立映射關係,一些場景裏,發出的HTTP請求只是用來執行了某個函數,然後將結果作為HTTP響應報文返回,比如用作簡單加減計算的web API,可能會使用偽資源作為URI,並將查詢字符串當作計算的參數。例如,一個URI為/add?operand1=99&operand2的GET請求,會得到一個報文內容為100的返回結果。不過最好還是有節制的使用這種形式的URIs。
通過HTTP方法定義API操作
HTTP協議定義了一些有特殊語義的請求方法,在RESTful接口中最常用到的有:
- GET 獲取指定URI表述的資源數據,響應報文裏包含請求資源的詳細內容。
- POST 使用指定URI創建資源對象,並返回對象的詳細內容。注意POST有時也用來觸發實際上並不會創建資源的操作。
- PUT 根據請求消息的不同指定,創建或更新指定URI的資源對象
- PATCH 可以在請求體中指定一系列變更內容,來執行對應資源的部分更新動作
- DELETE 使用指定URI刪除資源
資源是集合還是單獨個體,會使得請求的效果有所不同。下面總結了一些電商系統中常見的RESTful實現慣例。有些請求沒有處理 - 這取決於場景。
| Resource | POST | GET | PUT | DELETE |
|---|---|---|---|---|
| /customers | 創建一個新客户 | 檢索所有客户 | 批量更新客户 | 刪除所有客户 |
| /customers/1 | 無 | 檢索客户1的詳細信息 | 更新客户1的詳細信息(若存在) | 刪除客户1 |
| customers/1/orders | 為客户1創建一個新訂單 | 檢索客户1的所有訂單 | 批量更新客户1的所有訂單 | 刪除客户1的所有訂單 |
強調下POST、PUT、PATCH方法的差異:
- POST請求會創建一個資源,服務端會給新創建的資源分配一個URI,並將其返回給客户端。在REST模式下,經常會對集合使用POST操作,新資源被創建後便被加入到集合中。POST請求也用在提交數據給現存資源來完成一些操作,過程中並不會有新資源被創建。
- PUT請求用來創建新資源或更新已經存在的資源。客户端指定資源的URI,並在請求體中包含資源的完整表示。如果指定的資源已經存在,它便會被請求內容替換掉,否則便會創建一個新資源(如果服務端支持該操作)。PUT請求更多的用在單獨資源上,而不是集合資源,比如一個特定的客户。服務端通常會支持PUT請求的更新操作,但不一定會支持創建資源,這取決於客户端是否可以在資源不存在時分派新URI給它。如果不支持,請使用POST來創建新資源,然後用PUT或PATCH來更新它。
- PATCH請求用來執行已存在資源的部分更新。客户端指定資源的URI,並在請求體中攜帶需要執行變更的內容集合。這種做法會比PUT更有效,鑑於客户端只需要發送變更的部分,而不是整個資源數據。從技術上來説PATCH也可以創建資源,通過把所有資源變更數據都指定為null來實現,不過最終要看服務端是否支持這種做法。
PUT請求必須是冪等的。如果客户端發送了多次同樣的請求,得到的結果應該是一樣的(同樣的資源被變更為了同樣的內容)。POST和PATCH則沒有這樣的要求。
遵守HTTP語義
本節介紹HTTP規範下需要考慮的一些常見注意事項,沒有涵蓋所有可能細節和場景,如有疑問請參閲HTTP規範。
Media types
上面提到,客户端和服務端會交換資源數據。比如在POST請求中,請求體需包含要創建資源的表述,而GET請求中,響應報文體中也會包含檢索資源的表述內容。
在HTTP協議裏,格式通過media types來指定,也被稱為MIME types。對於非二進制數據,大部分web API提供了JSON(media type = application/json)或者XML(media type = application/xml)格式的支持。
在請求或響應中通過Content-Type消息頭來指定數據格式。下面有一個包含了JSON數據的POST請求示例:
POST https://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57
{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}
如果服務端不支持請求的media type,那麼它應該返回415這個HTTP狀態碼(Unsupported Media Type)。
客户端請求可以攜帶一個Accept的消息頭來向服務端表明它可以接受報文的media type列表,例如:
GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json
如果服務端不能滿足列出的media type請求,它應該返回HTTP狀態碼406(Not Acceptable)。
GET方法
GET請求成功後通常應返回狀態碼200(OK)。如果請求的資源不存在,則應返回404(Not Found)。
POST方法
如果POST請求創建了新資源,它應返回HTTP狀態碼201(Created)。新創建資源的URI應在響應報文頭Location中攜帶,而報文體則應包含該資源的表述。
如果方法做了一些處理,但是並沒有創建新資源,它同樣可以返回200狀態碼,並把處理結果放在響應報文中,而當並沒有任何處理結果需要返回時,可以使用204狀態碼(No Content)來代替,並返回空響應報文。
如果客户端在請求中攜帶了無效數據,服務端應該返回狀態碼400(Bad Request),響應報文中則可以放入關於錯誤的詳細説明信息,或者提供一個鏈接以用來查看更多信息。
PUT方法
如果PUT方法創建了新的資源,它會返回201狀態碼(Created),跟POST一樣。如果它更新了一個存在的資源,可以返回200(OK)或者204(No Content)。某些情況下,可能無法更新指定的資源,這時可以考慮返回HTTP狀態碼409(Conflict)。
可以考慮提供一個批量更新PUT方法,在方法體裏放上要更新的所有資源內容,並使用URI指明要更新的資源集合。這樣能降低網絡開銷和提高性能。
PATCH方法
使用PATCH請求時,客户端使用一種 補丁文件 的格式,向已經存在的數據資源發送更新請求。服務端會使用這個補丁文件處理更新。補丁文件並不需要描述資源的所有內容,只需要告訴更新哪些部分即可。PATCH方法的規範 (RFC 5789)並沒有定義這個補丁文件要有什麼樣的特定格式,格式需要根據請求的media type而定。
JSON應該是web API屆最通用的數據格式了。而patch方法用到的基於JSON的補丁文件格式有兩個,叫做JSON patch和JSON merge patch。
JSON merge patch相對簡單一點。它的結構就像是原始資源對象的JSON形式內容,只是它包含的僅僅是需要更新或者添加的數據字段集合,在補丁文件中通過將字段指定為 null 甚至還可以刪除字段(不過不適用於原始資源可以顯式的包含'null'值的情況)
舉個例子,如果原始資源對象的JSON形式內容為:
{
"name":"gizmo",
"category":"widgets",
"color":"blue",
"price":10
}
可能對應的更新文件的內容會是這樣:
{
"price":12,
"color":null,
"size":"small"
}
這個文件告訴服務端,要更新 price ,刪掉 color ,並添加 size 字段,同時 name 和 category 字段沒有任何改動。有關JSON merge patch的更多具體內容可以參考 RFC 7396。它對應的media type為 application/merge-patch+json。
Merge patch並不適用於原本資源中包含顯式'null'值的情況,因為補丁文件中的null有特殊含義(代表刪除)。並且補丁文件不能指定更新被執行的順序,不過這個有沒有具體影響要看數據對象的處理邏輯。在RFC 6902中定義的JSON patch,相對就靈活一些。它可以指明具體要執行的操作序列,操作的類型可以是增加、刪除、替換、拷貝和測試(用來驗證特定值)。它的media type是 application/json-patch+json。
下表列出了一些處理PATCH請求時可能會遇到的一些典型錯誤條件,和對應適當的HTTP狀態碼。
| Error condition | HTTP status code |
|---|---|
| The patch document format isn't supported. | 415 (Unsupported Media Type) |
| Malformed patch document. | 400 (Bad Request) |
| The patch document is valid, but the changes can't be applied to the resource in its current state. | 409 (Conflict) |
DELETE方法
如果一個刪除請求被成功執行,web服務端應該返回一個204狀態碼(No Content),這代表着執行已經成功,響應報文不需要有任何內容。如果請求的資源不存在,應該得到一個HTTP 404(Not Found)。
異步操作
有時一個POST、PUT、PATCH或者DELETE操作可能會需要執行一段時間,如果在成功相應之前一直等待,過長的延遲可能不太好接受。這時可以考慮將操作改成異步來執行。返回一個202(Accepted)狀態碼來表明請求已經接受並在處理中,但是還沒有完成。
你應該暴露一個端點,來提供異步請求執行狀態的查詢,這樣客户端就能輪詢它來監測狀態。可以在202報文頭中包含這個狀態端點的地址,如:
HTTP/1.1 202 Accepted
Location: /api/status/12345
如果客户端向該端點發起了一個GET請求,響應報文中應該包含操作的執行狀態。另外,也可以提供其他一些信息,比如預計要消耗的時間和一個可以取消操作的鏈接。
HTTP/1.1 200 OK
Content-Type: application/json
{
"status":"In progress",
"link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
}
如果異步操作創建了新的資源,那麼這個狀態端點應該在操作執行完成後返回一個303(See Other),並在響應報文頭信息中提供新資源的訪問地址。
HTTP/1.1 303 See Other
Location: /api/orders/12345
更多相關信息,可參考 Asynchronous Request-Reply pattern.
數據過濾和分頁
提供一個可以查詢資源集合的URI,可能會出現客户端其實只需要一小部分數據,但卻發起了一個很大數據量的請求。比如,客户程序需要查詢所有成本大於某個值的所有訂單,那麼可能它會先請求 /orders 這個URI拿到所有訂單數據,然後在客户端把這些數據作一下過濾。很顯然這樣處理非常的不效率,而且浪費了貸款和服務器算力。
相對的,這個API也可以提供請求URI時攜帶查詢字符串的支持,比如 /orders?minCost=n。然後服務端解析處理這個 minCost 查詢項,並返回過濾後的結果。
一個對資源集合的GET請求總是有可能會返回一大堆數據,所以最好在你的web API中提供對單次請求返回結果數量的限制。可以考慮提供一個最大數量請求查詢參數,來指明能得到的最大結果數量,同時需要提供一個偏移量參數,例如:
/orders?limit=25&offset=50
同時為了避免可能受到的拒絕服務攻擊,要對這個最大數量參數設置個上限。另外,為了協助客户應用程序完成分頁配置,服務端對GET請求返回分頁數據時,要在響應報文中包含一些元數據,用來標明資源可用的總頁數等。
類似的,還可以提供一個sort參數用來對請求數據排序,可以使用結果字段中的任意一個,比如 /orders?sort=ProductID。不過這種做法有一個壞處,因為有些緩存實現用查詢字符串作為緩存的key,sort參數的調整可能會讓之前的緩存失效。
在查詢數據的字段數量特別多時,還可以對返回字段作一下限制。提供一個支持逗號分隔的查詢字符串參數來表示需要的字段列表,比如 /orders?fields=ProductID,Quantity。
對每一個支持的查詢字符串參數,提供一個有意義的默認值。例如,如果支持分頁,最好將數據條數默認為10,偏移量(頁碼)默認為0。如果支持排序,將sort列默認為資源的主鍵,如果支持投影(可選查詢列),將參數默認為所有列。
大型二進制文件的部分響應
有些資源可能包含二進制字段,比如圖片和文件。這些內容在不穩定網絡環境下傳輸可能會出現問題,比如連接中斷,或者處理時間過長,為了克服這些問題,可以考慮分塊獲取。首先,API需要對GET請求中的Accept-Ranges這個用來請求大文件資源的消息頭提供支持,這個消息頭表明GET操作支持部分請求,客户端可以發起一個指定了字節數範圍的GET請求,來得到一部分資源數據。
同時,最好也實現對這些資源的HTTP HEAD請求處理。HEAD請求類似於GET,只不過它只返回描述資源的HTTP消息頭,而消息體是空的。客户程序可以發出HEAD請求來判斷是否需要使用GET部分請求方式獲取資源。例如:
HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1
對應的響應報文為:
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580
Content-Length 報文頭給出了資源的總大小,而 Accept-Ranges 報文頭則表明對應的GET操作支持部分結果的請求。有了這些信息,客户程序便可以把圖片分成小塊獲取。第一個請求使用了 Range 頭獲取了2500個字節:
GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499
響應報文使用HTTP狀態碼206來表明這是完整內容的一部分。Content-Length 報文頭説明了消息體內的實際字節數(而非資源的完整大小),Content-Range 報文頭則表明這是整個資源的哪一部分(完整4580中的0-2499這部分)
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580
[...]
客户程序可以發起後續請求來獲得資源的剩餘部分。
使用資源導航 (HATEOAS)
REST有一個推崇的概念,應當在不需要先了解URI方案的情況下,做到對整個資源集的導航。每一個HTTP GET請求的響應報文,都應該包含一系列超鏈接信息,用來獲取跟請求對象有直接關係的資源,並提供這些資源上的可用操作説明。這個原則被稱為 HATEOAS ,即 Hypertext as the Engine of Application State。這個系統實際上是一種有限狀態機,請求的響應報文包含了從一種狀態轉移到另一種狀態的必要信息,跟狀態轉換無關的內容則不應包含在內。
當前並沒有針對HATEOAS原則的通用建模標準,本節示例只展示了一種特定用途下的方案。
比如,處理訂單和客户關係時,可以在訂單的表現上包含一些鏈接,用來標識跟這個訂單相關客户的一些可用操作。比如:
{
"orderID":3,
"productID":2,
"quantity":4,
"orderValue":16.60,
"links":[
{
"rel":"customer",
"href":"https://adventure-works.com/customers/3",
"action":"GET",
"types":["text/xml","application/json"]
},
{
"rel":"customer",
"href":"https://adventure-works.com/customers/3",
"action":"PUT",
"types":["application/x-www-form-urlencoded"]
},
{
"rel":"customer",
"href":"https://adventure-works.com/customers/3",
"action":"DELETE",
"types":[]
},
{
"rel":"self",
"href":"https://adventure-works.com/orders/3",
"action":"GET",
"types":["text/xml","application/json"]
},
{
"rel":"self",
"href":"https://adventure-works.com/orders/3",
"action":"PUT",
"types":["application/x-www-form-urlencoded"]
},
{
"rel":"self",
"href":"https://adventure-works.com/orders/3",
"action":"DELETE",
"types":[]
}]
}
在這個例子裏,links這個數組節點包含了一個鏈接數據集合,每個鏈接數據都代表了一個相關實體的可用操作。每個鏈接數據又包含了關係("customer")、URI(https://adventure-works.com/c...)、HTTP方法,和可用的MIME類型。這些信息足夠一個客户程序用來執行操作了。
鏈接數組裏還包含了一些自引用信息,跟原本獲取到的請求資源有關,它們的關係被標記為 'self'。
根據不同的資源狀態,links數組節點中的內容可能會有所不同。這也正是超文本作為 "engine of application state." 的含義。
RESTful web API的版本控制
通常web API不會一成不變,隨着業務需求變化,會有更多資源集合出現,資源之間的關係會變化,資源的數據結構也會有所改變。更新web API來處理變化的需求或許沒什麼難度,但是必須得考慮這些改動對於那些正在使用這些API的客户端程序的影響。設計和維護這些web API的程序員,對這些接口有完全的控制權,但是對那些客户端程序可並不一定是這樣,有可能開發它們的是一些遠處的第三方。所以關鍵的是能讓現存的客户端程序繼續正常運行,同時新的客户端程序可以充分使用這些新功能和資源。
版本控制可以使web API表明它公開的功能和資源,同時能讓客户端程序向功能和資源的某個特定版本發起請求。下面幾節描述了不同的版本控制方法,各有其優劣。
無版本控制
這是最簡單的方法,一些內部調用API可以接受這麼做。重大的改動一般體現為新的資源或新鏈接,對已經存在的資源添加內容一般也不會有什麼問題,因為請求端程序遇到期望之外的數據內容,一般會自動忽略掉。
比如,向URI https://adventure-works.com/customers/3 發起請求,會返回一個包含了id、name和address字段的單個客户明細數據,這些數據符合請求端程序的預期:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
簡單起見,本節示例響應報文並沒有包含 HATEOAS 鏈接.
如果向客户資源的結構中添加了 DateCreated 字段,響應內容會變為:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}
已經存在的客户程序會繼續正常運行,只要它們可以忽略掉不認識的字段,而新的客户程序可以設計為能夠處理這個新添加的字段。不過,如果資源的結構發生了更徹底的改動(比如刪除或者重命名了某些字段),或者資源間的關係發生了變化,那就有可能使得這些正在運行的客户程序無法再正確運轉。這種情況下,你就得考慮下面的這些方法了。
URI 版本控制
每當對web API或者資源結構進行改動時,可以對每個資源的URI添加一個版本號,先前的URI應繼續像之前那樣運作,返回原本的資源數據。
對前面的例子作下擴展,如果將 address 字段重構為包含不同組成部分的子字段(比如街道、城市、省份和郵政編碼),這個版本的資源可以發佈為一個包含了版本號的URI,例如 https://adventure-works.com/v2/customers/3:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}
這種版本控制機制非常簡單,但是它依賴於服務器能將請求路由到正確的服務端點。而且,隨着web API的逐步迭代和服務器提供支持的版本數量增多,它會變得很笨重。另外,從純粹主義角度去看,返回了同樣數據(客户3)的URI,它們的版本不應該是不一樣的。這個方案還會讓 HATEOAS 的實現更加複雜,因為所有鏈接都不得不將版本號包含在它們的URI中。
查詢字符串版本控制
相比於提供多個不通的URI,更好的做法可能是在HTTP請求上附帶一個指定了資源版本的查詢字符串參數,比如 https://adventure-works.com/customers/3?version=2 。給這個版本參數一個有意義的默認值,比如1,這樣舊的客户程序可以忽略掉它而不受到影響。
這種做法的好處是,在語義上,同樣的資源請求用了相同的URI,不過這需要相應的代碼正確的解析請求中的查詢字符串,並給與正確的HTTP響應。另外此方法同樣會遇到跟URI版本控制一樣的問題,即實現 HATEOAS 會比較麻煩。
一些舊版瀏覽器和網絡代理不會緩存包含查詢字符串的請求響應內容,在其上運行的網絡程序調用web API時的性能可能會受到影響。
消息頭版本控制
除了將版本號放到查詢字符串參數中,還可以實現一個自定義消息頭來指明需要的資源版本。這個做法需要客户端程序在所有請求上攜帶正確的消息頭,儘管服務代碼可以在請求忽略了這個消息頭時給一個默認值(比如版本1)。下面這個例子使用了名為 Custom-Header 的消息頭,裏面的值指明瞭web API的版本。
Version 1:
GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
Version 2:
GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}
和前面兩種版本控制方案一樣,實現 HATEOAS 也需要在所有鏈接中帶上正確的消息頭。
媒體類型版本控制
在前面有介紹過,當客户端程序發出一個HTTP GET請求到web服務端時,它應當使用 Accept 消息頭明確它可以處理的內容格式。Accept 消息頭經常用來指明客户端程序希望得到的響應報文格式應該是XML、JSON或者其他一些通用格式。不過,也可以擴展一些自定義媒體類型,使得客户程序可以在其中加一些信息來指明需要的資源版本。
下面這個例子在 Accept 消息頭中指定了 application/vnd.adventure-works.v1+json,其中的 vnd.adventure-works.v1 就向web服務端表明了它需要的是1這個資源版本,並希望得到json格式的響應內容:
GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json
處理請求的代碼需要處理這個消息頭,並儘可能的滿足它(客户端程序可能會在 Accept 中指定多個格式,服務端只需要選擇其中最合適的一個作為響應格式即可)。Web服務端在響應體中使用 Content-Type 報文頭來對數據格式進行確認:
HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8
{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}
如果 Accept 消息頭中指定了無法處理的媒體類型,服務端可以返回HTTP狀態碼406(Not Acceptable),或者使用一個默認類型來處理。
這種做法被認為是最純粹的版本控制方案了(譯者猜應該是説這種做法可以在連接上只是明確指向資源即可,不需要做其他調整,URI版本方案,和查詢字符串版本方案,都會影響這個URI的乾淨度),並且它原生支持 HATEOAS ,因為可以在鏈接節點中指明 MIME 類型。
在選擇一種版本控制方案時,需要同時考慮到對性能的潛在影響,特別是在web服務端的緩存處理。URI和查詢字符串版本控制方案是可以緩存的,因為攜帶了版本信息的完整URI和查詢字符串會作為緩存的唯一標識,同樣的版本請求每次都指向同樣的數據。
The Header versioning and Media Type versioning mechanisms typically require additional logic to examine the values in the custom header or the Accept header. In a large-scale environment, many clients using different versions of a web API can result in a significant amount of duplicated data in a server-side cache. This issue can become acute if a client application communicates with a web server through a proxy that implements caching, and that only forwards a request to the web server if it does not currently hold a copy of the requested data in its cache.(這段譯者未能完全理解,猜其意思,可能跟代理的緩存策略有關,因為Header和MediaType的版本控制方案,在請求不同版本時的URI和query string並沒有變化,代理可能會使用不正確的緩存數據直接返回,而不是向web服務端發起新的請求來獲取數據)
Open API 計劃
Open API 計劃是由一個行業聯盟創建的,目的是標準化供應商之間的REST API描述。作為這一計劃的一部分,Swagger 2.0規範被重新命名為OpenAPI規範(OAS),並納入了Open API 計劃。
如果你想要把自己的web API改造為OpenAPI,可以參考下面幾點:
- OpenAPI規範附帶了一套關於REST API應該如何設計的固執的指導方針。這對互操作性有好處,但在設計API以符合規範時需要更加小心。
- OpenAPI提倡契約優先的方法,而不是實現優先的方法。契約優先意味着首先設計API契約(接口),然後編寫實現該契約的代碼。
- Swagger這樣的工具可以從API契約直接生成客户端代碼或文檔。可參考 ASP.NET Web API help pages using Swagger。
More information
- Microsoft REST API guidelines. Detailed recommendations for designing public REST APIs.
- Web API checklist. A useful list of items to consider when designing and implementing a web API.
- Open API Initiative. Documentation and implementation details on Open API.