rest-簡介

 一說到REST,我想你們的第一反應就是「啊,就是那種先後臺通訊方式。」可是在要求詳細講述它所提出的各個約束,以及如何開始搭建REST服務時,卻不多有人可以清晰地說出它究竟是什麼,須要遵照什麼樣的準則。html

  在您將看到的這一篇文章中,咱們將對REST,尤爲是基於HTTP的REST服務進行詳細地介紹。經過這些文章,您不只能夠了解到什麼是REST,更能清晰地瞭解到您在編寫REST服務時所須要遵照的各個守則,設計RESTful API時須要考慮的各類因素以及實現過程當中可能遇到的問題等內容。web

 

REST示例算法

  我想,不少讀者可能並不太清楚REST究竟是一個什麼概念。那麼,首先讓咱們來看一個簡單的基於HTTP的REST服務示例。數據庫

  假設用戶正在訪問一個電子商務網站www.egoods.com。該網站對其所銷售的各個物品進行了詳細分類。當用戶登陸該網站進行購物時,他首先須要在該網站上選擇其所須要尋找物品的分類,進而列出屬於該分類的各個物品。json

  固然,雖然從業務邏輯的角度來講這個流程很是簡單,但實際上瀏覽器向後臺發送了多個請求:頁面邏輯在頁面加載時將首先獲得全部的商品分類,並將這些分類顯示在了頁面中。在用戶選擇了一個分類的時候,頁面邏輯將發送一個請求獲得該分類的詳細信息,併發送另一個請求來獲得該分類的商品列表:api

  在經過瀏覽器的調試功能查看這些請求的時候,咱們能夠看到其首先向www.egoods.com/api/categories發送一個GET請求,以取得全部的商品分類:數組

1 GET /api/categories
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  而服務端將返回全部的類別:瀏覽器

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4  
 5 [
 6    {
 7       "label" : "食品",
 8       "url" : "/api/categories/1"
 9    }, {
10       "label" : "服裝",
11       "url" : "/api/categories/2"
12    }
13    ...
14    {
15       "label" : "電子設備",
16       "url" : "/api/categories/25"
17    }
18 ]

  該響應返回了一個用JSON表示的數組。該數組中的每一個元素包含了兩部分信息:用戶可以讀懂的表示分類名稱的label以及相應分類所對應的URL。其中Label所記錄的分類名稱將在頁面中顯示給用戶。而在用戶根據label所標示的分類名選擇了一個分類的時候,頁面邏輯會取得該分類所對應的URL並向該URL 發送請求,以獲得該分類的詳細信息。例如在用戶點擊了「食品」這個分類的時候,瀏覽器將會向服務器發送以下的請求:緩存

1 GET /api/categories/1
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  這一次,頁面邏輯根據用戶對分類的選擇「食品」來獲得了其所對應的URL,並向該URL發送了一個GET請求。而該請求所獲得的響應則爲:性能優化

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: xxx
 
{
   "url" : "/api/categories/1",
   "label" : "Food",
   "items_url" : "/api/items?category=1",
   "brands" : [
         {
            "label" : "友臣",
            "brand_key" : "32073",
            "url" : "/api/brands/32073"
         }, {
            "label" : "樂事",
            "brand_key" : "56632",
            "url" : "/api/brands/56632"
         }
         ...
   ],
   "hot_searches" : …
}

  該響應略爲複雜。首先,響應中的URL標示了「食品」分類所對應的URL。而label屬性則和前面同樣,用來在頁面上顯示分類的名稱。一個較爲特殊的屬性則是items_url。其用來標示獲取屬於食品分類的各個產品的URL。而屬性brands則用來列出在「食品」分類中的著名品牌,例如友臣,樂事等。這些品牌被組織爲一個對象數組,而數組中的每一個對象都擁有label,url等屬性。在這些屬性的幫助下,頁面能夠列出這些著名品牌的名稱,並容許用戶經過點擊跳轉到這些品牌所對應的頁面上。除了這些屬性以外,Food分類還包含了其它一系列屬性,如表示當前其它用戶正在搜索的hot_searches屬性等,這裏就再也不贅述。

  該響應有一個問題,那就是符合用戶篩選條件的各個產品並無包含在該響應中。這是由於頁面所列出的各個產品是根據用戶所設置的篩選條件,即其選擇的品牌以及搜索關鍵字而變化的。所以,頁面邏輯會根據屬性items_url以及用戶所設定的搜索條件組合成爲目標URL,再次發送請求到後臺,以請求須要在頁面中展示的各個物品。

  例如用戶在只想瀏覽屬於樂事品牌的食品時,其能夠鉤選樂事這個品牌,那麼此時的URL將由食物分類的items_url以及表示按照品牌進行篩選的URL參數共同組成:

1 GET /api/items?category=1&brand_key=56632
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  如今讓咱們來總結一下上面所展現的基於HTTP的REST系統的整個運行流程。在開始的時候,咱們拿到了全部分類的列表。列表中的各個條目不只僅包含了用戶能夠看到的分類名稱等信息,更擁有一個額外的URL屬性。在用戶選擇該列表中的一項時,頁面邏輯將會向對應的URL發送一個請求,以得到該項目的詳細信息。在這個詳細信息中,一些內容又包含了一些其它的URL,從而使得頁面邏輯又能經過該URL屬性發送請求。

  您也許會說,哎,這不和咱們現有系統的運行流程同樣的嘛。是的。在上面所舉出的例子中,咱們也更偏重地描述了REST系統所須要具備的HATEOAS(Hypermedia As The Engine Of Application State)特性。正是因爲這個特性已經在你們所建立的系統裏面普遍地使用了,所以我更但願從熟悉的地方入手,而不是開始就很是教條地說REST必定要這樣,必定要那樣,徒增了學習的難度。

  反過來講,上面所展現的REST服務並不具備典型性。在充分了解了REST後,您會發現,REST在系統設計上的視角將再也不把流程放在了最優先的位置。

  而在後面的章節中,咱們則會逐漸展開,詳細地介紹如何建立一個純正的基於HTTP的REST服務。

 

REST的定義

  OK,如今讓咱們來看看REST的定義。Wikipedia是這樣描述它的:

 

