RESTful服務最佳實踐(轉)

原文:http://www.javashuo.com/article/p-vvvfqzex-p.html 譯文做者:Jaxuhtml

英文原文:https://files-cdn.cnblogs.com/files/jaxu/RESTful_Best_Practices_v1_2.pdf前端

本文主要讀者

  該最佳實踐文檔適用於對RESTful Web服務感興趣的開發人員,該服務爲跨多個服務的組件提供了較高的可靠性和一致性。按照本文的指導,可快速、普遍、公開地爲內外部客戶採用。git

  本文中的指導原則一樣適用於工程師們,他們但願使用這些依據最佳實踐原則開發的服務。雖然他們更加關注緩存、代理規則、監聽及安全等相關方面,可是該文檔能做爲一份涵蓋全部種類服務的總指南。github

  另外,經過從這些指導原則,管理人員瞭解到建立公共的、提供高穩定性的服務所需花費的努力,他們也可從中受益。web

 

引言

  現今已有大量關於RESTful Web服務最佳實踐的相關資料(詳見本文最後的相關文獻部分)。因爲創做的時間不一樣,許多資料中的內容是矛盾的。此外,想要經過查閱文獻來了解這種服務的發展是不太可取的。爲了瞭解RESTful這一律念,至少須要查閱三到五本相關文獻,而本文將可以幫你加速這一過程——摒棄多餘的討論,最大化地提煉出REST的最佳實踐和規範。ajax

  與其說REST是一套標準,REST更像是一種原則的集合。除了六個重要的原則外就沒有其餘的標準了。實際上,雖然有所謂的「最佳實踐」和標準,但這些東西都和宗教鬥爭同樣,在不斷地演化。正則表達式

  本文圍繞REST的廣泛問題提出了意見和仿食譜式的討論,並經過介紹一些簡單的背景知識對建立真實情境下的預生產環境中一致的REST服務提供知識。本文收集了來自其餘渠道的信息,經歷過一次次的失敗後不斷改進。數據庫

  但對於REST模式是否必定比SOAP好用仍有較大爭議(反之亦然),也許在某些狀況下仍須要建立SOAP服務。本文在說起SOAP時並未花較大篇幅來討論它的相對優勢。相反因爲技術和行業在不斷進步,咱們將繼續堅持咱們的假設–REST是當下設計web服務的最佳方法。編程

  第一部分概述REST的含義、設計準則和它的獨特之處。第二部分列舉了一些小貼士來記憶REST的服務理念。以後的部分則會更深刻地爲web服務建立人員提供一些細節的支持和討論,來實現一個可以公開展現在生產環境中的高質量REST服務。json

 