Representational State Transfer (REST) is a software architecture style consisting of guidelines and best practices for creating scalable web services. REST is a coordinated set of constraints applied to the design of components in a distributed hypermedia system that can lead to a more performant and maintainable architecture.

 

  從上面的定義中,咱們能夠發現REST實際上是一種組織Web服務的架構,而並非咱們想象的那樣是實現Web服務的一種新的技術,更沒有要求必定要使用HTTP。其目標是爲了建立具備良好擴展性的分佈式系統。

  反過來,做爲一種架構,其提出了一系列架構級約束。這些約束有:

  1. 使用客戶/服務器模型。客戶和服務器之間經過一個統一的接口來互相通信。
  2. 層次化的系統。在一個REST系統中,客戶端並不會固定地與一個服務器打交道。
  3. 無狀態。在一個REST系統中,服務端並不會保存有關客戶的任何狀態。也就是說,客戶端自身負責用戶狀態的維持,並在每次發送請求時都須要提供足夠的信息。
  4. 可緩存。REST系統須要可以恰當地緩存請求,以儘可能減小服務端和客戶端之間的信息傳輸,以提升性能。
  5. 統一的接口。一個REST系統須要使用一個統一的接口來完成子系統之間以及服務與用戶之間的交互。這使得REST系統中的各個子系統能夠獨自完成演化。

  若是一個系統知足了上面所列出的五條約束,那麼該系統就被稱爲是RESTful的。

  下面咱們再次經過電子商務網站egoods這個示例來幫助咱們理解這些約束。首先,egoods是一個電子商務網站。用戶須要經過瀏覽器,手機或者網站所發佈的瀏覽應用來訪問該網站的內容。所以其使用的天然是客戶/服務器模型。而在瀏覽過程當中,用戶須要訪問不一樣類型的數據,如商品描述、購物車等信息。這些信息可能由egoods網站服務中不一樣的服務器來提供的,所以在用戶瀏覽過程當中可能須要與不止一個服務器進行交互。若是在服務端保存了有關客戶的任何狀態,那麼在用戶與不一樣服務器進行交互的時候,客戶的狀態就須要在這些服務之間進行同步,大大地增長了系統的複雜度。所以,REST要求客戶端自行維護狀態,並在每次發送請求的時候提供自身所儲存的處理該請求所必需的信息。而恰當地使用緩存這一條也很是容易理解。在客戶端請求一個自上次請求後沒有發生過變化的信息時,如產品分類列表,服務端僅僅須要返回一個304響應便可。

  這裏您能夠看到,前四條約束中除了無狀態這條約束較爲特別以外,其它三條約束在基於HTTP的Web服務中都很常見,也較容易達成。而無狀態約束在其它類型的Web服務中並不十分常見,所以如何避免違反該約束是在實現REST服務時最常討論的話題。其不只僅會影響到不少功能的設計,更是REST系統擴展性的關鍵。所以在後面的章節中,咱們會對無狀態約束單獨進行講解。

  在簡單地介紹了前四個約束以後,咱們就須要着重講解統一接口這個約束了。能夠說,前面的四個約束實際上都較爲容易達成。惟一須要注意的無非是是否某些技術實現違反了這些約束。而第五條約束,統一接口,能夠說是REST服務設計的核心所在,也是決定REST服務設計的成敗之處。在實現一個基於HTTP的REST服務時,軟件開發人員不只僅須要考慮REST所設置的一系列約束,更須要考慮HTTP各組成的語意,HTTP相關技術如何與REST服務約束結合,如何保持先後向兼容性以及如何進行版本管理等問題,才能給出一個天然的,具備較高易用性和較強生命力的REST系統。

  而在介紹統一接口約束以前,咱們則須要瞭解一下和REST密切相關的兩個名詞:資源和狀態。能夠說,資源是REST系統的核心概念。全部的設計都會以資源爲中心,包括如何對資源進行添加,更新,查找以及修改等。而資源自己則擁有一系列狀態。在每次對資源進行添加 ,刪除或修改的時候,資源就將從一個狀態轉移到另一個狀態。

  好比說,在egoods中,商品的分類就是一種資源。該資源有不少實例,包括表示食品的分類,其所對應的URL是「/api/categories/1」。一樣地,食品的品牌也是一種資源。這些資源的實例都對應着一個當前的狀態。在修改了一個資源實例以後,好比修改了食品分類中的熱搜關鍵字,那麼其將對應着一個新的狀態。這種狀態之間的變化被稱爲是狀態的轉移。

  在大概瞭解了REST系統中的資源和狀態的定義後,咱們來看看統一接口這個約束。該約束又包含了四個子約束:

  1. 每一個資源都擁有一個資源標識。每一個資源的資源標識能夠用來惟一地標明該資源。
  2. 消息的自描述性。在REST系統中所傳遞的消息須要可以提供自身如何被處理的足夠信息。例如該消息所使用的MIME類型,是否能夠被緩存等。
  3. 資源的自描述性。一個REST系統所返回的資源須要可以描述自身,並提供足夠的用於操做該資源的信息,如如何對資源進行添加,刪除以及修改等操做。也就是說,一個典型的REST服務不須要額外的文檔對如何操做資源進行說明。
  4. HATEOAS。即客戶只能夠經過服務端所返回各結果中所包含的信息來獲得下一步操做所須要的信息,如究竟是向哪一個URL發送請求等。也就是說,一個典型的REST服務不須要額外的文檔標示經過哪些URL訪問特定類型的資源,而是經過服務端返回的響應來標示到底能在該資源上執行什麼樣的操做。一個REST服務的客戶端也不須要知道任何有關哪裏有什麼樣的資源這種信息。

  如今,讓咱們仍然以egoods做爲示例來解釋一下上面四個子約束。

  在前面的章節中,咱們已經看到了從egoods所返回的表示食品這個分類的響應:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4 
 5 {
 6    "url" : "/api/categories/1",
 7    "label" : "Food",
 8    "items_url" : "/api/items?category=1",
 9    "brands" : [
10          {
11             "label" : "友臣",
12             "brand_key" : "32073",
13             "url" : "/api/brands/32073"
14          }, {
15             "label" : "樂事",
16             "brand_key" : "56632",
17             "url" : "/api/brands/56632"
18          }
19          ...
20    ],
21    "hot_searches" : …
22 }

  首先咱們看到的是,該響應經過Content-Type響應頭來標示響應中所包含的信息是按照JSON格式來組織的。在看到了該響應頭中所標示的格式以後,消息的接收方就能夠按照JSON的格式理解或分析該響應中的負載。這也即是消息的自描述性。

  固然,消息的自描述性不只僅包含如何解析其所攜帶的負載。在一個基於HTTP的REST系統中,咱們能夠經過使用大部分HTTP標準所提供的功能來提升消息的自描述性。因爲這些功能已經擁有了完備的文檔,被廣大的軟件開發人員所熟知,並獲得了衆多瀏覽器廠商以及Web類庫的支持,所以根據這些標準實現REST服務具備較高的消息自描述性。舉例來講,若是在請求中標明瞭If-Modified-Since頭,那麼服務端將可能返回一個304 Not Modified響應。在看到該響應的時候,瀏覽器或其它瀏覽工具能夠從緩存中取得上一次獲得的結果。所以,在一個基於HTTP的REST系統中,如何準確地使用HTTP協議是一項很是重要的內容。

  在獲知瞭如何對響應所攜帶的負載進行解析以後,咱們就來看看資源的自描述性。在上面的示例中,服務端響應使用了JSON表示了食品分類。該表示首先經過label屬性描述了本身是一個什麼分類。接下來,其經過brands屬性表示了該分類中的著名品牌,並經過hot_searches標示了在該分類中的熱搜關鍵字。能夠看到,該負載中的全部屬性都清晰地描述了自身所表達的含義。

  那在該資源表示中的url屬性是什麼意思?實際上這是爲子約束「每一個資源都擁有一個資源標識」所添加的一個屬性。該子約束要求每一個資源的資源標識能夠用來惟一地標明該資源。對於網絡應用來講,資源標識就是URI。而在一個基於HTTP的系統中,最天然的資源標示即是URL。在表示單個資源的時候,這個URL經常會包含着資源在該類資源中的ID。

  在本文的其它章節中,咱們就將以這種方式來區分URL和ID:URL用來指向資源所在的地址,而ID則表示該資源在該類型資源中的ID。請讀者必定要記得這兩個術語所對應的不一樣意義,以防止理解錯誤。

  如今還有一部分食品分類表示中的屬性沒有被講解,那就是在該表示中的各個URL。這是爲子約束HATEOAS服務的。在用戶看到items_url屬性時,其就能夠經過向該URL發送GET消息獲得屬於食品分類中的全部商品的列表。而在商品品牌的表示中也擁有一個url屬性。也就是說,向該URL發送一個GET請求也可以獲得相應品牌的詳細信息。

  您可能會問:既然在介紹HATEOAS時說REST服務並不須要文檔來告訴用戶哪裏擁有什麼樣的資源,那用戶應該如何知道向/api/categories發送GET請求就能獲得全部的分類呢?標準的作法則是向/api直接發送一個GET請求:

1 GET /api
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  而在返回的響應中將標示出REST API的版本以及全部能夠訪問的資源等信息:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4 
 5 {
 6    "version": "1.0",
 7    "resources": [
 8       {
 9          "label" : "Categories",
10          "description" : "Product categories",
11          "uri": "/api/categories"
12       }, {
13          "label" : "Items",
14          "description" : "All items on sell",
15          "uri": "/api/items"
16       }
17    ]
18 }

  能夠看到,在該響應中列出了能夠被訪問的兩種資源:表示商品分類的Categories以及表示商品的Items。在須要訪問特定類型的資源時,軟件開發人員能夠經過直接向這兩種資源所對應的URI發送GET請求便可。

  OK,相信如今讀者已經瞭解了REST服務所提供的各類約束。那麼在後面的章節中,咱們將會逐步講解如何設計一個基於HTTP的REST服務。

 