REST是什麼?

  REST架構方式描述了六種約束。這些應用於架構的約束,最先是由Roy Fielding在他的博士論文中提出並定義了RESTful風格的基準。(詳見http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

  這六個約束分別是:

  • 統一接口
  • 無狀態
  • 可緩衝
  • C-S架構
  • 分層系統
  • 按需編碼

  如下是這些約束的詳細討論:

統一接口

  統一接口約束定義了客戶端和服務端之間的接口,簡化和分離了框架結構,這樣一來每一個部分均可獨立演化。如下是接口統一的四個原則:

  基於資源

  不一樣資源須要用URI來惟一標識。返回給客戶端的表徵和資源自己在概念上有所不一樣,例如服務端不會直接傳送一個數據庫資源,然而,一些HTML、XML或JSON數據可以展現部分數據庫記錄,如用芬蘭語來表述仍是用UTF-8編碼則要根據請求和服務器實現的細節來決定。

  經過表徵來操做資源

  當客戶端收到包含元數據的資源的表徵時,在有權限的狀況下,客戶端已掌握的足夠的信息,能夠對服務端的資源進行刪改。

  自描述的信息

  每條信息都包含足夠的數據用以確認信息該如何處理。例如要由網絡媒體類型(已知的如MIME類型)來確認需調用哪一個解析器。響應一樣也代表了它們的緩存能力。

  超媒體即應用狀態引擎(HATEOAS)

  客戶端經過body內容、查詢串參數、請求頭和URI(資源名稱)來傳送狀態。服務端經過body內容,響應碼和響應頭傳送狀態給客戶端。這項技術被稱爲超媒體(或超文本連接)。

  除了上述內容外,HATEOS也意味着,必要的時候連接也可被包含在返回的body(或頭部)中,以提供URI來檢索對象自己或關聯對象。下文將對此進行更詳細的闡述。

  統一接口是每一個REST服務設計時的必要準則。

無狀態

  正如REST是REpresentational State Transfer的縮寫,無狀態很關鍵。本質上,這代表了處理請求所需的狀態已經包含在請求自己裏,也有多是URI的一部分、查詢串參數、body或頭部。URI可以惟一標識每一個資源,body中也包含了資源的轉態(或轉態變動狀況)。以後,服務器將進行處理,將相關的狀態或資源經過頭部、狀態和響應body傳遞給客戶端。

  從事咱們這一行業的大多數人都習慣使用容器來編程,容器中有一個「會話」的概念,用於在多個HTTP請求下保持狀態。在REST中,若是要在多個請求下保持用戶狀態,客戶端必須囊括客戶端的全部信息來完成請求,必要時從新發送請求。自從服務端不須要維持、更新或傳遞會話狀態後,無狀態性獲得了更大的延展。此外,負載均衡器無需擔憂和無狀態系統之間的會話。

  因此狀態和資源間有什麼差異?服務器對於狀態,或者說是應用狀態,所關注的點是在當前會話或請求中要完成請求所需的數據。而資源,或者說是資源狀態,則是定義了資源表徵的數據,例如存儲在數據庫中的數據。因而可知,應用狀態是是隨着客戶端和請求的改變而改變的數據。相反,資源狀態對於發出請求的客戶端來講是不變的。

  在網絡應用的某一特定位置上擺放一個返回按鈕,是由於它但願你能按必定的順序來操做嗎?實際上是由於它違反了無狀態的原則。有許多不遵照無狀態原則的案例,例如3-Legged OAuth,API調用速度限制等。但仍是要儘可能確保服務器中不須要在多個請求下保持應用狀態。

可緩存

  在萬維網上,客戶端能夠緩存頁面的響應內容。所以響應都應隱式或顯式的定義爲可緩存的,若不可緩存則要避免客戶端在屢次請求後用舊數據或髒數據來響應。管理得當的緩存會部分地或徹底地除去客戶端和服務端之間的交互,進一步改善性能和延展性。

C-S架構

  統一接口使得客戶端和服務端相互分離。關注分離意味什麼?打個比方,客戶端不須要存儲數據,數據都留在服務端內部,這樣使得客戶端代碼的可移植性獲得了提高;而服務端不須要考慮用戶接口和用戶狀態,這樣一來服務端將更加簡單易拓展。只要接口不改變,服務端和客戶端能夠單獨地進行研發和替換。

分層系統

  客戶端一般沒法代表本身是直接仍是間接與端服務器進行鏈接。中介服務器能夠經過啓用負載均衡或提供共享緩存來提高系統的延展性。分層時一樣要考慮安全策略。

按需編碼(可選)

  服務端經過傳輸可執行邏輯給客戶端,從而爲其臨時拓展和定製功能。相關的例子有編譯組件Java applets和客戶端腳本JavaScript。

  聽從上述原則,與REST架構風格保持一致,能讓各類分佈式超媒體系統擁有指望的天然屬性,好比高性能,延展性,簡潔,可變性,可視化,可移植性和可靠性。

  提示:REST架構中的設計準則中,只有按需編碼爲可選項。若是某個服務違反了其餘任意一項準則,嚴格意思上不能稱之爲RESTful風格。

 

REST快速提示

  (根據上面提到的六個原則)無論在技術上是否是RESTful的,這裏有一些相似REST概念的建議。遵循它們,能夠實現更好、更有用的服務:

使用HTTP動詞表示一些含義

  任何API的使用者可以發送GET、POST、PUT和DELETE請求,它們很大程度明確了所給請求的目的。同時,GET請求不能改變任何潛在的資源數據。測量和跟蹤仍可能發生,但只會更新數據而不會更新由URI標識的資源數據。

合理的資源名

  合理的資源名稱或者路徑(如/posts/23而不是/api?type=posts&id=23)能夠更明確一個請求的目的。使用URL查詢串來過濾數據是很好的方式,但不該該用於定位資源名稱。

  適當的資源名稱爲服務端請求提供上下文,增長服務端API的可理解性。經過URI名稱分層地查看資源,能夠給使用者提供一個友好的、容易理解的資源層次,以在他們的應用程序上應用。資源名稱應該是名詞,避免爲動詞。使用HTTP方法來指定請求的動做部分,能讓事情更加的清晰。

XML和JSON

  建議默認支持json,而且,除非花費很驚人,不然就同時支持json和xml。在理想狀況下,讓使用者僅經過改變擴展名.xml和.json來切換類型。此外,對於支持ajax風格的用戶界面,一個被封裝的響應是很是有幫助的。提供一個被封裝的響應,在默認的或者有單獨擴展名的狀況下,例如:.wjson和.wxml,代表客戶端請求一個被封裝的json或xml響應(請參見下面的封裝響應)。

  「標準」中對json的要求不多。而且這些需求只是語法性質的,無關內容格式和佈局。換句話說,REST服務端調用的json響應是協議的一部分——在標準中沒有相關描述。更多關於json數據格式能夠在http://www.json.org/上找到。

  關於REST服務中xml的使用,xml的標準和約定除了使用語法正確的標籤和文本外沒有其它的做用。特別地,命名空間不是也不該該是被使用在REST服務端的上下文中。xml的返回更相似於json——簡單、容易閱讀,沒有模式和命名空間的細節呈現——僅僅是數據和連接。若是它比這更復雜的話,參看本節的第一段——使用xml的成本是驚人的。鑑於咱們的經驗,不多有人使用xml做爲響應。在它被徹底淘汰以前,這是最後一個可被確定的地方。

建立適當粒度的資源

  一開始,系統中模擬底層應用程序域或數據庫架構的API更容易被建立。最終,你會但願將這些服務都整合到一塊兒——利用多項底層資源減小通訊量。在建立獨立的資源以後再建立更大粒度的資源,比從更大的合集中建立較大粒度的資源更加容易一些。從一些小的容易定義的資源開始,建立CRUD(增刪查改)功能,能夠使資源的建立變得更容易。隨後,你能夠建立這些基於用例和減小通訊量的資源。

考慮連通性

  REST的原理之一就是連通性——經過超媒體連接實現。當在響應中返回連接時,api變的更具備自描述性,而在沒有它們時服務端依然可用。至少,接口自己能夠爲客戶端提供如何檢索數據的參考。此外,在經過POST方法建立資源時,還能夠利用頭位置包含一個連接。對於響應中支持分頁的集合,"first"、 "last"、"next"、和"prev"連接至少是很是有用的。

 

定義

冪等性

  不要從字面意思來理解什麼是冪等性,偏偏相反,這與某些功能紊亂的領域無關。下面是來自維基百科的解釋:

在計算機科學中,術語冪等用於更全面地描述一個操做,一次或屢次執行該操做產生的結果是一致的。根據應用的上下文,這可能有不一樣的含義。例如,在方法或者子例程調用具備反作用的狀況下,意味着在第一調用以後被修改的狀態也保持不變。

  從REST服務端的角度來看,因爲操做(或服務端調用)是冪等的,客戶端能夠用重複的調用而產生相同的結果——在編程語言中操做像是一個"setter"(設置)方法。換句話說,就是使用多個相同的請求與使用單個請求效果相同。注意,當冪等操做在服務器上產生相同的結果(反作用),響應自己多是不一樣的(例如在多個請求之間,資源的狀態可能會改變)。

  PUT和DELETE方法被定義爲是冪等的。查看http請求中delete動詞的警告信息,能夠參照下文的DELETE部分。GET、HEAD、OPTIO和TRACE方法自從被定義爲安全的方法後,也被定義爲冪等的。參照下面關於安全的段落。

安全

  來自維基百科:

一些方法(例如GET、HEAD、OPTIONS和TRACE)被定義爲安全的方法,這意味着它們僅被用於信息檢索,而不能更改服務器的狀態。換句話說,它們不會有反作用,除了相對來講無害的影響如日誌、緩存、橫幅廣告或計數服務等。任意的GET請求,不考慮應用狀態的上下文,都被認爲是安全的。

  總之,安全意味着調用的方法不會引發反作用。所以,客戶端能夠反覆使用安全的請求而不用擔憂對服務端產生任何反作用。這意味着服務端必須遵照GET、HEAD、OPTIONS和TRACE操做的安全定義。不然,除了對消費端產生混淆外,它還會致使Web緩存,搜索引擎以及其它自動代理的問題——這將在服務器上產生意想不到的後果。

  根據定義,安全操做是冪等的,由於它們在服務器上產生相同的結果。

  安全的方法被實現爲只讀操做。然而,安全並不意味着服務器必須每次都返回相同的響應。

 

HTTP動詞

  Http動詞主要遵循「統一接口」規則,並提供給咱們對應的基於名詞的資源的動做。最主要或者最經常使用的http動詞(或者稱之爲方法,這樣稱呼可能更恰當些)有POST、GET、PUT和DELETE。這些分別對應於建立、讀取、更新和刪除(CRUD)操做。也有許多其它的動詞,可是使用頻率比較低。在這些使用較少的方法中,OPTIONS和HEAD每每使用得更多。

GET

  HTTP的GET方法用於檢索(或讀取)資源的數據。在正確的請求路徑下,GET方法會返回一個xml或者json格式的數據,以及一個200的HTTP響應代碼(表示正確返回結果)。在錯誤狀況下,它一般返回404(不存在)或400(錯誤的請求)。

  例如:

  GET http://www.example.com/customers/12345
  GET http://www.example.com/customers/12345/orders
  GET http://www.example.com/buckets/sample

  按照HTTP的設計規範,GET(以及附帶的HEAD)請求僅用於讀取數據而不改變數據。所以,這種使用方式被認爲是安全的。也就是說,它們的調用沒有數據修改或污染的風險——調用1次和調用10次或者沒有被調用的效果同樣。此外,GET(以及HEAD)是冪等的,這意味着使用多個相同的請求與使用單個的請求最終都擁有相同的結果。

  不要經過GET暴露不安全的操做——它應該永遠都不能修改服務器上的任何資源。

PUT

  PUT一般被用於更新資源。經過PUT請求一個已知的資源URI時,須要在請求的body中包含對原始資源的更新數據。

  不過,在資源ID是由客服端而非服務端提供的狀況下,PUT一樣能夠被用來建立資源。換句話說,若是PUT請求的URI中包含的資源ID值在服務器上不存在,則用於建立資源。同時請求的body中必須包含要建立的資源的數據。有人以爲這會產生歧義,因此除非真的須要,使用這種方法來建立資源應該被慎用。

  或者咱們也能夠在body中提供由客戶端定義的資源ID而後使用POST來建立新的資源——假設請求的URI中不包含要建立的資源ID(參見下面POST的部分)。

  例如:

  PUT http://www.example.com/customers/12345
  PUT http://www.example.com/customers/12345/orders/98765
  PUT http://www.example.com/buckets/secret_stuff

  當使用PUT操做更新成功時,會返回200(或者返回204,表示返回的body中不包含任何內容)。若是使用PUT請求建立資源,成功返回的HTTP狀態碼是201。響應的body是可選的——若是提供的話將會消耗更多的帶寬。在建立資源時沒有必要經過頭部的位置返回連接,由於客戶端已經設置了資源ID。請參見下面的返回值部分。

  PUT不是一個安全的操做,由於它會修改(或建立)服務器上的狀態,但它是冪等的。換句話說,若是你使用PUT建立或者更新資源,而後重複調用,資源仍然存在而且狀態不會發生變化。

  例如,若是在資源增量計數器中調用PUT,那麼這個調用方法就再也不是冪等的。這種狀況有時候會發生,且可能足以證實它是非冪等性的。不過,建議保持PUT請求的冪等性。並強烈建議非冪等性的請求使用POST。

POST

  POST請求常常被用於建立新的資源,特別是被用來建立從屬資源。從屬資源即歸屬於其它資源(如父資源)的資源。換句話說,當建立一個新資源時,POST請求發送給父資源,服務端負責將新資源與父資源進行關聯,並分配一個ID(新資源的URI),等等。

  例如:

  POST http://www.example.com/customers
  POST http://www.example.com/customers/12345/orders

  當建立成功時,返回HTTP狀態碼201,並附帶一個位置頭信息,其中帶有指向最早建立的資源的連接。

  POST請求既不是安全的又不是冪等的,所以它被定義爲非冪等性資源請求。使用兩個相同的POST請求極可能會致使建立兩個包含相同信息的資源。

PUT和POST的建立比較

  總之,咱們建議使用POST來建立資源。當由客戶端來決定新資源具備哪些URI(經過資源名稱或ID)時,使用PUT:即若是客戶端知道URI(或資源ID)是什麼,則對該URI使用PUT請求。不然,當由服務器或服務端來決定建立的資源的URI時則使用POST請求。換句話說,當客戶端在建立以前不知道(或沒法知道)結果的URI時,使用POST請求來建立新的資源。

DELETE

  DELETE很容易理解。它被用來根據URI標識刪除資源。

  例如:

  DELETE http://www.example.com/customers/12345
  DELETE http://www.example.com/customers/12345/orders
  DELETE http://www.example.com/buckets/sample

  當刪除成功時,返回HTTP狀態碼200(表示正確),同時會附帶一個響應體body,body中可能包含了刪除項的數據(這會佔用一些網絡帶寬),或者封裝的響應(參見下面的返回值)。也能夠返回HTTP狀態碼204(表示無內容)表示沒有響應體。總之,能夠返回狀態碼204表示沒有響應體,或者返回狀態碼200同時附帶JSON風格的響應體。

  根據HTTP規範,DELETE操做是冪等的。若是你對一個資源進行DELETE操做,資源就被移除了。在資源上反覆調用DELETE最終致使的結果都相同:即資源被移除了。但若是將DELETE的操做用於計數器(資源內部),則DETELE將再也不是冪等的。如前面所述,只要數據沒有被更新,統計和測量的用法依然可被認爲是冪等的。建議非冪等性的資源請求使用POST操做。

  然而,這裏有一個關於DELETE冪等性的警告。在一個資源上第二次調用DELETE每每會返回404(未找到),由於該資源已經被移除了,因此找不到了。這使得DELETE操做再也不是冪等的。若是資源是從數據庫中刪除而不是被簡單地標記爲刪除,這種狀況須要適當妥協。

  下表總結出了主要HTTP的方法和資源URI,以及推薦的返回值:

HTTP請求 /customers /customers/{id}
GET 200(正確),用戶列表。使用分頁、排序和過濾大導航列表。 200(正確),查找單個用戶。若是ID沒有找到或ID無效則返回404(未找到)。
PUT 404(未找到),除非你想在整個集合中更新/替換每一個資源。 200(正確)或204(無內容)。若是沒有找到ID或ID無效則返回404(未找到)。
POST 201(建立),帶有連接到/customers/{id}的位置頭信息,包含新的ID。 404(未找到)
DELETE 404(未找到),除非你想刪除整個集合——一般不被容許。 200(正確)。若是沒有找到ID或ID無效則返回404(未找到)。

 

資源命名

  除了適當地使用HTTP動詞,在建立一個能夠理解的、易於使用的Web服務API時,資源命名能夠說是最具備爭議和最重要的概念。一個好的資源命名,它所對應的API看起來更直觀而且易於使用。相反,若是命名很差,一樣的API會讓人感受很笨拙而且難以理解和使用。當你須要爲你的新API建立資源URL時,這裏有一些小技巧值得借鑑。

  從本質上講,一個RESTFul API最終均可以被簡單地看做是一堆URI的集合,HTTP調用這些URI以及一些用JSON和(或)XML表示的資源,它們中有許多包含了相互關聯的連接。RESTful的可尋址能力主要依靠URI。每一個資源都有本身的地址或URI——服務器能提供的每個有用的信息均可以做爲資源來公開。統一接口的原則部分地經過URI和HTTP動詞的組合來解決,並符合使用標準和約定。

  在決定你係統中要使用的資源時,使用名詞來命名這些資源,而不是用動詞或動做來命名。換句話說,一個RESTful URI應該關聯到一個具體的資源,而不是關聯到一個動做。另外,名詞還具備一些動詞沒有的屬性,這也是另外一個顯著的因素。

  一些資源的例子:

  • 系統的用戶
  • 學生登記的課程
  • 一個用戶帖子的時間軸
  • 關注其餘用戶的用戶
  • 一篇關於騎馬的文章

  服務套件中的每一個資源至少有一個URI來標識。若是這個URI能表示必定的含義而且可以充分描述它所表明的資源,那麼它就是一個最好的命名。URI應該具有可預測性和分層結構,這將有助於提升它們的可理解性和可用性的:可預測指的是資源應該和名稱保持一致;而分層指的是數據具備關係上的結構。這並不是REST規則或規範,可是它強化了對API的定義。

  RESTful API是提供給消費端的。URI的名稱和結構應該將它所表達的含義傳達給消費者。一般咱們很難知道數據的邊界是什麼,可是從你的數據上你應該頗有可能去嘗試找到要返回給客戶端的數據是什麼。API是爲客戶端而設計的,而不是爲你的數據。

  假設咱們如今要描述一個包括客戶、訂單,列表項,產品等功能的訂單系統。考慮一下咱們該如何來描述在這個服務中所涉及到的資源的URIs:

資源URI示例

  爲了在系統中插入(建立)一個新的用戶,咱們能夠使用:

  POST http://www.example.com/customers

 

  讀取編號爲33245的用戶信息:

  GET http://www.example.com/customers/33245

  使用PUT和DELETE來請求相同的URI,能夠更新和刪除數據。

 

  下面是對產品相關的URI的一些建議:

  POST http://www.example.com/products

  用於建立新的產品。

 

  GET|PUT|DELETE http://www.example.com/products/66432

  分別用於讀取、更新、刪除編號爲66432的產品。

 

  那麼,如何爲用戶建立一個新的訂單呢?

  一種方案是:

  POST http://www.example.com/orders

  這種方式能夠用來建立訂單,但缺乏相應的用戶數據。

  

  由於咱們想爲用戶建立一個訂單(注意之間的關係),這個URI可能不夠直觀,下面這個URI則更清晰一些:

  POST http://www.example.com/customers/33245/orders

  如今咱們知道它是爲編號33245的用戶建立一個訂單。

 

  那下面這個請求返回的是什麼呢?

  GET http://www.example.com/customers/33245/orders

  多是一個編號爲33245的用戶所建立或擁有的訂單列表。注意:咱們能夠屏蔽對該URI進行DELETE或PUT請求,由於它的操做對象是一個集合。

 

  繼續深刻,那下面這個URI的請求又表明什麼呢?

  POST http://www.example.com/customers/33245/orders/8769/lineitems

  多是(爲編號33245的用戶)增長一個編號爲8769的訂單條目。沒錯!若是使用GET方式請求這個URI,則會返回這個訂單的全部條目。可是,若是這些條目與用戶信息無關,咱們將會提供POST www.example.com/orders/8769/lineitems這個URI。

  從返回的這些條目來看,指定的資源可能會有多個URIs,因此咱們可能也須要要提供這樣一個URI GET http://www.example.com/orders/8769,用來在不知道用戶ID的狀況下根據訂單ID來查詢訂單。

 

  更進一步:

  GET http://www.example.com/customers/33245/orders/8769/lineitems/1

  可能只返回同個訂單中的第一個條目。

  如今你應該理解什麼是分層結構了。它們並非嚴格的規則,只是爲了確保在你的服務中這些強制的結構可以更容易被用戶所理解。與全部軟件開發中的技能同樣,命名是成功的關鍵。

  

  多看一些API的示例並學會掌握這些技巧,和你的隊友一塊兒來完善你API資源的URIs。這裏有一些APIs的例子:

資源命名的反例

  前面咱們已經討論過一些恰當的資源命名的例子,然而有時一些反面的例子也頗有教育意義。下面是一些不太具備RESTful風格的資源URIs,看起來比較混亂。這些都是錯誤的例子! 

  首先,一些serivices每每使用單一的URI來指定服務接口,而後經過查詢參數來指定HTTP請求的動做。例如,要更新編號12345的用戶信息,帶有JSON body的請求多是這樣:

  GET http://api.example.com/services?op=update_customer&id=12345&format=json

  儘管上面URL中的"services"的這個節點是一個名詞,但這個URL不是自解釋的,由於對於全部的請求而言,該URI的層級結構都是同樣的。此外,它使用GET做爲HTTP動詞來執行一個更新操做,這簡直就是反人類(甚至是危險的)。

  下面是另一個更新用戶的操做的例子:

  GET http://api.example.com/update_customer/12345

  以及它的一個變種:

  GET http://api.example.com/customers/12345/update

  你會常常看到在其餘開發者的服務套件中有不少這樣的用法。能夠看出,這些開發者試圖去建立RESTful的資源名稱,並且已經有了一些進步。可是你仍然可以識別出URL中的動詞短語。注意,在這個URL中咱們不須要"update"這個詞,由於咱們能夠依靠HTTP動詞來完成操做。下面這個URL正好說明了這一點:

  PUT http://api.example.com/customers/12345/update

  這個請求同時存在PUT和"update",這會對消費者產生迷惑!這裏的"update"指的是一個資源嗎?所以,這裏咱們費些口舌也是但願你可以明白……

複數

  讓咱們來討論一下複數和「單數」的爭議…還沒據說過?但這種爭議確實存在,事實上它能夠歸結爲這個問題……

  在你的層級結構中URI節點是否須要被命名爲單數或複數形式呢?舉個例子,你用來檢索用戶資源的URI的命名是否須要像下面這樣:

  GET http://www.example.com/customer/33245

  或者:

  GET http://www.example.com/customers/33245

  兩種方式都沒問題,但一般咱們都會選擇使用複數命名,以使得你的API URI在全部的HTTP方法中保持一致。緣由是基於這樣一種考慮:customers是服務套件中的一個集合,而ID33245的這個用戶則是這個集合中的其中一個。

  按照這個規則,一個使用複數形式的多節點的URI會是這樣(注意粗體部分):

  GET http://www.example.com/customers/33245/orders/8769/lineitems/1

  「customers」、「orders」以及「lineitems」這些URI節點都使用的是複數形式。

  這意味着你的每一個根資源只須要兩個基本的URL就能夠了,一個用於建立集合內的資源,另外一個用來根據標識符獲取、更新和刪除資源。例如,以customers爲例,建立資源能夠使用下面的URL進行操做:

  POST http://www.example.com/customers

  而讀取、更新和刪除資源,使用下面的URL操做:

  GET|PUT|DELETE http://www.example.com/customers/{id}

  正如前面提到的,給定的資源可能有多個URI,但做爲一個最小的完整的增刪改查功能,利用兩個簡單的URI來處理就夠了。

  或許你會問:是否在有些狀況下複數沒有意義?嗯,事實上是這樣的。當沒有集合概念的時候(此時複數沒有意義)。換句話說,當資源只有一個的狀況下,使用單數資源名稱也是能夠的——即一個單一的資源。例如,若是有一個單一的整體配置資源,你能夠使用一個單數名稱來表示:

  GET|PUT|DELETE http://www.example.com/configuration

  注意這裏缺乏configuration的ID以及HTTP動詞POST的用法。假設每一個用戶有一個配置的話,那麼這個URL會是這樣:

  GET|PUT|DELETE http://www.example.com/customers/12345/configuration

  一樣注意這裏沒有指定configuration的ID,以及沒有給定POST動詞的用法。在這兩個例子中,可能也會有人認爲使用POST是有效的。好吧...

 

返回表徵

  正如前面提到的,RESTful接口支持多種資源表徵,包括JSON和XML,以及被封裝的JSON和XML。建議JSON做爲默認表徵,不過服務端應該容許客戶端指定其它表徵。

  對於客戶端請求的表徵格式,咱們能夠在Accept頭經過文件擴展名來進行指定,也能夠經過query-string等其它方式來指定。理想狀況下,服務端能夠支持全部這些方法。可是,如今業內更傾向於經過相似於文件擴展名的方式來進行指定。所以,建議服務端至少須要支持使用文件擴展名的方式,例如「.json」,「.xml」以及它們的封裝版本「.wjon」,「.wxml」。

  經過這種方式,在URI中指定返回表徵的格式,能夠提升URL的可見性。例如,GET http://www.example.com/customers.xml將返回customer列表的XML格式的表徵。一樣,GET http://www.example.com/customers.json將返回一個JSON格式的表徵。這樣,即便是在最基礎的客戶端(例如「curl」),服務使用起來也會更加簡便。推薦使用這種方式。

  此外,當url中沒有包含格式說明時,服務端應該返回默認格式的表徵(假設爲JSON)。例如:

  GET http://www.example.com/customers/12345

  GET http://www.example.com/customers/12345.json

  以上二者返回的ID爲12345的customer數據均爲JSON格式,這是服務端的默認格式。

  GET http://www.example.com/customers/12345.xml

  若是服務端支持的話,以上請求返回的ID爲12345的customer數據爲XML格式。若是該服務器不支持XML格式的資源,將返回一個HTTP 404的錯誤。

  使用HTTP Accept頭被普遍認爲是一種更優雅的方式,而且符合HTTP的規範和含義,客戶端能夠經過這種方式來告知HTTP服務端它們可支持的數據類型有哪些。可是,爲了使用Accept頭,服務端要同時支持封裝和未封裝的響應,你必須實現自定義的類型——由於這些格式不是標準的類型。這大大增長了客戶端和服務端的複雜性。請參見RFC 2616的14.1節有關Accept頭的詳細信息(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1)。使用文件擴展名來指定數據格式是最簡單直接的方法,用最少的字符就能夠完成,而且支持腳本操做——無需利用HTTP頭。

  一般當咱們提到REST服務,跟XML是絕不相關的。即便服務端支持XML,也幾乎沒有人建議在REST中使用XML。XML的標準和公約在REST中不太適用。特別是它連命名空間都沒有,就更不應在RESTful服務體系中使用了。這隻會使事情變得更復雜。因此返回的XML看起來更像JSON,它簡單易讀,沒有模式和命名空間的限制,換句話來講是無標準的,易於解析。

資源經過連接的可發現性(HATEOAS續)

  REST指導原則之一(根據統一接口原則)是application的狀態經過hypertext(超文本)來傳輸。這就是咱們一般所說的Hypertext As The Engine of Application State (即HATEOAS,用超文原本做爲應用程序狀態機),咱們在「REST是什麼」一節中也提到過。

  根據Roy Fielding在他的博客中的描述(http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertextdriven),REST接口中最重要的部分是超文本的使用。此外,他還指出,在給出任何相關的信息以前,一個API應該是可用和可理解的。也就是說,一個API應當能夠經過其連接導航到數據的各個部分。不建議只返回純數據。

  不過目前的業界先驅們並無常常採用這種作法,這反映了HATEOAS僅僅在成熟度模型中的使用率更高。縱觀衆多的服務體系,它們大多返回更多的數據,而返回的連接卻不多(或者沒有)。這是違背Fielding的REST約定的。Fielding說:「信息的每個可尋址單元都攜帶一個地址……查詢結果應該表現爲一個帶有摘要信息的連接清單,而不是對象數組。」

  另外一方面,簡單粗暴地將整個連接集合返回會大大影響網絡帶寬。在實際狀況中,根據所需的條件或使用狀況,API接口的通訊量要根據服務器響應中超文本連接所包含的「摘要」數量來平衡。

  同時,充分利用HATEOAS可能會增長實現的複雜性,並對服務客戶端產生明顯的負擔,這至關於下降了客戶端和服務器端開發人員的生產力。所以,當務之急是要平衡超連接服務實踐和現有可用資源之間的問題。

  超連接最小化的作法是在最大限度地減小客戶端和服務器之間的耦合的同時,提升服務端的可用性、可操縱性和可理解性。這些最小化建議是:經過POST建立資源並從GET請求返回集合,對於有分頁的狀況後面咱們會提到。

最小化連接推薦

  在create的用例中,新建資源的URI(連接)應該在Location響應頭中返回,且響應主體是空的——或者只包含新建資源的ID。

  對於從服務端返回的表徵集合,每一個表徵應該在它的連接集合中攜帶一個最小的「自身」連接屬性。爲了方便分頁操做,其它的連接能夠放在一個單獨的連接集合中返回,必要時能夠帶有「第一頁」、「上一頁」、「下一頁」、「最後一頁」等信息。

  參照下文連接格式部分的例子獲取更多信息。

連接格式

  參照整個連接格式的標準,建議遵照一些相似Atom、AtomPub或Xlink的風格。JSON-LD也不錯,但並無被普遍採用(若是曾經被用過)。目前業內最廣泛的方式是使用帶有"rel"元素和包含資源完整URI的"href"元素的Atom連接格式,不包含任何身份驗證或查詢字符串參數。"rel"元素能夠包含標準值"alternate"、"related"、"self"、"enclosure"和"via",還有分頁連接的「第一頁」、「上一頁」、「下一頁」,「最後一頁」。在須要時能夠自定義並添加使用它們。

  一些XML Atom格式的概念對於用JSON格式表示的連接來講是無用的。例如,METHOD屬性對於一個RESTful資源來講是不須要的,由於對於一個給定的資源,在全部支持的HTTP方法(CRUD行爲)中,資源的URI都是相同的——因此單獨列出這些是沒有必要的。

  讓咱們舉一些具體的例子來進一步說明這一點。下面是調用建立新資源的請求後的響應:

  POST http://api.example.com/users

  下面是響應頭集合中帶有建立新資源的URI的Location部分:

HTTP/1.1 201 CREATED 
Status: 201 
Connection: close 
Content-Type: application/json; charset=utf-8 
Location: http://api.example.com/users/12346

  返回的body能夠爲空,或者包含一個被封裝的響應(見下文封裝響應)。

  下面的例子經過GET請求獲取一個不包含分頁的表徵集合的JSON響應:

複製代碼
{
  "data": [
    {
      "user_id": "42",
      "name": "Bob",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/42"
        }
      ]
    },
    {
      "user_id": "22",
      "name": "Frank",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/22"
        }
      ]
    },
    {
      "user_id": "125",
      "name": "Sally",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/125"
        }
      ]
    }
  ]
}
複製代碼

  注意,links數組中的每一項都包含一個指向「自身(self)」的連接。該數組還可能還包含其它關係,如children、parent等。

  最後一個例子是經過GET請求獲取一個包含分頁的表徵集合的JSON響應(每頁顯示3項),咱們給出第三頁的數據:

複製代碼
{
  "data": [
    {
      "user_id": "42",
      "name": "Bob",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/42"
        }
      ]
    },
    {
      "user_id": "22",
      "name": "Frank",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/22"
        }
      ]
    },
    {
      "user_id": "125",
      "name": "Sally",
      "links": [
        {
          "rel": "self",
          "href": "http://api.example.com/users/125"
        }
      ]
    }
  ],
  "links": [
    {
      "rel": "first",
      "href": "http://api.example.com/users?offset=0&limit=3"
    },
    {
      "rel": "last",
      "href": "http://api.example.com/users?offset=55&limit=3"
    },
    {
      "rel": "previous",
      "href": "http://api.example.com/users?offset=3&limit=3"
    },
    {
      "rel": "next",
      "href": "http://api.example.com/users?offset=9&limit=3"
    }
  ]
}
複製代碼

  在這個例子中,響應中用於分頁的links集合中的每一項都包含一個指向「自身(self)」的連接。這裏可能還會有一些關聯到集合的其它連接,但都與分頁自己無關。簡而言之,這裏有兩個地方包含links。一個就是data對象中所包含的集合(這個也是接口要返回給客戶端的數據表徵集合),其中的每一項至少要包括一個指向「自身(self)」的links集合;另外一個則是一個單獨的對象links,其中包括和分頁相關的連接,該部分的內容適用於整個集合。

  對於經過POST請求建立資源的狀況,須要在響應頭中包含一個關聯新建對象連接的Location