資源識別

  在通常狀況下,對資源的識別一般都是REST服務設計的第一步。在準確地識別出了各資源以後,怎麼用HTTP規範中的各組成來表示這些資源即是瓜熟蒂落的事情。在本節中,咱們將對如何識別REST系統中的資源進行講解。

  在一般的軟件開發過程當中,咱們經常須要分析達成某個目標所須要使用的業務邏輯,併爲業務邏輯的執行提供一系列運行接口。在一些Web服務中,這些接口經常表達了某個動做,如將商品放入購物車,提交訂單等。這一系列動做組合在一塊兒就能夠組成完成目標所須要執行的業務邏輯。在須要調用這些接口的時候,軟件開發人員須要向這些接口所在的URL發送一個請求,從而驅使服務執行該動做。

  而在REST服務中,咱們所提供的各個接口則須要是一系列資源,而業務邏輯須要經過對資源的操做來完成。也就是說,REST服務中的API將再也不以執行了什麼動做爲中心,而是以資源爲中心。一些對資源的通用操做有添加,取得,修改,刪除,以及對符合特定條件的資源進行列表操做。

  仍然讓咱們以上面所舉的「將商品放入購物車」這個操做爲例。在一個REST系統中,購物車將被抽象爲一個資源,而「將商品放入購物車」這個操做將被解釋爲對購物車這個資源的更新:更新購物車,以使特定商品包含在購物車內。

  可能對於剛剛學習REST的各位讀者而言,這種以資源爲中心的描述方法有些彆扭。這種描述方法的確有別於不少Web服務那樣以動做爲中心。而與之對應的則是系統設計步驟的改變:咱們將再也不首先是別完成業務邏輯所需的各動做,而是支持業務邏輯所須要的各資源。那麼咱們應該如何抽象出這些資源呢?首先,咱們對某個操做不要再關注它所執行的動做,而是關心它所操做的賓語。一般狀況下,該賓語就會是REST系統中的資源。

  在這裏,咱們就以「提交訂單」做爲示例來展現如何抽象資源。

  首先,在「提交訂單」這個動做中,訂單是賓語。所以對於該業務邏輯,其將做爲一個資源存在。除此以外,在訂單中還須要包含一系列信息,例如訂單中所包含的商品,訂單所屬人等。一旦這些均可以被該REST系統中的其它資源使用,那麼它們也將成爲獨立的資源。

  可是有時候,一個動做可能並不存在着它所操做的賓語。在這種狀況下,咱們就須要考慮該動做產生或消除了哪一個實體,或者哪一個實體的狀態發生了變化。這個發生了變化的實體實際上就是一種資源。例如對於登錄這一行爲,其實際上在服務端建立了一個會話實例。該會話實例中則包含了登錄IP,登錄時間,以及登錄時所用的憑證等。再好比對於用戶更改密碼這種行爲,其所操做的資源就是用戶資料。

  在抽象資源的過程當中,咱們須要按照自頂向下的方式,即首先辨識出系統中的最主要資源,而後再辨識這些主要資源的子資源,並依次進行迭代。

  對主資源的抽取主要經過分析業務邏輯來完成。在獲得功能需求之後,咱們首先要分析這些業務邏輯所操做的賓語。這些賓語可能有兩種狀況:主資源或者其它資源的子資源。主資源實際上就是可以獨立存在的一系列資源。而子資源則須要依附於主資源之上才能表達實際的意義。同時各個子資源也可能擁有自身的子資源。

  判斷一個資源是不是子資源的一個方法就是看它是否能獨立地表示其具體含義。例如對於一個egoods上所銷售的商品,其名稱,價格,簡介等屬性能夠清晰地描述該商品究竟是什麼,到底如何銷售。所以這些商品其實是一個主資源。可是每種商品所支持的郵遞服務須要是一個子資源:一個商品能夠支持多種郵遞服務。這些郵遞服務根據派送距離等須要不一樣的價格,也提供了不一樣的郵遞速度。因爲這些郵遞服務與商家和郵遞服務公司所達成的服務價格有關,而且會因爲商品重量的變化而變化,所以這些郵遞服務並不能爲其它商家所提供的郵遞服務做爲參考,所以其應該做爲該商品的一個子資源。

  或者也能夠說,若是一個資源是主資源,那麼其能夠被不一樣的資源實例包含引用而不會產生歧義。而若是一個資源是子資源,那麼被不一樣的資源實例引用可能會產生歧義。

  可是須要注意的是,一種資源可能有多種不一樣的表現形式。例如對於在使用列表展現各個商品的時候,egoods只須要展現商品的名稱,一個對該商品的簡單描述,商品的價格以及一張商品的照片。而在用戶打開了該商品頁以後,頁面則須要顯示更詳盡的信息,如商品的重量,商品所在地等等。

  除此以外,資源列表也有可能擁有多種不一樣的表現形式。舉例來講,若是egoods上屬於某個分類的商品太多,須要分頁顯示,那麼這種分頁是否也應該是一種資源?答案是,這些分頁並非一種資源,而其只是資源列表的一種表現方式。在每頁所包含商品數量,排序規則等條件發生變化的時候,該資源列表中所包含的各個商品也會發生變化。

  那麼如何判斷咱們爲REST服務所定義的資源是否合理呢?通常狀況下,我都使用下面的一些判斷方法:

  首先,咱們須要考慮對該資源的CRUD是否有意義,從而驗證資源的定義是否合理。就以剛剛說到的列表的分頁顯示爲例,咱們能夠想象一下如何對分頁進行添加和刪除?一旦刪除了該分頁,那麼屬於該分頁中的各個商品也應該被刪除麼?並且刪除了分頁X的數據後,本來X + 1分頁的數據將展現在X分頁中。很顯然,將商品的分頁定義爲資源並不合理。

  其次,咱們須要檢查資源是否須要除CRUD以外的動詞來操做。該方法用來檢查資源中是否還有子資源沒有被抽象。若是該資源還須要額外的動詞,那麼咱們就須要考慮這些操做到底引發了什麼樣的狀態變化,進而抽象出該資源的子資源。

  除此以外,咱們還須要檢查這些資源是不是被總體使用,建立和刪除。該方法用來探測是否一個子資源應該是一個主資源。若是在刪除一個資源的時候,其子資源還能夠被其它資源重用,那麼該子資源實際上具備較高的重用性,應該是一個主資源。

 

資源的URL設計

  在前面已經提到過,統一接口約束中的第一條子約束就是每一個資源都擁有一個資源標識。在正確地辨識出了一個資源以後,咱們就須要爲這些資源分配其所對應的URI。一個資源所對應的URI可能有多種表示方式,如究竟是用單數仍是複數表示資源等。所以在一個基於HTTP的REST系統中,如何組織針對各個資源的URL其實是最重要的一部分。畢竟一個明確的,有意義而且穩定的API接口其實是對服務對用戶的一種承諾。

  在HTTP中,一個URL主要由如下幾個部分組成:

  1. 協議。即HTTP以及HTTPS。
  2. 主機名和端口。如www.egoods.com:8421
  3. 資源的相對路徑。如/api/categories。
  4. 請求參數。即由問號開始的由鍵值對組成的字符串:?page=1&page_size=20

  在爲一個資源設計其所對應的URL時,咱們須要着重考慮第三部分和第四部分組成。

 

經過URL來表示資源

  在辨識出了REST系統中的各個資源之後,咱們就須要開始爲這些資源設計各自所對應的URL了。

  首先要介紹的是,全部的資源都應該存在於一個相對路徑之下。請讀者回憶以前咱們介紹的經過向/api發送一個GET請求獲得全部能夠被訪問的資源這個示例:

 1 GET /api
 2 Host: www.egoods.com
 3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
 4 Accept: application/json
 5 
 6 HTTP/1.1 200 OK
 7 Content-Type: application/json
 8 Content-Length: xxx
 9 
10 {
11    "version": "1.0",
12    "resources": [
13       {
14          "label" : "Categories",
15          "description" : "Product categories",
16          "uri": "/api/categories"
17       }, {
18          "label" : "Items",
19          "description" : "All items on sell",
20          "uri": "/api/items"
21       }
22    ]
23 }
 

  所以對於從向該相對路徑發送請求才能獲得的各個主資源來講,將它們置於相對路徑/api之下是很是合理的。

  除了這個緣由以外,API的版本更迭也是一個考慮。假如軟件開發人員須要開發一個新版本的REST API,那麼他可能就須要從新抽象並定義系統中的各個資源。可是若是兩個版本的API中都擁有一個categories資源,而且系統爲了保持後向兼容性同時保留了兩個版本的API,那麼將只有一個資源可使用/categories這個相對路徑。也正由於如此,將這些資源置於相對路徑/api之下,並在第二個版本的API出現以後將新的資源抽象置於/api-v2下是一種較爲流行的作法。

  在明確了全部的資源都應該置於/api這樣一個相對路徑下以後,咱們就來說解如何爲資源定義對應的URL。一個最簡單的狀況是:指定主資源所對應的URL。因爲主資源是一類獨立的資源,所以它應該直接置於/api下。例如egoods網站中的產品分類就是一個主資源,咱們會爲其分配以下URL:

1 /api/categories

  而對於其它主資源,如egoods網站中的產品,咱們也會爲其賦予一個具備相似結構的URL:

1 /api/items

  這樣,每類主資源都將擁有一個特定於該類資源的URL。這些URL就對應着相應資源實例的集合。

  若是須要表示某個主資源類型中的特定實例,那麼咱們就須要在該類主資源所對應的URL以後添加該實例的ID。如egoods網站中的食品分類的ID爲1,那麼其所對應的URL就將是:

1 /api/categories/1

  一個較爲特殊的狀況則是,對於某種類型的主資源,整個系統將有且僅有一個該類型資源的實例。那麼該資源將再也不須要經過ID來訪問。我能想到的一個例子就是對整個系統進行介紹的資源。該資源實例所對應的URL將是:

1 /api/about

  而一個資源實例中還可能擁有子資源。這些子資源與資源實例之間的關係主要有兩種狀況:資源實例包含了一個子資源的集合,以及資源實例僅僅能夠包含一個子資源。對於資源實例包含了一個子資源集合的狀況,咱們須要將該子資源集合的URL置於該資源的相對路徑下。例如對於egoods上所銷售的ID爲23456的商品所提供的郵遞服務,咱們將使用以下的URL:

1 /api/items/23456/shipments

  在該URI中,/api/items/23456對應的就是商品自己,而該商品所提供的郵遞服務則是該商品的子資源。與主資源特定實例所具備的URI相似,其中一個ID爲87256的郵遞服務所對應的URI則爲:

1 /api/items/23456/shipments/87256

  若是資源實例僅僅能夠包含一個子資源,那麼對該子資源的訪問也將再也不須要ID。如當前商品的折扣信息:

1 /api/items/23456/discount

 

單數 vs. 複數

  接下來要考慮的一點是,資源在URL中須要由單數表示仍是複數表示?這在stackoverflow等衆多論壇上已經成爲了一個經久不衰的話題。咱們知道,在一個基於HTTP的REST系統中,一個資源所對應的URL實際上也就是對其進行操做的URL。所以適當地使用單數和複數對於該系統的用戶而言有必定的指示做用。在stackoverflow上的一個常見觀點是:若是一個URL所對應的資源是使用複數表示的,那麼該類型的資源可能有多個。對該URL發送Get請求可能返回該資源的一個列表。反之,若是一個URL所對應的資源是使用單數表示的,那麼該類型的資源將只有一個,所以對該URL發送Get請求將只返回該資源的一個實例。

  以egoods中的商品分類爲例。因爲一個網站所售賣的商品可能有多種類別,所以其須要在URL中使用複數形式:/api/categories。而對於一個該網站的用戶而言,因爲其只會有一個我的偏好設置,所以其URL則須要使用單數形式:/api/users/{user_id}/preference。

  你可能會問:若是須要獲得具備特定ID的某個實例時,咱們應該對該資源使用單數仍是複數呢?答案是複數。這是由於在經過特定ID訪問某個資源的實例實際上就是從該資源的集合中取出特定實例。所以表示該資源集合的URL實際上仍然須要使用複數形式,而其後所使用的ID則標明瞭其所訪問的是資源中的單一實例,所以向這個URL發送Get請求將返回該資源的單一實例。

  就以「食品」分類爲例。該分類所對應的URL爲/api/categories/1。該URL中的前半部分/api/categories表示egoods網站中全部分類的集合,而1則表示在該分類集合中的ID爲1的分類。

 

相對路徑 vs. 請求參數

  另外一個常常致使疑惑的地方就是針對資源的某一種特徵,咱們究竟是將其定義爲URL中相對路徑的一部分仍是做爲請求參數。

  請考慮下面一個例子。在egoods網站中,咱們售賣的手機主要有蘋果,三星等品牌。那麼在爲這些手機設計URL的時候,咱們是否須要按照品牌對這些手機進行細分,從而用戶只要經過向/api/mobiles/brands/apple發送請求就能列出全部的蘋果手機?仍是說,直接將手機的品牌置於請求參數中,從而經過/api/mobiles?brand=apple來列出全部的蘋果手機?

  在判斷究竟是使用請求參數仍是相對路徑時,咱們通常分爲下面幾步。

  首先,可選參數通常都應置於請求參數中。仍以egoods中的手機爲例。在選擇手機時,用戶能夠選擇品牌以及顏色。若是將品牌和顏色都定義在相對URL中,那麼具備特定品牌和顏色的手機將能夠經過兩個不一樣的URL訪問:/api/mobiles/brand/{brand}/color/{color}以及/api/mobiles/color/{color}/brand/{brand}。就用戶而言,其並沒有法瞭解這兩個URL所表示的是同一類資源仍是不一樣類型的資源。固然,您能夠說,咱們只用/api/mobiles/brand/{brand}/color/{color}。可是該URL將沒法處理用戶僅僅選擇了顏色,卻沒有選擇品牌的狀況。

  其次,不是全部字符均可以在URL中被使用,如漢字,標點。爲了處理這種狀況,包含這些字符的篩選條件須要置於請求參數中。

  最後,若是該特徵下包含子資源,那麼它自身也就是一個資源,所以須要以相對路徑的方式展示它。例如在egoods網站中,每件商品所屬於的分類僅僅是它的一個特徵。可是一個分類更包含了屬於它的各個品牌以及熱搜關鍵字等衆多信息。所以它實際上是一個資源,須要在URI路徑中表示它。

  總的來講,既然使用HTTP來構建REST系統,那麼咱們就須要遵照URL各組成中的含義:URL中的相對路徑將用來標示「What I want」,也既對應着資源;而請求參數則用來標示「How I want」,即查看資源的方式。

 

使用合適的動詞

  在知道了如何爲每種資源定義URI以後,咱們來看看如何操做這些資源。

  首先,在一個資源的生命週期以內經常會發生一系列通用事件(CRUD)。一開始,一個資源並不存在。只有用戶或REST服務建立了該資源之後其才存在,也便是上面所列出的通用事件中的C,Create。在一個資源建立完畢之後,用戶可能會從服務端請求該資源的表示,也就是上面所列出的通用事件的R,Retrieve。在特定狀況下,用戶可能決定要更新該資源,所以會使用上面的通用事件中的U,即Update來更新資源。而在資源再也不須要的時候,用戶可能須要經過通用事件D,即Delete來刪除該資源。同時用戶有時也須要列出屬於特定類型資源的資源實例,即經過List操做來獲得屬於特定類型的資源的列表。

  在前面的講解中咱們已經提到過,在REST系統中的每一個資源都有一個特定的URI與之對應。HTTP協議提供了多種在URI上操做的動詞,如GET,PUT,POST以及DELETE等。所以在一個基於HTTP的REST服務中,咱們須要使用這些HTTP動詞來表示如何對這些資源進行CRUD操做。而在什麼狀況下到底使用哪一個動詞則是由這些動詞自己在HTTP協議中的意義所決定的。

  這其中GET和DELETE兩個動詞的含義較爲清晰:

 

The GET method means retrieve whatever information (in the form of an entity) is identified by the Request-URI.

The DELETE method requests that the origin server delete the resource identified by the Request-URI.

 

  也就是說,在須要讀取某個資源的時候,咱們向該資源所對應的URI發送一個GET請求便可。相似的,在須要刪除一個資源的時候,咱們只須要向該資源所對應的URI發送一個DELETE請求便可。而在但願獲得某類型資源的列表的時候,咱們能夠直接向該類型資源所對應的URI發送一個GET請求。

  而動詞PUT和POST則是較爲容易混淆的兩個動詞。在HTTP規範中,POST的定義以下所示:

 

  The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line

 

  也就是說,POST動詞會在目標URI之下建立一個新的子資源。例如在向服務端發送下面的請求時,REST系統將建立一個新的分類:

1 POST /api/categories
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json
5 
6 {
7    "label" : "Electronics",
8    ……
9 }

  

  而PUT的定義則更爲晦澀一些:

 

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI."

 

  也就是說,PUT則是根據請求建立或修改特定位置的資源。此時向服務端發送的請求的目標URI須要包含所處理資源的ID:

1 POST /api/categories/8fa866a1-735a-4a56-b69c-d7e79896015e
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json
5 
6 {
7    "label" : "Electronics",
8    ……
9 }

  能夠看到,二者都有建立的含義,可是意義卻不一樣。在決定究竟是使用PUT仍是POST來建立資源的時候,軟件開發人員須要考慮一系列問題:

  首先就是資源的ID是如何生成的。若是但願客戶端在建立資源的時候顯式地指定該資源的ID,那麼就須要使用PUT。而在由服務端爲該資源自動賦予ID的時候,咱們就須要在建立資源時使用POST。在決定使用PUT建立資源的時候,防止資源URI與其它資源所具備的URI重複的任務須要由客戶端來保證。在這種狀況下,客戶端經常使用GUID/UUID做爲將資源的ID。可是到底使用GUID/UUID仍是由服務端來生成ID不只僅和REST有關,更會對數據庫性能等多個方面產生影響。所以在決定使用它們以前要仔細地考慮清楚。

  同時須要注意的是,由於REST要求客戶只能夠經過服務端返回結果中所包含的信息來獲得下一步操做所須要的信息,所以客戶端僅僅能夠決定資源的ID,而URI中的其它部分則須要從以前獲得的響應中取得。

  可是軟件開發人員經常會進入另一個誤區不少人認爲REST服務中的HATEOAS只能經過Hyperlink完成。實際上在Roy對REST的定義中使用的是Hypermedia,即響應中的全部多媒體信息。就像Roy在其我的網站上所說(http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven):

 