封裝響應

   服務器能夠在響應中同時返回HTTP狀態碼和body。有許多JavaScript框架沒有把HTTP狀態響應碼返回給最終的開發者,這每每會致使客戶端沒法根據狀態碼來肯定具體的行爲。此外,雖然HTTP規範中有不少種響應碼,可是每每只有少數客戶端會關心這些——一般你們只在意"success"、"error"或"failture"。所以,將響應內容和響應狀態碼封裝在包含響應信息的表徵中,是有必要的。

  OmniTI 實驗室有這樣一個提議,它被稱爲JSEND響應。更多信息請參考http://labs.omniti.com/labs/jsend。另一個提案是由Douglas Crockford提出的,能夠查看這裏http://www.json.org/JSONRequest.html

  這些提案在實踐中並無徹底涵蓋全部的狀況。基本上,如今最好的作法是依照如下屬性封裝常規(非JSONP)響應:

  • code——包含一個整數類型的HTTP響應狀態碼。
  • status——包含文本:"success","fail"或"error"。HTTP狀態響應碼在500-599之間爲"fail",在400-499之間爲"error",其它均爲"success"(例如:響應狀態碼爲1XX、2XX和3XX)。
  • message——當狀態值爲"fail"和"error"時有效,用於顯示錯誤信息。參照國際化(il8n)標準,它能夠包含信息號或者編碼,能夠只包含其中一個,或者同時包含並用分隔符隔開。
  • data——包含響應的body。當狀態值爲"fail"或"error"時,data僅包含錯誤緣由或異常名稱。

  下面是一個返回success的封裝響應:

複製代碼
{
  "code": 200,
  "status": "success",
  "data": {
    "lacksTOS": false,
    "invalidCredentials": false,
    "authToken": "4ee683baa2a3332c3c86026d"
  }
}
複製代碼

  返回error的封裝響應:

複製代碼
{
  "code": 401,
  "status": "error",
  "message": "token is invalid",
  "data": "UnauthorizedException"
}
複製代碼

  這兩個封裝響應對應的XML以下:

複製代碼
<response>
    <code>200</code>
    <status>success</status>
    <data class="AuthenticationResult">
        <lacksTOS>false</lacksTOS>
        <invalidCredentials>false</invalidCredentials>
        <authToken>1.0|idm|idm|4ee683baa2a3332c3c86026d</authToken>
    </data>
</response>
複製代碼

  和:

複製代碼
<response>
    <code>401</code>
    <status>error</status>
    <message>token is invalid</message>
    <data class="string">UnauthorizedException</data>
</response>
複製代碼

處理跨域問題

   咱們都據說過有關瀏覽器的同源策略或同源性需求。它指的是瀏覽器只能請求當前正在顯示的站點的資源。例如,若是當前正在顯示的站點是www.Example1.com,則該站點不能對www.Example.com發起請求。顯然這會影響站點訪問服務器的方式。

  目前有兩個被普遍接受的支持跨域請求的方法:JSONP和跨域資源共享(CORS)。JSONP或「填充的JSON」是一種使用模式,它提供了一個方法請求來自不一樣域中的服務器的數據。其工做方式是從服務器返回任意的JavaScript代碼,而不是JSON。客戶端的響應由JavaScript解析器進行解析,而不是直接解析JSON數據。另外,CORS是一種web瀏覽器的技術規範,它爲web服務器定義了一種方式,從而容許服務器的資源能夠被不一樣域的網頁訪問。CORS被看作是JSONP的最新替代品,而且能夠被全部現代瀏覽器支持。所以,不建議使用JSONP。任何狀況下,推薦選擇CORS。

支持CORS

  在服務端實現CORS很簡單,只須要在發送響應時附帶HTTP頭,例如: 

Access-Control-Allow-Origin: *

  只有在數據是公共使用的狀況下才會將訪問來源設置爲"*"。大多數狀況下,Access-Control-Allow-Origin頭應該指定哪些域能夠發起一個CORS請求。只有須要跨域訪問的URL才設置CORS頭。

Access-Control-Allow-Origin: http://example.com:8080 http://foo.example.com

  以上Access-Control-Allow-Origin頭中,被設置爲只容許受信任的域能夠訪問。

Access-Control-Allow-Credentials: true

  只在須要時才使用上面這個header,由於若是用戶已經登陸的話,它會同時發送cookies/sessions。

  這些headers能夠經過web服務器、代理來進行配置,或者從服務器自己發送。不推薦在服務端實現,由於很不靈活。或者,能夠使用上面的第二種方式,在web服務器上配置一個用空格分隔的域的列表。更多關於CORS的內容能夠參考這裏:http://enable-cors.org/

支持JSONP

  JSONP經過利用GET請求避開瀏覽器的限制,從而實現對全部服務的調用。其工做原理是請求方在請求的URL上添加一個字符串查詢參數(例如:jsonp=」jsonp_callback」),其中「jsonp」參數的值是JavaScript函數名,該函數在有響應返回時將會被調用。

  因爲GET請求中沒有包含請求體,JSONP在使用時有着嚴重的侷限性,所以數據必須經過字符串查詢參數來傳遞。一樣的,爲了支持PUT,POST和DELETE方法,HTTP方法必須也經過字符串查詢參數來傳遞,相似_method=POST這種形式。像這樣的HTTP方法傳送方式是不推薦使用的,這會讓服務處於安全風險之中。

  JSONP一般在一些不支持CORS的老舊瀏覽器中使用,若是要改爲支持CORS的,會影響整個服務器的架構。或者咱們也能夠經過代理來實現JSONP。總之,JSONP正在被CORS所替代,咱們應該儘量地使用CORS。

  爲了在服務端支持JSONP,在JSONP字符串查詢參數傳遞時,響應必需要執行如下這些操做:

  1. 響應體必須封裝成一個參數傳遞給jsonp中指定的JavaScript函數(例如:jsonp_callback("<JSON response body>"))。
  2. 始終返回HTTP狀態碼200(OK),而且將真實的狀態做爲JSON響應中的一部分返回。

  另外,響應體中經常必須包含響應頭。這使得JSONP回調方法須要根據響應體來肯定響應處理方式,由於它自己沒法得知真實的響應頭和狀態值。

  下面的例子是按照上述方法封裝的一個返回error狀態的jsonp(注意:HTTP的響應狀態是200):