A REST API must not define fixed resource names or hierarchies (an obvious coupling of client and server). Servers must have the freedom to control their own namespace. Instead, allow servers to instruct clients on how to construct appropriate URIs, such as is done in HTML forms and URI templates, by defining those instructions within media types and link relations.

 

  另一個須要考慮的因素則是PUT的等冪性是否對REST系統的設計有所幫助。因爲在同一個URI上調用兩次PUT所獲得的結果相同。所以用戶在沒有接到PUT請求響應時能夠放心地重複發送該響應。這在網絡丟包較爲嚴重時是一個很是好的功能。反過來,在同一個URI上調用兩次POST將可能建立兩個獨立的子資源。

  除此以外,還須要考慮是否將資源的建立和更新歸結爲一個API能夠簡化用戶對REST服務的使用。用戶能夠經過PUT動詞來同時完成建立和更新一個資源這兩種不一樣的任務。這樣的好處在於簡化了REST服務所提供的接口,可是反過來也讓一個API執行了兩種不一樣的任務,在必定程度上違反了API設計時每一個API都須要有明確的意義這一原則。

  所以在決定到底使用POST仍是PUT來完成資源的建立以前,請考慮上面所列出的三條問題,以肯定到底哪一個動詞更加適合。

  除此以外,另一對相似的動詞則是PUT和PATCH。二者之間的不一樣則在於PUT是對整個資源的更新,而PATCH則是對部分資源的更新。而該動詞的侷限性則在於對該動詞的支持程度。畢竟在某些類庫中並無提供原生的對PATCH動詞的支持。

 

使用標準的狀態碼

  在與REST服務進行交互的時候,用戶須要經過服務所返回的信息決定其所發送的請求是否被適當地處理。這部分功能是由REST服務實現時所使用的協議所決定的,與REST架構無關。而在基於HTTP的REST服務中,該功能就由HTTP響應的狀態碼(Status Code)來完成。所以在設計一個REST服務時,咱們須要額外地注意是否返回了正確的狀態碼。

  可是這些預約義的HTTP狀態碼並不能知足全部的狀況。有時候一個REST服務所但願返回的錯誤信息可以更加精確地描述問題,例如在用戶重設密碼時,咱們須要在用戶所輸入原密碼與系統中所記錄的密碼不匹配時返回「您所輸入的密碼有誤」這樣的消息。在HTTP協議中,咱們並無辦法找到一個可以精確地表示該意義的狀態碼。

  所以在一般狀況下,REST服務都會在響應中額外地提供一個說明性的負載來告知用戶到底產生了什麼問題。例如對於上面的重設密碼失敗的狀況,服務端可能會返回以下響應:

1 HTTP/1.1 400 Bad Request
2 Content-Type: application/json
3 Content-Length: xxx
4 
5 {
6    "error_id" : "100045",
7    "header" : "Reset password failed",
8    "description" : "The original password is not correct"
9 }

  上面的示例響應中主要包含如下的說明性信息:

  1. 服務端響應的狀態碼。頁面邏輯能夠經過判斷該狀態碼是不是4XX或5XX來判斷是否請求出錯,從而在頁面中展現一個警告對話框。
  2. 服務所提供的內部錯誤ID。一般狀況下,該內部錯誤ID也須要在警告對話框中展現出來。從而容許軟件用戶根據內部錯誤ID來獲取支持服務。
  3. 錯誤的標題及簡述。經過該錯誤的標題及簡述,軟件用戶可以瞭解系統內部到底發生了什麼,並在是用戶輸入錯誤的時候容許用戶自行修改錯誤並從新發送正確的請求。

  在該錯誤中,最關鍵的當屬服務端的響應代碼。一個響應代碼不只僅標示了請求是否成功,更有用戶該如何操做的含義。例如對於401 Unauthorized響應代碼而言,其表示該響應沒有提供一個合法的身份憑證,所以須要用戶首先執行登錄操做以獲得一個合法的身份憑證,而後該資源可能就能夠被訪問了。而403 Forbidden響應代碼則表示當前請求已經提供了一個合法的身份憑證,可是該身份憑證並無訪問該資源的權限,所以使用該身份憑證登錄從新登錄系統等操做並不能解決問題。

  所以在返回錯誤信息以前,軟件開發人員首先須要考慮清楚在響應中到底應該使用什麼樣的響應代碼。而正確地選擇響應代碼則創建在軟件開發人員對這些響應代碼擁有一個正確的理解的前提下。

  固然,要將全部的響應代碼徹底理解也須要大量的工做,並且REST服務的用戶也可能並無那麼多的領域知識來了解全部的響應代碼的含義。所以在不少基於HTTP的REST系統中,系統在標示錯誤時只使用一系列經常使用的響應代碼,如400,401,403,404,405,500,503等。在用戶請求被處理時,系統將返回200 OK,表示請求已經被處理。而在處理時發生錯誤時則儘可能使用這些響應代碼來表示。若是一個錯誤較爲複雜,那麼直接返回400或500,並在響應的負載中提供具體的錯誤信息。

  不得不說的是,這種作法有時顯得簡單粗暴,尤爲是對於一個開放平臺而言則更是致命的。當一個第三方廠商爲一個開放平臺開發一個應用軟件,卻每次只能獲得一個400錯誤,那麼其內部應用邏輯將沒法判斷究竟是哪裏出了問題。爲了能讓用戶知道這裏產生了錯誤,該第三方軟件只能將開放平臺所給出的信息直接顯示給用戶。可是這些信息其實是創建在開放平臺這個語境下的,所以對於第三方廠商的用戶而言,這些信息晦澀難懂,甚至可能一點幫助也沒有。

  也就是說,到底如何組織這些響應代碼須要用戶根據所編寫的項目決定,尤爲是該產品的使用者來決定。在定義一個平臺時,儘可能使用更多的HTTP響應代碼,由於用戶極有可能經過該平臺編寫本身的第三方軟件。而在爲一個普通的產品定義REST API時,將響應代碼定得很是專業可能反而致使易用性的降低。

  另一點須要說明的是,我的不建議使用Wikipedia查找各個狀態碼的含義,而應該使用RFC所描述的各狀態碼的定義。 IANA提供了一張各個狀態碼所對應的RFC協議的列表,從而能夠很容易地找到各個狀態碼所對應的RFC協議以及其所在的章節。該列表的地址爲:http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

  之因此不建議使用Wikipedia的緣由主要有兩點:

  1. 描述不夠詳細。在RFC定義中,每一個狀態碼都對應着一段或多段文字,而且解釋很是清晰。而在Wikipedia中,每一個狀態碼經常只有一句話。
  2. 不夠準確。在Wikipedia的Reference節中,咱們能夠看到一系列特定平臺所定義的狀態碼,如Spring Framework所定義的420 Method Failure等。這很是具備誤導性。

 

選擇適當的表示結構

  接下來咱們要講解的就是如何爲資源定義一個恰當的表示。

  首先須要強調的是,REST並無規定其服務中須要使用什麼格式來表示資源。表示資源時所能夠選取的表示形式其實是由實現REST所使用的協議決定的。而在一個基於HTTP的REST服務中,咱們可使用JSON,也可使用XML,甚至是自定義的MIME類型來表示資源。這些表現形式經常是等效的。相信讀者已經看到,本系列文章會使用JSON來表示這些資源。

  一個REST服務經常會同時支持多種客戶端。這些客戶端可能會使用不一樣的協議來與服務進行溝通。並且就算是使用相同的協議,不一樣的客戶端所能夠接受的負載表示形式也會有所不一樣。所以客戶端須要與REST服務協商在通信過程當中所使用的負載。

  客戶端和服務端對所使用負載類型的協商一般都按照協議所規定的標準協商過程來完成。例如對於一個基於HTTP的REST服務,咱們就須要使用Accept頭來標示客戶端所能夠接受的負載類型:

1 GET /api/categories
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/json

  而在服務端支持的狀況下,返回的響應就將使用該MIME類型組織其負載:

1 HTTP/1.1 200 OK
2 Content-Type: application/json
3 Content-Length: xxx

  在這裏咱們再重複一次:REST是一種組織Web服務的架構,其只在架構方面提出了一系列約束。能夠說,全部對REST的講解都已經在前兩個章節,即「REST的定義」以及「資源識別」中完成了。而有關客戶端和服務端如何進行溝通,爲資源定義什麼樣的URI,使用什麼格式的數據進行溝通等討論都是在闡述如何將REST架構所提出的各類約束和基於HTTP協議的Web服務結合在一塊兒。畢竟在一般狀況下,實現一個單純的技術不難,可是如何將多種技術規範天然地混合在一塊兒,構成一個天然的,成熟穩定的解決方案纔是項目開發中的難點。HTTP協議並非爲REST架構所定義的,所以如何用HTTP協議來恰當地描述一個REST服務纔是本文所着重介紹的。

 

負載的自描述性

  在前面對REST提出的幾個約束的講解中咱們已經提到過,REST系統中所傳遞的各個消息的負載須要提供足夠的用於操做該資源的信息,如如何對資源進行添加,刪除以及修改等操做,並能夠根據負載中所包含的對其它各資源的引用來訪問各個資源。這也對負載的自描述性提出了更高的要求。

  首先讓咱們回頭看看egoods電子商務網站對食品分類的描述:

 1 {
 2    "uri" : "/api/categories/1",
 3    "label" : "Food",
 4    "items_url" : "/api/items?category=1",
 5    "brands" : [
 6          {
 7             "label" : "友臣",
 8             "brand_key" : "32073",
 9             "url" : "/api/brands/32073"
10          }, {
11             "label" : "樂事",
12             "brand_key" : "56632",
13             "url" : "/api/brands/56632"
14          }
15          ...
16    ],
17    "hot_searches" : …
18 }

  我想讀者在看到該響應以後可能就已經明白了不少域的含義。但仍是讓咱們依次對這些域進行講解。

  第一個要講解的是url域。該域用來標示該資源所對應的URL。可能您會問:既然咱們就是從這個URL返回的該資源,那麼爲何咱們還須要在該資源中保存一個它所對應的URL呢?首先這是由於在統一接口約束中要求每一個資源都擁有一個資源標識。在這裏咱們使用URL做爲標識。而另外一些基於HTTP的REST系統中,用來做爲資源標識的經常是該資源的ID。我的更傾向於使用URL的緣由則是:在某些狀況下,如對某個資源定時刷新以進行監控的時候,URL能夠直接被使用。

  接下來是label域。其用來記錄用於展現給用戶的分類名。

  items_url域則用來表示取得屬於該分類物品列表的URL。注意這裏我使用了後綴_url以明確標明其是一個URL,須要經過跳轉來取得實際的數據。

  下一個域brands則用來表示屬於該分類的著名商品品牌。這裏咱們使用了一個數組,而數組中的每一個元素都表示了一個品牌。每一個品牌的表示都包含了一個展現給用戶的label,在搜索時所使用的鍵,以及該品牌所對應的url。您可能會懷疑爲何咱們僅僅提供了這麼少的域。這是由於他們僅僅是對這個品牌的引用,而並不是是把該資源的詳細信息都包含進來了的緣故。在用戶但願查看該品牌的詳細信息的時候,他須要向該品牌引用中所標明的品牌的URL發送一個GET請求。

  而因爲hot_searches域的組成及使用基本上與brands域相似,所以這裏再也不贅述。

  在大體地瞭解了食品分類的JSON表示中各個域的含義後,咱們就將開始講解如何自行定義資源的JSON表示。對於一個簡單的,不包含任何子資源以及對其它資源的引用的資源,咱們只須要經過一個包含簡單屬性的JSON來表示它。例如對於一個品牌,咱們可能僅僅提供了一系列描述性信息:品牌的名稱,以及對品牌的簡單描述。那麼它所對應的JSON表示能夠表示爲:

1 {
2    "uri" : "/api/brands/32059",
3    "label" : "Dole",
4    "description" : "An American-based agricultural multinational corporation."
5 }

  而在另外一個資源中,可能包含了對其它資源的引用。在這種狀況下,咱們就須要在表示對其它資源進行引用的域中經過URL來標明被引用資源的位置。例如一件Dole果汁中,可能就須要包含對品牌Dole的引用:

 1 {
 2    "uri" : "/api/items/1438299",
 3    "label" : "Dole Grape Juice",
 4    "price" : "$3.99",
 5    "brand" : {
 6       "label" : "Dole"
 7       "uri" : "/api/brands/32059"
 8    }
 9    ……
10 }

  在上面的Dole果汁的表示中,咱們能夠看到它的brand域就是對品牌的引用。該引用中包含了該品牌的品牌名稱以及一個指向該品牌的URL。

  在一個基於HTTP的REST系統中,咱們經常在資源的引用中包含必定量的描述信息。這主要由於兩點:

  1. 提升性能。在一個對資源的引用中添加了用於顯示的屬性後,客戶端頁面能夠避免再次經過url發送請求獲得資源的具體描述,以獲得用於顯示的信息。
  2. 自描述性的要求。若是一個資源中包含了一個對其它資源進行引用的數組,那麼用戶就須要經過該標籤來決定到底訪問哪一個被引用的資源。

  固然,若是須要在展現Dole果汁的頁面中須要Dole這個品牌的完整信息,咱們也能夠將它直接嵌到Dole果汁的表示中:

 1 {
 2    "uri" : "/api/items/1438299",
 3    "label" : "Dole Grape Juice",
 4    "price" : "$3.99",
 5    "brand" : {
 6       "uri" : "/api/brands/32059",
 7       "label" : "Dole",
 8       "description" : "An American-based agricultural multinational corporation."
 9    }
10    ……
11 }

  固然,若是一個資源的表示太過複雜,並且有些屬性其實是相互關聯的,那麼咱們也能夠經過一個屬性將它們歸結在一塊兒:

 1 {
 2    "uri" : "/api/items/1438299",
 3    "label" : "Dole Grape Juice",
 4    "price" : "$3.99",
 5    "brand" : {
 6       "uri" : "/api/brands/32059",
 7       "label" : "Dole",
 8       "description" : "An American-based agricultural multinational corporation."
 9    }
10    "nutrient component" : {
11       "sugar" : "14.5",
12       "protein" : "0.3",
13       "fat" : "0.1"
14    }
15    ……
16 }

  在上面的Dole果汁的表示中,咱們使用域nutrient component來表示全部的養分成分,而該域內部的各個子域則用來表示一系列相關的養分成分所佔比例。

  另外,在不一樣的狀況下,咱們還可能對同一個資源提供不一樣的表現形式。例如在一個資源極爲複雜,其JSON表示甚至能夠達到幾百K的時候,咱們能夠爲該資源提供一個簡化版本,以在非必要的狀況下減小傳輸的數據量。

  例如在egoods中,咱們會將某些物美價廉的商品置於它的首頁上,以吸引用戶購買。在用戶將鼠標移動到某個商品上並停留一段時間時,咱們會爲用戶展現一個Tooltip,並在該Tooltip中展現該商品的一部分信息。在這種狀況下,向服務端請求該商品的全部信息以展現Tooltip便顯得有些效率低下了。

  有時候,一個資源可能並不支持特定用戶執行某個操做。例如一個管理員所建立的資源可能對普通用戶只讀。在這種狀況下,咱們須要禁止普通用戶對該資源的修改和刪除。爲了能明確地告知用戶他所具備的權限,咱們須要一個能顯式地標示用戶能夠在一個資源上所執行操做的組成。在REST響應中,這種組成被稱爲Hypermedia Controls。例如對於一個普通用戶,其從egoods中所返回的分類列表將以下所示:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4  
 5 [
 6    {
 7       "label" : "Food",
 8       "uri" : "/api/categories/1",
 9       "actions" : ["GET"]
10    }, {
11       "label" : "Clothes",
12       "uri" : "/api/categories/2",
13       "actions" : ["GET"]
14    }
15    ...
16    {
17       "label" : "Electronics",
18       "uri" : "/api/categories/25",
19       "actions" : ["GET"]
20    }
21 ]

  能夠看到,在上面的分類列表中,咱們經過actions域顯式地標示了用戶能夠在各個類別上所能執行的操做。而對於管理員,其還能夠執行修改,刪除等操做:

 1 HTTP/1.1 200 OK
 2 Content-Type: application/json
 3 Content-Length: xxx
 4  
 5 [
 6    {
 7       "label" : "Food",
 8       "uri" : "/api/categories/1",
 9       "actions" : ["GET", "PUT", "DELETE"]
10    }, {
11       "label" : "Clothes",
12       "uri" : "/api/categories/2",
13       "actions" : ["GET", "PUT", "DELETE"]
14    }
15    ...
16    {
17       "label" : "Electronics",
18       "uri" : "/api/categories/25",
19       "actions" : ["GET", "PUT", "DELETE"]
20    }
21 ]

  而在一系列較爲著名的REST系統中,如Sun Cloud API,其更是經過Hypermedia Controls定義了除CRUD以外的動詞。如對於一個虛擬機,其在運行狀態下能夠執行中止命令,而在中止狀態下能夠執行啓動命令:

 1 {
 2    "vms" : [
 3       {
 4          "id" : "1",
 5          ......
 6          "status" : "stopped",
 7          "links" : [
 8             {
 9                "rel" : "start",
10                "method" : "post",
11                "uri" : "vms/1?op=start"
12             }
13          ]
14       }, {
15          "id" : "2",
16          ......
17          "status" : "started",
18          "links" : [
19             {
20                "rel" : "stop",
21                "method" : "post",
22                "uri" : "vms/2?op=stop"
23             }
24          ]
25       }
26    ]
27 }

  可是一個常見的觀點是:若是一個資源須要除CRUD以外的額外的動詞,那麼這種需求經常表示咱們對於某個資源的定義並非十分合理。所以在遇到這種狀況時,軟件開發人員首先須要考慮爲資源添加額外的動詞是否合適。

 