jsonp_callback("{'code':'404', 'status':'error','headers':[],'message':'resource XYZ not
found','data':'NotFoundException'}")

  成功建立後的響應相似於這樣(HTTP的響應狀態還是200):

jsonp_callback("{'code':'201', 'status':'error','headers':
[{'Location':'http://www.example.com/customers/12345'}],'data':'12345'}")

 

查詢,過濾和分頁

  對於大數據集,從帶寬的角度來看,限制返回的數據量是很是重要的。而從UI處理的角度來看,限制數據量也一樣重要,由於UI一般只能展示大數據集中的一小部分數據。在數據集的增加速度不肯定的狀況下,限制默認返回的數據量是頗有必要的。以Twitter爲例,要獲取某個用戶的推文(經過我的主頁的時間軸),若是沒有特別指定,請求默認只會返回20條記錄,儘管系統最多能夠返回200條記錄。

  除了限制返回的數據量,咱們還須要考慮如何對大數據集進行「分頁」或下拉滾動操做。建立數據的「頁碼」,返回大數據列表的已知片斷,而後標出數據的「前一頁」和「後一頁」——這一行爲被稱爲分頁。此外,咱們可能也須要指定響應中將包含哪些字段或屬性,從而限制返回值的數量,而且咱們但願最終可以經過特定值來進行查詢操做,並對返回值進行排序。

  有兩種主要的方法來同時限制查詢結果和執行分頁操做。首先,咱們能夠創建一個索引方案,它能夠以頁碼爲導向(請求中要給出每一頁的記錄數及頁碼),或者以記錄爲導向(請求中直接給出第一條記錄和最後一條記錄)來肯定返回值的起始位置。舉個例子,這兩種方法分別表示:「給出第五頁(假設每頁有20條記錄)的記錄」,或「給出第100到第120條的記錄」。

  服務端將根據運做機制來進行切分。有些UI工具,好比Dojo JSON會選擇模仿HTTP規範使用字節範圍。若是服務端支持out of box(即開箱即用功能),則前端UI工具和後端服務之間無需任何轉換,這樣使用起來會很方便。

  下文將介紹一種方法,既可以支持Dojo這樣的分頁模式(在請求頭中給出記錄的範圍),也能支持使用字符串查詢參數。這樣一來服務端將變得更加靈活,既能夠使用相似Dojo同樣先進的UI工具集,也能夠使用簡單直接的連接和標籤,而無需再爲此增長複雜的開發工做。但若是服務不直接支持UI功能,能夠考慮不要在請求頭中給出記錄範圍。

  要特別指出的是,咱們並不推薦在全部服務中使用查詢、過濾和分頁操做。並非全部資源都默認支持這些操做,只有某些特定的資源才支持。服務和資源的文檔應當說明哪些接口支持這些複雜的功能。

結果限制

  「給出第3到第55條的記錄」,這種請求數據的方式和HTTP的字節範圍規範更一致,所以咱們能夠用它來標識Range header。而「從第2條記錄開始,給出最多20條記錄」這種方式更易於閱讀和理解,所以咱們一般會用字符串查詢參數的方式來表示。

  綜上所述,推薦既支持使用HTTP Range header,也支持使用字符串查詢參數——offset(偏移量)和limit(限制),而後在服務端對響應結果進行限制。注意,若是同時支持這兩種方式,那麼字符串查詢參數的優先級要高於Range header。

  這裏你可能會有個疑問:「這兩種方法功能類似,可是返回的數據不徹底一致。這會不會讓人混淆呢?」恩…這是兩個問題。首先要回答的是,這的確會讓人混淆。關鍵是,字符串查詢參數看起來更加清晰易懂,在構建和解析時更加方便。而Range header則更可能是由機器來使用(偏向於底層),它更加符合HTTP使用規範。

  總之,解析Range header的工做會增長複雜度,相應的客戶端在構建請求時也須要進行一些處理。而使用單獨的limit和offset參數會更加容易理解和構建,而且不須要對開發人員有更多的要求。

用範圍標記進行限制

  當用HTTP header而不是字符串查詢參數來獲取記錄的範圍時,Ranger header應該經過如下內容來指定範圍: 

  Range: items=0-24

  注意記錄是從0開始的連續字段,HTTP規範中說明了如何使用Range header來請求字節。也就是說,若是要請求數據集中的第一條記錄,範圍應當從0開始算起。上述的請求將會返回前25個記錄,假設數據集中至少有25條記錄。

  而在服務端,經過檢查請求的Range header來肯定該返回哪些記錄。只要Range header存在,就會有一個簡單的正則表達式(如"items=(\d+)-(\d+)")對其進行解析,來獲取要檢索的範圍值。

用字符串查詢參數進行限制

  字符串查詢參數被做爲Range header的替代選擇,它使用offset和limit做爲參數名,其中offset表明要查詢的第一條記錄編號(與上述的用於範圍標記的items第一個數字相同),limit表明記錄的最大條數。下面的例子返回的結果與上述用範圍標記的例子一致:

  GET http://api.example.com/resources?offset=0&limit=25

  Offset參數的值與Range header中的相似,也是從0開始計算。Limit參數的值是返回記錄的最大數量。當字符串查詢參數中未指定limit時,服務端應當給出一個缺省的最大limit值,不過這些參數的使用都須要在文檔中進行說明。

基於範圍的響應

  對一個基於範圍的請求來講,不管是經過HTTP的Range header仍是經過字符串查詢參數,服務端都應該有一個Content-Range header來響應,以代表返回記錄的條數和總記錄數:

  Content-Range: items 0-24/66

  注意這裏的總記錄數(如本例中的66)不是從0開始計算的。若是要請求數據集中的最後幾條記錄,Content-Range header的內容應該是這樣:

  Content-Range: items 40-65/66

  根據HTTP的規範,若是響應時總記錄數未知或難以計算,也能夠用星號("*")來代替(如本例中的66)。本例中響應頭也可這樣寫:

  Content-Range: items 40-65/*

  不過要注意,Dojo或一些其它的UI工具可能不支持該符號。

分頁

  上述方式經過請求方指定數據集的範圍來限制返回結果,從而實現分頁功能。上面的例子中一共有66條記錄,若是每頁25條記錄,要顯示第二頁數據,Range header的內容以下:

  Range: items=25-49

  一樣,用字符串查詢參數表示以下:

  GET …?offset=25&limit=25

  服務端會相應地返回一組數據,附帶的Content-Range header內容以下:

  Content-Range: 25-49/66

  在大部分狀況下,這種分頁方式都沒有問題。但偶爾會有這種狀況,就是要返回的記錄數量沒法直接表示成數據集中的行號。還有就是有些數據集的變化很快,不斷會有新的數據插入到數據集中,這樣必然會致使分頁出現問題,一些重複的數據可能會出如今不一樣的頁中。

  按日期排列的數據集(例如Twitter feed)就是一種常見的狀況。雖然你仍是能夠對數據進行分頁,但有時用"after"或"before"這樣的關鍵字並與Range header(或者與字符串查詢參數offset和limit)配合來實現分頁,看起來會更加簡潔易懂。

  例如,要獲取給定時間戳的前20條評論:

  GET http://www.example.com/remarks/home_timeline?after=<timestamp> 

  Range: items=0-19

  GET http://www.example.com/remarks/home_timeline?before=<timestamp> 

  Range: items=0-19

  用字符串查詢參數表示爲:

  GET http://www.example.com/remarks/home_timeline?after=<timestamp>&offset=0&limit=20 

  GET http://www.example.com/remarks/home_timeline?before=<timestamp>&offset=0&limit=20

  有關在不一樣狀況對時間戳的格式化處理,請參見下文的「日期/時間處理」。

  若是請求時沒有指定要返回的數據範圍,服務端返回了一組默認數據或限定的最大數據集,那麼服務端同時也應該在返回結果中包含Content-Range header來和客戶端進行確認。以上面我的主頁的時間軸爲例,不管客戶端是否指定了Range header,服務端每次都只返回20條記錄。此時,服務端響應的Content-Range header應該包含以下內容:

  Content-Range: 0-19/4125

  或 Content-Range: 0-19/*

結果的過濾和排序

  針對返回結果,還須要考慮如何在服務端對數據進行過濾和排列,以及如何按指定的順序對子數據進行檢索。這些操做能夠與分頁、結果限制,以及字符串查詢參數filter和sort等相結合,能夠實現強大的數據檢索功能。

  再強調一次,過濾和排序都是複雜的操做,不須要默認提供給全部的資源。下文將介紹哪些資源須要提供過濾和排序。

過濾

  在本文中,過濾被定義爲「經過特定的條件來肯定必需要返回的數據,從而減小返回的數量」。若是服務端支持一套完整的比較運算符和複雜的條件匹配,過濾操做將變得至關複雜。不過咱們一般會使用一些簡單的表達式,如starts-with(以...開始)或contains(包含)來進行匹配,以保證返回數據的完整性。

  在咱們開始討論過濾的字符串查詢參數以前,必須先明白爲何要使用單個參數而不是多個字符串查詢參數。從根本上來講是爲了減小參數名稱的衝突。咱們已經有offsetlimitsort(見下文)參數了。若是可能的話還會有jsonpformat標識符,或許還會有afterbefore參數,這些都是在本文中提到過的字符串查詢參數。字符串查詢中使用的參數越多,就越可能致使參數名稱的衝突,而使用單個過濾參數則會將衝突的可能性降到最低。

  此外,從服務端也很容易僅經過單個的filter參數來判斷請求方是否須要數據過濾功能。若是查詢需求的複雜度增長,單個參數將更具備靈活性——能夠本身創建一套功能完整的查詢語法(詳見下文OData註釋或訪問http://www.odata.org)。

  經過引入一組常見的、公認的分隔符,用於過濾的表達式能夠以很是直觀的形式被使用。用這些分隔符來設置過濾查詢參數的值,這些分隔符所建立的參數名/值對可以更加容易地被服務端解析並提升數據查詢的性能。目前已有的分隔符包括用來分隔每一個過濾短語的豎線("|")和用來分隔參數名和值的雙冒號("::")。這套分隔符足夠惟一,並適合大多數狀況,同時用它來構建的字符串查詢參數也更加容易理解。下面將用一個簡單的例子來介紹它的用法。假設咱們想要給名爲「Todd」的用戶們發送請求,他們住在丹佛,有着「Grand Poobah」之稱。用字符串查詢參數實現的請求URI以下:

  GET http://www.example.com/users?filter="name::todd|city::denver|title::grand poobah"

  雙冒號("::")分隔符將屬性名和值分開,這樣屬性值就可以包含空格——服務端能更容易地從屬性值中解析出分隔符。

  注意查詢參數名/值對中的屬性名要和服務端返回的屬性名相匹配。

  簡單而有效。有關大小寫敏感的問題,要根據具體狀況來看,但總的來講,在不用關心大小寫的狀況下,過濾功能能夠很好地運做。若查詢參數名/值對中的屬性值未知,你也能夠用星號("*")來代替。

  除了簡單的表達式和通配符以外,若要進行更復雜的查詢,你必需要引入運算符。在這種狀況下,運算符自己也是屬性值的一部分,可以被服務端解析,而不是變爲屬性名的一部分。當須要複雜的query-language-style(查詢語言風格)功能時,可參考Open Data Protocol (OData) Filter System Query Option說明中的查詢概念(詳見http://www.odata.org/documentation/uriconventions#FilterSystemQueryOption)。

排序

  排序決定了從服務端返回的記錄的順序。也就是對響應中的多條記錄進行排序。

  一樣,咱們這裏只考慮一些比較簡單的狀況。推薦使用排序字符串查詢參數,它包含了一組用分隔符分隔的屬性名。具體作法是,默認對每一個屬性名按升序排列,若是屬性名有前綴"-",則按降序排列。用豎線("|")分隔每一個屬性名,這和前面過濾功能中的參數名/值對的作法同樣。

  舉個例子,若是咱們想按用戶的姓和名進行升序排序,而對僱傭時間進行降序排序,請求將是這樣的:

  GET http://www.example.com/users?sort=last_name|first_name|-hire_date

  再次強調一下,查詢參數名/值對中的屬性名要和服務端返回的屬性名相匹配。此外,因爲排序操做比較複雜,咱們只對須要的資源提供排序功能。若是須要的話也能夠在客戶端對小的資源集合進行排列。

 

服務版本管理

   坦率地講,一說到版本就會讓人以爲很困難,很麻煩,不太容易,甚至會讓人以爲難受——由於這會增長API的複雜度,並同時可能會對客戶端產生一些影響。所以,在API的設計中要儘可能避免多個不一樣的版本。

  不支持版本,不將版本控制做爲糟糕的API設計的依靠。若是你在APIs的設計中引入版本,這早晚都會讓你抓狂。因爲返回的數據經過JSON來呈現,客戶端會因爲不一樣的版本而接收到不一樣的屬性。這樣就會存在一些問題,如從內容自己和驗證規則方面改變了一個已存在的屬性的含義。

  固然,咱們沒法避免API可能在某些時候須要改變返回數據的格式和內容,而這也將致使消費端的一些變化,咱們應當避免進行一些重大的調整。將API進行版本化管理是避免這種重大變化的一種有效方式。

經過內容協商支持版本管理

  以往,版本管理經過URI自己的版本號來完成,客戶端在請求的URI中標明要獲取的資源的版本號。事實上,許多大公司如Twitter、Yammer、Facebook、Google等常常在他們的URI裏使用版本號。甚至像WSO2這樣的API管理工具也會在它的URLs中要求版本號。

  面向REST原則,版本管理技術飛速發展。由於它不包含HTTP規範中內置的header,也不支持僅當一個新的資源或概念被引入時才應該添加新URI的觀點——即版本不是表現形式的變化。另外一個反對的理由是資源URI是不會隨時間改變的,資源就是資源。

  URI應該能簡單地識別資源——而不是它的「形狀」(狀態)。另外一個就是必須指定響應的格式(表徵)。還有一對HTTP headers:Accept 和 Content-Type。Accept header容許客戶端指定所但願或能支持的響應的媒體類型(一種或多種)。Content-Type header可分別被客戶端和服務端用來指定請求或響應的數據格式。

  例如,要獲取一個user的JSON格式的數據:

  #Request:

  GET http://api.example.com/users/12345
  Accept: application/json; version=1

  #Response:

  HTTP/1.1 200 OK
  Content-Type: application/json; version=1

  {"id":"12345", "name":"Joe DiMaggio"}

  如今,咱們對同一資源請求版本2的數據:

  #Request:

  GET http://api.example.com/users/12345
  Accept: application/json; version=2

  #Response:

  HTTP/1.1 200 OK
  Content-Type: application/json; version=2

  {"id":"12345", "firstName":"Joe", "lastName":"DiMaggio"}

  Accept header被用來表示所指望的響應格式(以及示例中的版本號),注意以上兩個相同的URI是如何作到在不一樣的版本中識別資源的。或者,若是客戶端須要一個XML格式的數據,能夠將Accept header設置爲"application/xml",若是須要的話也能夠帶一個指定的版本號。

  因爲Accept header能夠被設置爲容許多種媒體類型,在響應請求時,服務器將把響應的Content-Type header設置爲最匹配客戶端請求內容的類型。更多信息能夠參考http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.Html 。

  例如:

  #Request

  GET http://api.example.com/users/12345

  Accept: application/json; version=1, application/xml; version=1

  上述請求中,假設服務器支持JSON 和XML格式的請求,或者兩種都支持,那麼將由服務器來決定最終返回哪一種類型的數據。但不管服務器選擇哪種,都會在響應中包含Content-Type header。

  例如,若是服務器返回application/xml格式的數據,結果是:

  #Response

  HTTP/1.1 200 OK
  Content-Type: application/xml; version=1

  <user>
    <id>12345</id>
    <name>Joe DiMaggio</name>
  </user>

  爲了說明Content-Type在發送數據給服務器時的用處,這裏給出一個用JSON格式建立新用戶的例子:

  #Request

  POST http://api.example.com/users
  Content-Type: application/json;version=1

  {"name":"Marco Polo"}

  或者,調用版本2的接口:

  #Request

  POST http://api.example.com/users
  Content-Type: application/json;version=2

  {"firstName":"Marco", "lastName":"Polo"}

當沒有指定版本時,返回什麼版本?

  並不須要在每個請求中都指定版本號。因爲HTTP content-negotiation(內容協商)遵循類型的「最佳匹配」方式,因此你的API也應該遵循這一點。根據這一原則,當客戶端沒有指定版本時,API應當返回所支持的最先版本。

  仍是這個例子,獲取一個user的JSON格式的數據:

  #Request

  GET http://api.example.com/users/12345
  Accept: application/json

  #Response

  HTTP/1.1 200 OK
  Content-Type: application/json; version=1

  {"id":"12345", "name":"Joe DiMaggio"}

  相應地,當以POST方式向服務器發送數據時,若是服務器支持多個不一樣版本,而請求時又沒有指定版本,和上面的例子同樣——服務器會將最小/最先版本的數據包含在body中。爲了進行說明,下面的例子以JSON格式請求一個包含多版本資源的服務器,來建立一個新用戶(預期會返回版本1):

  #Request

  POST http://api.example.com/users
  Content-Type: application/json

  {"name":"Marco Polo"}

  #Response

  HTTP/1.1 201 OK
  Content-Type: application/json; version=1
  Location: http://api.example.com/users/12345

  {"id":"12345", "name":"Marco Polo"}

請求不支持的版本

  當請求一個不支持的版本號時(包含在API生命週期中已經消失的資源版本),API應當返回一個錯誤的HTTP狀態碼406(表示不被接受)。此外,API還應當返回一個帶有Content-Type: application/json的響應體,其中包含一個JSON數組,用於說明該服務器支持的類型。

  #Request

  GET http://api.example.com/users/12345
  Content-Type: application/json; version=999

  #Response

  HTTP/1.1 406 NOT ACCEPTABLE 

  Content-Type: application/json

  ["application/json; version=1", "application/json; version=2", "application/xml; version=1", "application/xml; version=2"]

何時應該建立一個新版本?

  API開發中的不少方面都會打破約定,並最終對客戶端產生一些不良影響。若是你不肯定API的修改會帶來怎樣的後果,保險起見最好考慮使用版本控制。當你在考慮提供一個新版本是否合適時,或者考慮對現有的返回表徵進行修改是否必定能知足須要並被客戶端所接受時,有這樣幾個因素要考慮。

破壞性的修改

  • 改變屬性名(例如將"name"改爲"firstName")
  • 刪除屬性
  • 改變屬性的數據類型(例如將numeric變爲string, boolean變爲bit/numeric,string 變爲 datetime等等)
  • 改變驗證規則
  • 在Atom樣式的連接中,修改"rel"的值
  • 在現有的工做流中引入必要資源
  • 改變資源的概念/意圖;概念/意圖或資源狀態的意義不一樣於它原始的意義。例如:
    • 一個content type是text/html的資源,以前表示的是全部支持的媒體類型的一個"links"集合,而新的text/html則表示的是用戶輸入的「web瀏覽器表單」。
    • 一個帶有"endTime"參數的API,對資源"…/users/{id}/exams/{id}"表達的含義是學生在那個時間提交試卷,而新的含義則是考試的預約結束時間。
  • 經過添加新的字段來改變現有的資源。將兩個資源合併爲一個並棄用原有的資源。
    • 有這樣兩個資源"…/users/{id}/dropboxBaskets/{id}/messages/{id}"和"…/users/{id}/dropboxBaskets/{id}/messages/{id}/readStatus"。新需求是把readStatus資源的屬性放到單獨的message資源中,並棄用readStatus資源。這將致使messages資源中指向readStatus資源的連接被移除。

  雖然上面列出的並不全面,但它給出了一些會對客戶端產生破壞性影響的變化類型,這時須要考慮提供一個新資源或新版本。

非破壞性的修改

  • 在返回的JSON中添加新屬性
  • 添加指向其它資源的"link"
  • 添加content-type支持的新格式
  • 添加content-language支持的新格式
  • 因爲API的建立者和消費者都要處理不一樣的casing,所以casing的變化可有可無

版本控制應在什麼級別出現?

  建議對單個的資源進行版本控制。對API的一些改變,如修改工做流,也許要跨多個資源的版本控制,以此來防止對客戶端產生破壞性的影響。

利用Content-Location來加強響應

  可選。見RDF(Resource Description Framework,即資源描述框架)規範。

帶有Content-Type的連接

  Atom風格的連接支持"type"屬性。提供足夠的信息以便客戶端能夠對特定的版本和內容類型進行調用。

找出支持的版本

我應該同時支持多少個版本?

  維護多個不一樣的版本會讓工做變得繁瑣、複雜、容易出錯,並且代價高,對於任何給定的資源,你應該支持不超過2個版本。

棄用

  Deprecated(棄用)的目的是用來講明資源對API仍然可用,但在未來會不存在並變得不可用。注意:棄用的時長將由棄用策略決定——這裏並無給出定義。

我如何告知客戶端被棄用的資源?

  許多客戶端未來訪問的資源可能在新版本引入後會被廢棄掉,所以,他們須要有一種方法來發現和監控他們的應用程序對棄用資源的使用。當請求一個棄用資源時,API應該正常響應,並帶有一個布爾類型的自定義Header "Deprecated"。如下用一個例子來進行說明。

  #Request

  GET http://api.example.com/users/12345
  Accept: application/json
  Content-Type: application/json; version=1

  #Response

  HTTP/1.1 200 OK
  Content-Type: application/json; version=1
  Deprecated: true
  {「id」:」12345」, 「name」:」Joe DiMaggio」}

 

日期/時間處理

  若是沒有妥善地、一致地處理好日期和時間的話,這將成爲一個大麻煩。咱們常常會碰到時區的問題,並且因爲日期在JSON中是以字符串的格式存在的,若是未指定統一的格式,那麼解析日期也會是一個問題。

  在接口內部,服務端應該以UTC或GMT時間來存儲、處理和緩存時間戳。這將有效緩解日期和時間的問題。

Body內容中的日期/時間序列化

  有一個簡單的方法能夠解決這些問題——在字符串中始終用相同的格式,包括時間片(帶有時區信息)。ISO8601時間格式是一個不錯的解決方案,它使用了徹底加強的時間格式,包括小時、分鐘、秒以及秒的小數部分(例如yyyy-MM-dd'T'HH:mm:ss.SSS'Z')。建議在REST服務的body內容中(請求和響應均包括)使用ISO8601表明全部的日期格式。

  順便提一下,對於那些基於JAVA的服務來講,DateAdapterJ庫使用DateAdapter,Iso8601TimepointAdapter和HttpHeaderTimestampAdapter類能夠很是容易地解析和格式化ISO8601日期和時間,以及HTTP 1.1 header(RFC1123)格式。能夠從https://github.com/tfredrich/DateAdapterJ下載。

  對於那些建立基於瀏覽器的用戶界面來講,ECMAScript5規範一開始就包含了JavaScript解析和建立ISO8601日期的內容,因此它應該成爲咱們所說的主流瀏覽器所聽從的方式。固然,若是你要支持那些不能自動解析日期的舊版瀏覽器,能夠使用JavaStript庫或正則表達式。這裏有幾個能夠解析和建立ISO8601時間的JavaStript庫:

  http://momentjs.com/

  http://www.datejs.com/

HTTP Headers中的日期/時間序列化

  然而上述建議僅適用於HTTP請求或響應內容中的JSON和XML內容,HTTP規範針對HTTP headers使用另外一種不一樣的格式。在被RFC1123更替的RFC822中指出,該格式包括了各類日期、時間和date-time格式。不過,建議始終使用時間戳格式,在你的request headers中它看起來像這樣:

  Sun, 06 Nov 1994 08:49:37 GMT

  不過,這種格式沒有考慮毫秒或者秒的十進制小數。Java的SimpleDataFormat的格式串是:"EEE, dd MMM yyyy HH:mm:ss 'GMT'"。

 

保護服務的安全

  Authentication(身份認證)指的是確認給定的請求是從服務已知的某人(或某個系統)發出的,且請求者是他本身所聲明的那我的。Authentication是爲了驗證請求者的真實身份,而authorization(受權)是爲了驗證請求者有權限去執行被請求的操做。

  本質上,這個過程是這樣的:

  1. 客戶端發起一個請求,將authentication的token(身份認證令牌)包含在X-Authentication header中,或者將token附加在請求的查詢串參數中。
  2. 服務器對authorization token(受權令牌)進行檢查,並進行驗證(有效且未過時),並根據令牌內容解析或者加載認證主體。
  3. 服務器調用受權服務,提供認證主體、被請求資源和必要的操做許可。
  4. 若是受權經過了,服務器將會繼續正常運行。

  上面第三步的開銷可能會比較大,可是假設若是存在一個可緩存的權限控制列表(ACL),那麼在發出遠程請求前,能夠在本地建立一個受權客戶端來緩存最新的ACLs。

身份驗證

  目前最好的作法是使用OAuth身份驗證。強烈推薦OAuth2,不過它仍然處於草案狀態。或者選擇OAuth1,它徹底能夠勝任。在某些狀況下也能夠選擇3-Legged OAuth。更多有關OAuth的規範能夠查看這裏http://oauth.net/documentation/spec/

  OpenID是一個附加選擇。不過建議將OpenID做爲一個附加的身份驗證選項,以OAuth爲主。更多有關OpenID的規範能夠查看這裏http://openid.net/developers/specs/

傳輸安全

  全部的認證都應該使用SSL。OAuth2須要受權服務器和access token(訪問令牌)來使用TLS(安全傳輸層協議)。

  在HTTP和HTTPS之間切換會帶來安全隱患,最好的作法是全部通信默認都使用TLS。

受權

  對服務的受權和對任何應用程序的受權同樣,沒有任何區別。它基於這樣一個問題:「主體是否對給定的資源有請求的許可?」這裏給出了簡單的三項數據(主體,資源和許可),所以很容易構造一個支持這種概念的受權服務。其中主體是被授予資源訪問許可的人或系統。使用這些通常概念,就能夠爲每個主題構建一個緩存訪問控制列表(ALC)。

應用程序安全

  對RESTful服務來講,開發一個安全的web應用適用一樣的原則。

  • 在服務器上驗證全部輸入。接受「已知」的正確的輸入並拒絕錯誤的輸入。
  • 防止SQL和NoSQL注入。
  • 使用library如微軟的Anti-XSS或OWASP的AntiSammy來對輸出的數據進行編碼。
  • 將消息的長度限制在肯定的字段長度內。
  • 服務應該只顯示通常的錯誤信息。
  • 考慮業務邏輯攻擊。例如,攻擊者能夠跳過多步驟的訂購流程來訂購產品而無需輸入信用卡信息嗎?
  • 對可疑的活動記錄日誌。

  RESTful安全須要注意的地方:

  • 驗證數據的JSON和XML格式。
  • HTTP動詞應該被限制在容許的方法中。例如,GET請求不能刪除一個實體。GET用來讀取實體而DELETE用來刪除實體。
  • 注意race conditions(競爭條件——因爲兩個或者多個進程競爭使用不能被同時訪問的資源,使得這些進程有可能由於時間上推動的前後緣由而出現問題)。

  API網關可用於監視、限制和控制對API的訪問。如下內容可由網關或RESTful服務實現。

  • 監視API的使用狀況,並瞭解哪些活動是正常的,哪些是非正常的。
  • 限制API的使用,使惡意用戶不能停掉一個API服務(DOS攻擊),而且有能力阻止惡意的IP地址。
  • 將API密鑰存儲在加密的安全密鑰庫中。

 

緩存和可伸縮性

  經過在系統層級消除經過遠程調用來獲取請求的數據,緩存提升了系統的可擴展性。服務經過在響應中設置headers來提升緩存的能力。遺憾的是,HTTP 1.0中與緩存相關的headers與HTTP 1.1不一樣,所以服務器要同時支持兩種版本。下表給出了GET請求要支持緩存所必須的最少headers集合,並給出了適當的描述。

HTTP Header 描述 示例
Date 響應返回的日期和時間(RFC1123格式)。 Date: Sun, 06 Nov 1994 08:49:37 GMT
Cache-Control 響應可被緩存的最大秒數(最大age值)。若是響應不支持緩存,值爲no-cache。

Cache-Control: 360

Cache-Control: no-cache

Expires 若是給出了最大age值,該時間戳(RFC1123格式)表示的是響應過時的時間,也就是Date(例如當前日期)加上最大age值。若是響應不支持緩存,該headers不存在。 Expires: Sun, 06 Nov 1994 08:49:37 GMT
Pragma 當Cache-Control爲no-cache時,該header的值也被設置爲no-cahche。不然,不存在。 Pragma: no-cache
Last-Modified 資源自己最後被修改的時間戳(RFC1123格式)。 Last-Modified: Sun, 06 Nov1994 08:49:37 GMT

  爲了簡化,這裏舉一個響應中的headers集合的例子。這是一個簡單的對資源進行GET請求的響應,緩存時長爲一天(24小時):

  Cache-Control: 86400
  Date: Wed, 29 Feb 2012 23:01:10 GMT
  Last-Modified: Mon, 28 Feb 2011 13:10:14 GMT
  Expires: Thu, 01 Mar 2012 23:01:10 GMT

  下面是一個相似的例子,不過緩存被徹底禁用:

  Cache-Control: no-cache
  Pragma: no-cache

ETag Header

  ETag header對於驗證緩存數據的新舊程度頗有用,同時也有助於條件的讀取和更新操做(分別爲GET和PUT)。它的值是一個任意字符串,用來表明返回數據的版本。不過,對於返回數據的不一樣格式,它也能夠不一樣——JSON格式響應的ETag與相同資源XML格式響應的ETag會不一樣。ETag header的值能夠像帶有格式的底層域對象的哈希表(例如Java中的Obeject.hashcode())同樣簡單。建議爲每一個GET(讀)操做返回一個ETag header。另外,確保用雙引號包含ETag的值,例如:

  ETag: "686897696a7c876b7e"

 

HTTP狀態碼(前10)

  如下是由RESTful服務或API返回的最經常使用的HTTP狀態碼,以及一些有關它們廣泛用法的簡短說明。其它HTTP狀態碼不太常用,它們要麼更特殊,要麼更高級。大多數服務套件只支持這些經常使用的狀態碼,甚至只支持其中的一部分,而且它們都能正常工做。

  200 (OK) —— 一般的成功狀態。表示成功的最多見代碼。

  201 (CREATED) ——(經過POST或PUT)建立成功。經過設置Location header來包含一個指向最新建立的資源的連接。

  204 (NO CONTENT) —— 封裝過的響應沒有使用,或body中沒有任何內容時(如DELETE),使用該狀態。

  304 (NOT MODIFIED) —— 用於有條件的GET調用的響應,以減小帶寬的使用。 若是使用該狀態,那麼必須爲GET調用設置Date、Content-Location和ETag headers。不包含響應體。

  400 (BAD REQUEST) —— 用於執行請求時可能引發無效狀態的通常錯誤代碼。如域名無效錯誤、數據丟失等。

  401 (UNAUTHORIZED) —— 用於缺乏認證token或認證token無效的錯誤代碼。

  403 (FORBIDDEN) —— 未受權的用戶執行操做,沒有權限訪問資源,或者因爲某些緣由資源不可用(如時間限制等),使用該錯誤碼。

  404 (NOT FOUND) —— 不管資源存不存在,不管是否有40一、403的限制,當請求的資源找不到時,出於安全因素考慮,服務器均可以使用該錯誤碼來掩飾。

  409 (CONFLICT) —— 每當執行請求可能會引發資源衝突時使用。例如,存在重複的實體,

相關文章
相關標籤/搜索