無狀態約束

  在Roy Fielding的論文中,其爲REST添加了一個無狀態約束:

 

We next add a constraint to the client-server interaction: communication must be stateless in nature … such that each request from client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. Session state is therefore kept entirely on the client.

 

  從上面的陳述中能夠看到,在一個REST系統中,用戶的狀態會隨着請求在客戶端和服務端之間來回傳遞。這也即是REST這個縮寫中ST(State Transfer)的來歷。

  爲REST系統添加這個約束有什麼好處呢?主要仍是基於集羣擴展性的考慮。若是REST服務中記錄了用戶相關的狀態,那麼在集羣中,這些用戶相關的狀態就須要及時地在集羣中的各個服務器之間同步。對用戶狀態的同步將會是一個很是棘手的問題:當一個用戶的相關狀態在一個服務器上發生了更改,那麼在何時,什麼狀況下對這些狀態進行同步?若是該狀態同步是同步進行的,那麼同時刷新多個服務器上的用戶狀態將致使對用戶請求的處理變得異常緩慢。若是該同步是異步的,那麼用戶在發送下一個請求時,其它服務器將可能因爲用戶狀態不一樣步的緣由沒法正確地處理用戶的請求。除此以外,若是集羣進行了不停機的橫向擴展,那麼用戶狀態的同步須要如何完成?這些實際上都是很是難以處理的問題。

  可是現有的不少較爲流行的技術及規範實際上都沒有限制用戶的請求是無狀態的。相信您知道,一個技術或規範實際上都擁有一個生態圈。在該生態圈以內的各技術之間能夠較好地契合在一塊兒。尤爲是,有些技術實際上就會以該生態圈中的核心技術或規範所創建的假設之上來實現本身的功能。若是但願禁止該假設,那麼讓某些技術工做起來就是很是困難的事情了。

  就以搭建基於HTTP的REST服務爲例。在HTTP中,一個重要的功能就是Cookie和Session的使用(RFC6265)。該功能會在服務器裏保留一個狀態。所以在一個基於HTTP的REST系統中,咱們經常須要避免使用這些在服務器裏面保留狀態的技術。可是某些技術,如用戶的登錄,實際上經常須要在服務器中添加一個狀態。

  因此在stackoverflow中,咱們經常會看到有人問:我如今使用了這樣一種解決方案。這樣實現是否是RESTful?此時一些人就會說,這不是RESTful。可是pure RESTful和almost RESTful之間的區別主要仍是在於一個是理論,一個是工程。在工程中,輕微地違反了一個準則並不必定表明這個解決方案一無可取。而是要看遵照該準則和輕微地違反了該準則以後工做量的大小以及後期的維護成本:之因此提出一系列準則,那是由於遵照該準則擁有必定的好處。若是對該準則的輕微違反能夠減小大量的工做量,並且遵照準則的好處並無消失,或者是經過另外一樣技術能夠快速地從新得到該好處,那麼對準則的輕微違反是值得的。

 

Authentication

  其實在上一節中,咱們已經提出了無狀態約束給REST實現帶來的麻煩:用戶的狀態是須要所有保存在客戶端的。當用戶須要執行某個操做的時候,其須要將全部的執行該請求所須要的信息添加到請求中。該請求將可能被REST服務集羣中的任意服務器處理,而不須要擔憂該服務器中是否存有用戶相關的狀態。

  可是在現有的各類基於HTTP的Web服務中,咱們經常使用會話來管理用戶狀態,至少是用戶的登錄狀態。所以,REST系統的無狀態約束實際上並非一個對傳統用戶登陸功能友好的約束:在傳統登錄過程當中,其自己就是經過用戶所提供的用戶名和密碼等在服務端建立一個用戶的登錄狀態,而REST的無狀態約束爲了橫向擴展性卻不想要這種狀態。而這也就是爲基於HTTP的REST服務添加身份驗證功能的困難之處。

  爲了解決該問題,最爲經典也最符合REST規範的實現是在每次發送請求的時候都將用戶的用戶名和密碼都發送給服務器。而服務器將根據請求中的用戶名和密碼調用登錄服務,以從該服務中獲得用戶所對應的Identity和其所具備的權限。接下來,在REST服務中根據用戶的權限來訪問資源。

  這裏有一個問題就是登錄的性能。隨着系統當前的加密算法愈來愈複雜,登錄已經再也不是一個輕量級的操做。所以用戶所發送的每次請求都要求一次登錄對於整個系統而言就是一個巨大的瓶頸。

  在當前,解決該問題的方法主要是一個獨立的緩存系統,如整個集羣惟一的登錄服務器。可是緩存系統自己所存儲的仍然是用戶的登錄狀態。所以該解決方案將仍然輕微地違反了REST的無狀態約束。

  還有一個相似的方法是經過添加一個代理來完成的。該代理會完成用戶的登錄並得到該用戶所擁有的權限。接下來,該代理會將與狀態有關的信息從請求中刪除,並添加用戶的權限信息。在通過了這種處理以後,這些請求就能夠轉發到其後的各個服務器上了。轉發目的地所在的服務器則會假設全部傳入的請求都是合法的並直接對這些請求進行處理。

  能夠看到,不管是一個獨立的登錄服務器仍是爲整個集羣添加一個代理,系統中都將有一個地方保留了用戶的登錄狀態。這實際上和在集羣中對會話集中進行管理並無什麼不一樣。也就是說,咱們所嘗試的經過禁止使用會話來達成徹底的無狀態並不現實。所以在一個基於HTTP的REST服務中,爲登錄功能使用集中管理的會話是合理的。

  既然咱們放鬆了對REST系統的無狀態約束,那麼一個REST系統所可使用的登錄機制將主要分爲如下兩種:

  1.   基於HTTPS的Basic Access Authentication

其好處是其易於實現,並且主流的瀏覽器都提供了對該功能的支持。可是因爲登錄窗口都是由瀏覽器所提供的,所以其與產品外觀有很大不一樣。除此以外,瀏覽器都沒有提供登出的功能,也沒有提供找回密碼等功能。

  2.   基於Cookie及Session的管理

在使用Cookie來管理用戶的註冊狀態的時候,其實際上就是將服務端所返回的Cookie在每次發送請求的時候添加到請求中。雖說這個Cookie並不是存儲了用戶應用的狀態,可是其實際存儲了用戶的登錄狀態。所以客戶端的角度來說,由服務端管理的Session並不符合REST所倡導的無狀態的要求。

  能夠說,上面的兩種方法各有優劣。可能第二種方法從客戶端的角度看來並非RESTful的,可是其優點則在於不少類庫都直接提供了對該功能的支持,從而簡化了會話管理服務器的實現。

  在這裏順便提一句,若是項目足夠大,將一些SSO產品集成到服務中也是不錯的選擇。

 

版本管理

  在前面已經提到過,一個REST系統爲資源所抽象出的URI其實是對用戶的一種承諾。但反過來講,軟件開發人員也很難預知一個資源的各方面特徵如何在將來發生變化,從而提供一個永遠不變的URI。

  在一個REST系統逐漸發展的過程當中,新的屬性,新的資源將逐漸被添加到該系統中。在這些更改過程當中,資源的URI,訪問資源的動詞,響應中的Status Code將不能發生變化。此時軟件開發人員所作的工做就是在現有系統上維護REST API的後向兼容性。

  當資源發生了過多的變化,原有的URI設計已經很難兼容現有資源應有的定義時,軟件開發人員就須要考慮是否應該提供一個新版本的REST API。那麼咱們該如何對資源的版本進行管理呢?

  首先要考慮的就是,新API的版本信息是否應當包含在資源的URI中。這在各著名論壇中仍然是一個爭議較大的話題。一種觀點認爲在不一樣版本的API中,一個資源擁有不一樣的地址在必定程度上違反了HATEOAS:URI只是用來指定一個資源所在的位置,而不是該資源如何被抽象。若是一個資源由不一樣的URI標示其不一樣的表現形式,那麼用戶將沒法經過一個響應中所標示的URI獲得其它URI所指向的表示形式。並且在URI中添加了有關版本的信息也就標示着其可能會隨着時間的推移發生變化。

  一種使用獨立URI的方法是基於Accept頭。在一個請求中,咱們經常標明瞭Accept頭,以標示客戶端但願獲得的表現形式。在該頭中,用戶能夠添加所請求的資源的版本信息:

1 GET /api/categories/1
2 Host: www.egoods.com
3 Authorization: Basic xxxxxxxxxxxxxxxxxxx
4 Accept: application/vnd.ambergarden.egoods-v3+json

  而在接收到該請求以後,服務端將返回該資源的第三個版本:

1 HTTP/1.1 200 OK
2 Content-Type: application/vnd.ambergarden.egoods-v3+json
3 Content-Length: xxx
4  
5 {
6    "uri" : "/api/categories/1",
7    "label" : "Food",
8    ……
9 }

  能夠看到,該方法是很是嚴格地遵照REST系統所提出的約束的。但其也並非沒有缺點:添加一個自定義MIME類型(Custom MIME Type)也是一個很麻煩的流程,並且在不少現有技術中都沒有很好地支持它,如HTML5中的Form。所以這種方案的缺點是對REST API用戶並不那麼友好。

  除此以外,另外一種基於重定向的解決方案也被提出。該方案容許一個REST系統提供多個版本的API,並在URI中標明版本號:

1 /api/v2/categories
2 /api/v1/categories

  這樣用戶能夠選擇使用特定版本的REST API來實現客戶端功能。因爲其使用固定版本的API,所以並不存在着一個資源有多種表示,進而違反了HATEOAS約束的問題。

  在REST系統的API隨時間逐漸發展出衆多版本的時候,系統對API的維護也將成爲一個較大的問題。此時就須要逐漸退役一些年代久遠的API 版本。對這些版本的退役主要分爲兩步:首先將其標爲過時的,可是還在一段時間內支持。在這種狀況下,對這些已通過期的API的訪問將獲得3XX響應,如301 Moved Permanently,以通知用戶該URI所標示的資源須要使用新版本的URI進行訪問。而再通過一段時間後,則將過時的REST API標記爲廢棄的。此時用戶在訪問這些URI時將返回4XX響應,如410 Gone。

  接下來,該REST系統還能夠提供一個通用的REST API接口,並與最新版本的API保持一致:

1 /api/categories

  這樣用戶還能夠選擇一直使用最新版本的API,只是同時也須要一直對其進行維護,以保持與最新版本API的兼容性。在REST系統的API隨着時間的推移逐漸發生變化的時候,該客戶端也須要逐漸更新自身的功能。

  可是該方法有一個問題:由通用URI所辨識出的各個資源須要是穩定的,不能在必定時間以後被廢棄,不然會給用戶帶來很是大的維護性的麻煩。舉例來講,假設客戶端邏輯添加了一系列操做分類的功能。當REST系統決定再也不採用分類做爲商品歸類的標準,那麼客戶端邏輯中與分類相關的各個功能都須要進行大幅度地修改。過於頻繁的這種改動很容易致使用戶對該系統所提供的API失去維護的信心。所以在抽象資源時必定要努力地將各個資源的邊界辨識清楚。雖說這聽起來很嚇人,可是在通過仔細考慮後這種狀況仍是較爲容易避免的。

  可是反過來講,理論經常與實際有些脫鉤,更況且REST是在2000年左右提出的,沒法作到可以預見到十餘年後所使用的各項技術。所以在儘可能符合REST所提出的各約束上提供一個最直觀的,具備最高易用性的API纔是王道。無限制地提供後向兼容性是一個很是困難,成本很是高的事情。所以在版本管理這一方面上來講,咱們也須要儘可能兼顧項目需求和徹底聽從理論這二者之間的平衡。

  而在同一個版本之中,咱們則須要保證API的後向兼容性。也就是說,在添加新的資源以及爲資源添加新的屬性的時候,原有的對資源進行操做的API也應該是工做的。

  對於一個基於HTTP的REST服務而言,軟件開發人員須要遵照以下的守則以保持API的後向兼容性:

  1. 不能在請求中添加新的必須的參數。
  2. 不能更改操做資源的動詞。
  3. 不能更改響應的HTTP status。

  而前向兼容性則顯得沒有那麼重要了。REST服務的前向兼容性要求現有的服務兼容將來版本服務的客戶端。可是因爲服務提供商所提供的服務經常是最新版本,所以對前向兼容性有要求的狀況不多出現。另一點是,爲一個服務提供前向兼容性其實並不那麼容易。由於這要求軟件開發人員對產品的將來方向進行很是多的假設,並且這些假設不能有錯誤。反過來,這種對服務的前向兼容性的要求主要由客戶端自身經過保持後向兼容性來完成。

 

性能

  接下來咱們就來簡單地說說基於HTTP的REST服務中的性能問題。在基於HTTP的REST服務中,性能提高主要分爲兩個方面:REST架構自己在提升性能方面作出的努力,以及基於HTTP協議的優化。

  首先要討論的就是對登錄性能的優化。在前面咱們已經介紹過,在一個基於HTTP的REST服務中,每次都將用戶的用戶名和密碼發送到服務端並由服務端驗證這些信息是否合法是一個很是消耗資源的流程。所以咱們經常須要在登錄服務中使用一個緩存,或者是使用第三方單點登錄(SSO)類庫。

  除此以外,軟件開發人員還能夠經過爲同一個資源提供不一樣的表現形式來減小在網絡上傳輸的數據量,從而提升REST服務的性能。

  而在集羣內部服務之間,咱們則能夠再也不使用JSON,XML等這種用戶能夠讀懂的負載格式,而是使用二進制格式。這樣能夠大大地減小內部網絡所須要傳輸的數據量。這在內部網絡交換數據頻繁而且所傳輸的數據量巨大時較爲有效。

  接下來就是REST系統的橫向擴展。在REST的無狀態約束的支持下,咱們能夠很容易地向REST系統中添加一個新的服務器。

  除了這些和REST架構自己相關的性能提高以外,咱們還能夠在如何更高效地使用HTTP協議上努力。一個最多見的方法就是使用條件請求(Conditional Request)。簡單地說,咱們可使用以下的HTTP頭來有條件地存取資源:

  1. ETag:一個對用戶不透明的用來標示資源實例的哈希值
  2. Data-Modified:資源被更改的時間
  3. If-Modified-Since:根據資源的更改時間有條件地Get資源。這將容許客戶端對未更改的資源使用本地緩存。
  4. If-None-Match:根據ETag的值有條件地Get資源。
  5. If-Unmodified-Since:根據資源的更改時間有條件地Put或Delete資源。
  6. If-Match:根據ETag的值有條件地Put或Delete資源。

  固然,這裏所提到的一系列性能優化方案實際上僅僅是比較常見的,與基於HTTP的REST服務關聯較大的方案。只是顧慮到過多地陳述和REST關聯不大的話題一方面顯得比較沒有效率,另外一方面也是由於經過寫另外一個系列博客能夠將問題陳述得更加清楚,所以在這裏咱們將再也不繼續討論性能相關的話題。

 

相關資源

AtomPub:http://atomenabled.org/。其是最爲普遍討論的並借鑑的RESTful服務。其由衆多HTTP和REST專家所編寫,甚至包括Roy Fielding本人也參與於其中

Roy Fielding的REST論文:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

Roy Fielding的我的網站:http://roy.gbiv.com/untangled/

RFC列表:http://www.ietf.org/rfc/

 

轉載請註明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/4669091.html

相關文章
相關標籤/搜索