一說(shuō)到REST,我想大家的第一反應就是“啊,就是那種前后臺通信方式?!钡窃谝笤敿氈v述它所提出的各個(gè)約束,以及如何開(kāi)始搭建REST服務(wù)時(shí),卻很少有人能夠清晰地說(shuō)出它到底是什么,需要遵守什么樣的準則。
在您將看到的這一篇文章中,我們將對REST,尤其是基于HTTP的REST服務(wù)進(jìn)行詳細地介紹。通過(guò)這些文章,您不僅可以了解到什么是REST,更能清晰地了解到您在編寫(xiě)REST服務(wù)時(shí)所需要遵守的各個(gè)守則,設計RESTful API時(shí)需要考慮的各種因素以及實(shí)現過(guò)程中可能遇到的問(wèn)題等內容。
REST示例
我想,很多讀者可能并不太清楚REST到底是一個(gè)什么概念。那么,首先讓我們來(lái)看一個(gè)簡(jiǎn)單的基于HTTP的REST服務(wù)示例。
假設用戶(hù)正在訪(fǎng)問(wèn)一個(gè)電子商務(wù)網(wǎng)站www.egoods.com。該網(wǎng)站對其所銷(xiāo)售的各個(gè)物品進(jìn)行了詳細分類(lèi)。當用戶(hù)登錄該網(wǎng)站進(jìn)行購物時(shí),他首先需要在該網(wǎng)站上選擇其所需要尋找物品的分類(lèi),進(jìn)而列出屬于該分類(lèi)的各個(gè)物品。

當然,雖然從業(yè)務(wù)邏輯的角度來(lái)說(shuō)這個(gè)流程非常簡(jiǎn)單,但實(shí)際上瀏覽器向后臺發(fā)送了多個(gè)請求:頁(yè)面邏輯在頁(yè)面加載時(shí)將首先得到所有的商品分類(lèi),并將這些分類(lèi)顯示在了頁(yè)面中。在用戶(hù)選擇了一個(gè)分類(lèi)的時(shí)候,頁(yè)面邏輯將發(fā)送一個(gè)請求得到該分類(lèi)的詳細信息,并發(fā)送另外一個(gè)請求來(lái)得到該分類(lèi)的商品列表:

在通過(guò)瀏覽器的調試功能查看這些請求的時(shí)候,我們可以看到其首先向www.egoods.com/api/categories發(fā)送一個(gè)GET請求,以取得所有的商品分類(lèi):
1 GET /api/categories2 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json
而服務(wù)端將返回所有的類(lèi)別:
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 ]
該響應返回了一個(gè)用JSON表示的數組。該數組中的每個(gè)元素包含了兩部分信息:用戶(hù)能夠讀懂的表示分類(lèi)名稱(chēng)的label以及相應分類(lèi)所對應的URL。其中Label所記錄的分類(lèi)名稱(chēng)將在頁(yè)面中顯示給用戶(hù)。而在用戶(hù)根據label所標示的分類(lèi)名選擇了一個(gè)分類(lèi)的時(shí)候,頁(yè)面邏輯會(huì )取得該分類(lèi)所對應的URL并向該URL 發(fā)送請求,以得到該分類(lèi)的詳細信息。例如在用戶(hù)點(diǎn)擊了“食品”這個(gè)分類(lèi)的時(shí)候,瀏覽器將會(huì )向服務(wù)器發(fā)送如下的請求:
1 GET /api/categories/12 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json
這一次,頁(yè)面邏輯根據用戶(hù)對分類(lèi)的選擇“食品”來(lái)得到了其所對應的URL,并向該URL發(fā)送了一個(gè)GET請求。而該請求所得到的響應則為:
HTTP/1.1 200 OKContent-Type: application/jsonContent-Length: xxx { "url" : "/api/categories/1", "label" : "Food", "items_url" : "/api/items?category=1", "brands" : [ { "label" : "友臣", "brand_key" : "32073", "url" : "/api/brands/32073" }, { "label" : "樂(lè )事", "brand_key" : "56632", "url" : "/api/brands/56632" } ... ], "hot_searches" : …}
該響應略為復雜。首先,響應中的URL標示了“食品”分類(lèi)所對應的URL。而label屬性則和前面一樣,用來(lái)在頁(yè)面上顯示分類(lèi)的名稱(chēng)。一個(gè)較為特殊的屬性則是items_url。其用來(lái)標示獲取屬于食品分類(lèi)的各個(gè)產(chǎn)品的URL。而屬性brands則用來(lái)列出在“食品”分類(lèi)中的著(zhù)名品牌,例如友臣,樂(lè )事等。這些品牌被組織為一個(gè)對象數組,而數組中的每個(gè)對象都擁有label,url等屬性。在這些屬性的幫助下,頁(yè)面可以列出這些著(zhù)名品牌的名稱(chēng),并允許用戶(hù)通過(guò)點(diǎn)擊跳轉到這些品牌所對應的頁(yè)面上。除了這些屬性之外,Food分類(lèi)還包含了其它一系列屬性,如表示當前其它用戶(hù)正在搜索的hot_searches屬性等,這里就不再贅述。
該響應有一個(gè)問(wèn)題,那就是符合用戶(hù)篩選條件的各個(gè)產(chǎn)品并沒(méi)有包含在該響應中。這是因為頁(yè)面所列出的各個(gè)產(chǎn)品是根據用戶(hù)所設置的篩選條件,即其選擇的品牌以及搜索關(guān)鍵字而變化的。因此,頁(yè)面邏輯會(huì )根據屬性items_url以及用戶(hù)所設定的搜索條件組合成為目標URL,再次發(fā)送請求到后臺,以請求需要在頁(yè)面中展現的各個(gè)物品。
例如用戶(hù)在只想瀏覽屬于樂(lè )事品牌的食品時(shí),其可以鉤選樂(lè )事這個(gè)品牌,那么此時(shí)的URL將由食物分類(lèi)的items_url以及表示按照品牌進(jìn)行篩選的URL參數共同組成:
1 GET /api/items?category=1&brand_key=566322 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json
現在讓我們來(lái)總結一下上面所展示的基于HTTP的REST系統的整個(gè)運行流程。在開(kāi)始的時(shí)候,我們拿到了所有分類(lèi)的列表。列表中的各個(gè)條目不僅僅包含了用戶(hù)可以看到的分類(lèi)名稱(chēng)等信息,更擁有一個(gè)額外的URL屬性。在用戶(hù)選擇該列表中的一項時(shí),頁(yè)面邏輯將會(huì )向對應的URL發(fā)送一個(gè)請求,以獲得該項目的詳細信息。在這個(gè)詳細信息中,一些內容又包含了一些其它的URL,從而使得頁(yè)面邏輯又能通過(guò)該URL屬性發(fā)送請求。
您也許會(huì )說(shuō),哎,這不和我們現有系統的運行流程一樣的嘛。是的。在上面所舉出的例子中,我們也更偏重地描述了REST系統所需要具有的HATEOAS(Hypermedia As The Engine Of Application State)特性。正是由于這個(gè)特性已經(jīng)在大家所創(chuàng )建的系統里面廣泛地使用了,因此我更希望從熟悉的地方入手,而不是開(kāi)始就非常教條地說(shuō)REST一定要這樣,一定要那樣,徒增了學(xué)習的難度。
反過(guò)來(lái)說(shuō),上面所展示的REST服務(wù)并不具有典型性。在充分了解了REST后,您會(huì )發(fā)現,REST在系統設計上的視角將不再把流程放在了最優(yōu)先的位置。
而在后面的章節中,我們則會(huì )逐漸展開(kāi),詳細地介紹如何創(chuàng )建一個(gè)純正的基于HTTP的REST服務(wù)。
REST的定義
OK,現在讓我們來(lái)看看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.
從上面的定義中,我們可以發(fā)現REST其實(shí)是一種組織Web服務(wù)的架構,而并不是我們想象的那樣是實(shí)現Web服務(wù)的一種新的技術(shù),更沒(méi)有要求一定要使用HTTP。其目標是為了創(chuàng )建具有良好擴展性的分布式系統。
反過(guò)來(lái),作為一種架構,其提出了一系列架構級約束。這些約束有:
如果一個(gè)系統滿(mǎn)足了上面所列出的五條約束,那么該系統就被稱(chēng)為是RESTful的。
下面我們再次通過(guò)電子商務(wù)網(wǎng)站egoods這個(gè)示例來(lái)幫助我們理解這些約束。首先,egoods是一個(gè)電子商務(wù)網(wǎng)站。用戶(hù)需要通過(guò)瀏覽器,手機或者網(wǎng)站所發(fā)布的瀏覽應用來(lái)訪(fǎng)問(wèn)該網(wǎng)站的內容。因此其使用的自然是客戶(hù)/服務(wù)器模型。而在瀏覽過(guò)程中,用戶(hù)需要訪(fǎng)問(wèn)不同類(lèi)型的數據,如商品描述、購物車(chē)等信息。這些信息可能由egoods網(wǎng)站服務(wù)中不同的服務(wù)器來(lái)提供的,因此在用戶(hù)瀏覽過(guò)程中可能需要與不止一個(gè)服務(wù)器進(jìn)行交互。如果在服務(wù)端保存了有關(guān)客戶(hù)的任何狀態(tài),那么在用戶(hù)與不同服務(wù)器進(jìn)行交互的時(shí)候,客戶(hù)的狀態(tài)就需要在這些服務(wù)之間進(jìn)行同步,大大地增加了系統的復雜度。因此,REST要求客戶(hù)端自行維護狀態(tài),并在每次發(fā)送請求的時(shí)候提供自身所儲存的處理該請求所必需的信息。而恰當地使用緩存這一條也非常容易理解。在客戶(hù)端請求一個(gè)自上次請求后沒(méi)有發(fā)生過(guò)變化的信息時(shí),如產(chǎn)品分類(lèi)列表,服務(wù)端僅僅需要返回一個(gè)304響應即可。
這里您可以看到,前四條約束中除了無(wú)狀態(tài)這條約束較為特別之外,其它三條約束在基于HTTP的Web服務(wù)中都很常見(jiàn),也較容易達成。而無(wú)狀態(tài)約束在其它類(lèi)型的Web服務(wù)中并不十分常見(jiàn),因此如何避免違反該約束是在實(shí)現REST服務(wù)時(shí)最常討論的話(huà)題。其不僅僅會(huì )影響到很多功能的設計,更是REST系統擴展性的關(guān)鍵。因此在后面的章節中,我們會(huì )對無(wú)狀態(tài)約束單獨進(jìn)行講解。
在簡(jiǎn)單地介紹了前四個(gè)約束之后,我們就需要著(zhù)重講解統一接口這個(gè)約束了??梢哉f(shuō),前面的四個(gè)約束實(shí)際上都較為容易達成。唯一需要注意的無(wú)非是是否某些技術(shù)實(shí)現違反了這些約束。而第五條約束,統一接口,可以說(shuō)是REST服務(wù)設計的核心所在,也是決定REST服務(wù)設計的成敗之處。在實(shí)現一個(gè)基于HTTP的REST服務(wù)時(shí),軟件開(kāi)發(fā)人員不僅僅需要考慮REST所設置的一系列約束,更需要考慮HTTP各組成的語(yǔ)意,HTTP相關(guān)技術(shù)如何與REST服務(wù)約束結合,如何保持前后向兼容性以及如何進(jìn)行版本管理等問(wèn)題,才能給出一個(gè)自然的,具有較高易用性和較強生命力的REST系統。
而在介紹統一接口約束之前,我們則需要了解一下和REST密切相關(guān)的兩個(gè)名詞:資源和狀態(tài)??梢哉f(shuō),資源是REST系統的核心概念。所有的設計都會(huì )以資源為中心,包括如何對資源進(jìn)行添加,更新,查找以及修改等。而資源本身則擁有一系列狀態(tài)。在每次對資源進(jìn)行添加 ,刪除或修改的時(shí)候,資源就將從一個(gè)狀態(tài)轉移到另外一個(gè)狀態(tài)。
比如說(shuō),在egoods中,商品的分類(lèi)就是一種資源。該資源有很多實(shí)例,包括表示食品的分類(lèi),其所對應的URL是“/api/categories/1”。同樣地,食品的品牌也是一種資源。這些資源的實(shí)例都對應著(zhù)一個(gè)當前的狀態(tài)。在修改了一個(gè)資源實(shí)例之后,比如修改了食品分類(lèi)中的熱搜關(guān)鍵字,那么其將對應著(zhù)一個(gè)新的狀態(tài)。這種狀態(tài)之間的變化被稱(chēng)為是狀態(tài)的轉移。
在大概了解了REST系統中的資源和狀態(tài)的定義后,我們來(lái)看看統一接口這個(gè)約束。該約束又包含了四個(gè)子約束:
現在,讓我們仍然以egoods作為示例來(lái)解釋一下上面四個(gè)子約束。
在前面的章節中,我們已經(jīng)看到了從egoods所返回的表示食品這個(gè)分類(lèi)的響應:
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" : "樂(lè )事",16 "brand_key" : "56632",17 "url" : "/api/brands/56632"18 }19 ...20 ],21 "hot_searches" : …22 }
首先我們看到的是,該響應通過(guò)Content-Type響應頭來(lái)標示響應中所包含的信息是按照JSON格式來(lái)組織的。在看到了該響應頭中所標示的格式之后,消息的接收方就可以按照JSON的格式理解或分析該響應中的負載。這也便是消息的自描述性。
當然,消息的自描述性不僅僅包含如何解析其所攜帶的負載。在一個(gè)基于HTTP的REST系統中,我們可以通過(guò)使用大部分HTTP標準所提供的功能來(lái)提高消息的自描述性。由于這些功能已經(jīng)擁有了完備的文檔,被廣大的軟件開(kāi)發(fā)人員所熟知,并得到了眾多瀏覽器廠(chǎng)商以及Web類(lèi)庫的支持,因此根據這些標準實(shí)現REST服務(wù)具有較高的消息自描述性。舉例來(lái)說(shuō),如果在請求中標明了If-Modified-Since頭,那么服務(wù)端將可能返回一個(gè)304 Not Modified響應。在看到該響應的時(shí)候,瀏覽器或其它瀏覽工具可以從緩存中取得上一次得到的結果。因此,在一個(gè)基于HTTP的REST系統中,如何準確地使用HTTP協(xié)議是一項非常重要的內容。
在獲知了如何對響應所攜帶的負載進(jìn)行解析之后,我們就來(lái)看看資源的自描述性。在上面的示例中,服務(wù)端響應使用了JSON表示了食品分類(lèi)。該表示首先通過(guò)label屬性描述了自己是一個(gè)什么分類(lèi)。接下來(lái),其通過(guò)brands屬性表示了該分類(lèi)中的著(zhù)名品牌,并通過(guò)hot_searches標示了在該分類(lèi)中的熱搜關(guān)鍵字??梢钥吹?,該負載中的所有屬性都清晰地描述了自身所表達的含義。
那在該資源表示中的url屬性是什么意思?實(shí)際上這是為子約束“每個(gè)資源都擁有一個(gè)資源標識”所添加的一個(gè)屬性。該子約束要求每個(gè)資源的資源標識可以用來(lái)唯一地標明該資源。對于網(wǎng)絡(luò )應用來(lái)說(shuō),資源標識就是URI。而在一個(gè)基于HTTP的系統中,最自然的資源標示便是URL。在表示單個(gè)資源的時(shí)候,這個(gè)URL常常會(huì )包含著(zhù)資源在該類(lèi)資源中的ID。
在本文的其它章節中,我們就將以這種方式來(lái)區分URL和ID:URL用來(lái)指向資源所在的地址,而ID則表示該資源在該類(lèi)型資源中的ID。請讀者一定要記得這兩個(gè)術(shù)語(yǔ)所對應的不同意義,以防止理解錯誤。
現在還有一部分食品分類(lèi)表示中的屬性沒(méi)有被講解,那就是在該表示中的各個(gè)URL。這是為子約束HATEOAS服務(wù)的。在用戶(hù)看到items_url屬性時(shí),其就可以通過(guò)向該URL發(fā)送GET消息得到屬于食品分類(lèi)中的所有商品的列表。而在商品品牌的表示中也擁有一個(gè)url屬性。也就是說(shuō),向該URL發(fā)送一個(gè)GET請求也能夠得到相應品牌的詳細信息。
您可能會(huì )問(wèn):既然在介紹HATEOAS時(shí)說(shuō)REST服務(wù)并不需要文檔來(lái)告訴用戶(hù)哪里擁有什么樣的資源,那用戶(hù)應該如何知道向/api/categories發(fā)送GET請求就能得到所有的分類(lèi)呢?標準的做法則是向/api直接發(fā)送一個(gè)GET請求:
1 GET /api2 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json
而在返回的響應中將標示出REST API的版本以及所有可以訪(fǎng)問(wèn)的資源等信息:
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 }
可以看到,在該響應中列出了可以被訪(fǎng)問(wèn)的兩種資源:表示商品分類(lèi)的Categories以及表示商品的Items。在需要訪(fǎng)問(wèn)特定類(lèi)型的資源時(shí),軟件開(kāi)發(fā)人員可以通過(guò)直接向這兩種資源所對應的URI發(fā)送GET請求即可。
OK,相信現在讀者已經(jīng)了解了REST服務(wù)所提供的各種約束。那么在后面的章節中,我們將會(huì )逐步講解如何設計一個(gè)基于HTTP的REST服務(wù)。
資源識別
在一般情況下,對資源的識別通常都是REST服務(wù)設計的第一步。在準確地識別出了各資源之后,怎么用HTTP規范中的各組成來(lái)表示這些資源便是順理成章的事情。在本節中,我們將對如何識別REST系統中的資源進(jìn)行講解。
在通常的軟件開(kāi)發(fā)過(guò)程中,我們常常需要分析達成某個(gè)目標所需要使用的業(yè)務(wù)邏輯,并為業(yè)務(wù)邏輯的執行提供一系列運行接口。在一些Web服務(wù)中,這些接口常常表達了某個(gè)動(dòng)作,如將商品放入購物車(chē),提交訂單等。這一系列動(dòng)作組合在一起就可以組成完成目標所需要執行的業(yè)務(wù)邏輯。在需要調用這些接口的時(shí)候,軟件開(kāi)發(fā)人員需要向這些接口所在的URL發(fā)送一個(gè)請求,從而驅使服務(wù)執行該動(dòng)作。
而在REST服務(wù)中,我們所提供的各個(gè)接口則需要是一系列資源,而業(yè)務(wù)邏輯需要通過(guò)對資源的操作來(lái)完成。也就是說(shuō),REST服務(wù)中的API將不再以執行了什么動(dòng)作為中心,而是以資源為中心。一些對資源的通用操作有添加,取得,修改,刪除,以及對符合特定條件的資源進(jìn)行列表操作。
仍然讓我們以上面所舉的“將商品放入購物車(chē)”這個(gè)操作為例。在一個(gè)REST系統中,購物車(chē)將被抽象為一個(gè)資源,而“將商品放入購物車(chē)”這個(gè)操作將被解釋為對購物車(chē)這個(gè)資源的更新:更新購物車(chē),以使特定商品包含在購物車(chē)內。
可能對于剛剛學(xué)習REST的各位讀者而言,這種以資源為中心的描述方法有些別扭。這種描述方法的確有別于很多Web服務(wù)那樣以動(dòng)作為中心。而與之對應的則是系統設計步驟的改變:我們將不再首先是別完成業(yè)務(wù)邏輯所需的各動(dòng)作,而是支持業(yè)務(wù)邏輯所需要的各資源。那么我們應該如何抽象出這些資源呢?首先,我們對某個(gè)操作不要再關(guān)注它所執行的動(dòng)作,而是關(guān)心它所操作的賓語(yǔ)。通常情況下,該賓語(yǔ)就會(huì )是REST系統中的資源。
在這里,我們就以“提交訂單”作為示例來(lái)展示如何抽象資源。
首先,在“提交訂單”這個(gè)動(dòng)作中,訂單是賓語(yǔ)。因此對于該業(yè)務(wù)邏輯,其將作為一個(gè)資源存在。除此之外,在訂單中還需要包含一系列信息,例如訂單中所包含的商品,訂單所屬人等。一旦這些都可以被該REST系統中的其它資源使用,那么它們也將成為獨立的資源。
但是有時(shí)候,一個(gè)動(dòng)作可能并不存在著(zhù)它所操作的賓語(yǔ)。在這種情況下,我們就需要考慮該動(dòng)作產(chǎn)生或消除了哪個(gè)實(shí)體,或者哪個(gè)實(shí)體的狀態(tài)發(fā)生了變化。這個(gè)發(fā)生了變化的實(shí)體實(shí)際上就是一種資源。例如對于登陸這一行為,其實(shí)際上在服務(wù)端創(chuàng )建了一個(gè)會(huì )話(huà)實(shí)例。該會(huì )話(huà)實(shí)例中則包含了登陸IP,登陸時(shí)間,以及登陸時(shí)所用的憑證等。再比如對于用戶(hù)更改密碼這種行為,其所操作的資源就是用戶(hù)資料。
在抽象資源的過(guò)程中,我們需要按照自頂向下的方式,即首先辨識出系統中的最主要資源,然后再辨識這些主要資源的子資源,并依次進(jìn)行迭代。
對主資源的抽取主要通過(guò)分析業(yè)務(wù)邏輯來(lái)完成。在得到功能需求以后,我們首先要分析這些業(yè)務(wù)邏輯所操作的賓語(yǔ)。這些賓語(yǔ)可能有兩種情況:主資源或者其它資源的子資源。主資源實(shí)際上就是能夠獨立存在的一系列資源。而子資源則需要依附于主資源之上才能表達實(shí)際的意義。同時(shí)各個(gè)子資源也可能擁有自身的子資源。
判斷一個(gè)資源是否是子資源的一個(gè)方法就是看它是否能獨立地表示其具體含義。例如對于一個(gè)egoods上所銷(xiāo)售的商品,其名稱(chēng),價(jià)格,簡(jiǎn)介等屬性可以清晰地描述該商品到底是什么,到底如何銷(xiāo)售。因此這些商品實(shí)際上是一個(gè)主資源。但是每種商品所支持的郵遞服務(wù)需要是一個(gè)子資源:一個(gè)商品可以支持多種郵遞服務(wù)。這些郵遞服務(wù)根據派送距離等需要不同的價(jià)格,也提供了不同的郵遞速度。由于這些郵遞服務(wù)與商家和郵遞服務(wù)公司所達成的服務(wù)價(jià)格有關(guān),并且會(huì )由于商品重量的變化而變化,因此這些郵遞服務(wù)并不能為其它商家所提供的郵遞服務(wù)作為參考,因此其應該作為該商品的一個(gè)子資源。
或者也可以說(shuō),如果一個(gè)資源是主資源,那么其可以被不同的資源實(shí)例包含引用而不會(huì )產(chǎn)生歧義。而如果一個(gè)資源是子資源,那么被不同的資源實(shí)例引用可能會(huì )產(chǎn)生歧義。
但是需要注意的是,一種資源可能有多種不同的表現形式。例如對于在使用列表展示各個(gè)商品的時(shí)候,egoods只需要展示商品的名稱(chēng),一個(gè)對該商品的簡(jiǎn)單描述,商品的價(jià)格以及一張商品的照片。而在用戶(hù)打開(kāi)了該商品頁(yè)之后,頁(yè)面則需要顯示更詳盡的信息,如商品的重量,商品所在地等等。
除此之外,資源列表也有可能擁有多種不同的表現形式。舉例來(lái)說(shuō),如果egoods上屬于某個(gè)分類(lèi)的商品太多,需要分頁(yè)顯示,那么這種分頁(yè)是否也應該是一種資源?答案是,這些分頁(yè)并不是一種資源,而其只是資源列表的一種表現方式。在每頁(yè)所包含商品數量,排序規則等條件發(fā)生變化的時(shí)候,該資源列表中所包含的各個(gè)商品也會(huì )發(fā)生變化。
那么如何判斷我們?yōu)镽EST服務(wù)所定義的資源是否合理呢?一般情況下,我都使用下面的一些判斷方法:
首先,我們需要考慮對該資源的CRUD是否有意義,從而驗證資源的定義是否合理。就以剛剛說(shuō)到的列表的分頁(yè)顯示為例,我們可以想象一下如何對分頁(yè)進(jìn)行添加和刪除?一旦刪除了該分頁(yè),那么屬于該分頁(yè)中的各個(gè)商品也應該被刪除么?而且刪除了分頁(yè)X的數據后,原本X + 1分頁(yè)的數據將展示在X分頁(yè)中。很顯然,將商品的分頁(yè)定義為資源并不合理。
其次,我們需要檢查資源是否需要除CRUD之外的動(dòng)詞來(lái)操作。該方法用來(lái)檢查資源中是否還有子資源沒(méi)有被抽象。如果該資源還需要額外的動(dòng)詞,那么我們就需要考慮這些操作到底引起了什么樣的狀態(tài)變化,進(jìn)而抽象出該資源的子資源。
除此之外,我們還需要檢查這些資源是否是被整體使用,創(chuàng )建和刪除。該方法用來(lái)探測是否一個(gè)子資源應該是一個(gè)主資源。如果在刪除一個(gè)資源的時(shí)候,其子資源還可以被其它資源重用,那么該子資源實(shí)際上具有較高的重用性,應該是一個(gè)主資源。
資源的URL設計
在前面已經(jīng)提到過(guò),統一接口約束中的第一條子約束就是每個(gè)資源都擁有一個(gè)資源標識。在正確地辨識出了一個(gè)資源之后,我們就需要為這些資源分配其所對應的URI。一個(gè)資源所對應的URI可能有多種表示方式,如到底是用單數還是復數表示資源等。因此在一個(gè)基于HTTP的REST系統中,如何組織針對各個(gè)資源的URL實(shí)際上是最重要的一部分。畢竟一個(gè)明確的,有意義并且穩定的API接口實(shí)際上是對服務(wù)對用戶(hù)的一種承諾。
在HTTP中,一個(gè)URL主要由以下幾個(gè)部分組成:
在為一個(gè)資源設計其所對應的URL時(shí),我們需要著(zhù)重考慮第三部分和第四部分組成。
通過(guò)URL來(lái)表示資源
在辨識出了REST系統中的各個(gè)資源以后,我們就需要開(kāi)始為這些資源設計各自所對應的URL了。
首先要介紹的是,所有的資源都應該存在于一個(gè)相對路徑之下。請讀者回憶之前我們介紹的通過(guò)向/api發(fā)送一個(gè)GET請求得到所有可以被訪(fǎng)問(wèn)的資源這個(gè)示例:
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 }
因此對于從向該相對路徑發(fā)送請求才能得到的各個(gè)主資源來(lái)說(shuō),將它們置于相對路徑/api之下是非常合理的。
除了這個(gè)原因之外,API的版本更迭也是一個(gè)考慮。假如軟件開(kāi)發(fā)人員需要開(kāi)發(fā)一個(gè)新版本的REST API,那么他可能就需要重新抽象并定義系統中的各個(gè)資源。但是如果兩個(gè)版本的API中都擁有一個(gè)categories資源,并且系統為了保持后向兼容性同時(shí)保留了兩個(gè)版本的API,那么將只有一個(gè)資源可以使用/categories這個(gè)相對路徑。也正因為如此,將這些資源置于相對路徑/api之下,并在第二個(gè)版本的API出現之后將新的資源抽象置于/api-v2下是一種較為流行的做法。
在明確了所有的資源都應該置于/api這樣一個(gè)相對路徑下之后,我們就來(lái)講解如何為資源定義對應的URL。一個(gè)最簡(jiǎn)單的情況是:指定主資源所對應的URL。由于主資源是一類(lèi)獨立的資源,因此它應該直接置于/api下。例如egoods網(wǎng)站中的產(chǎn)品分類(lèi)就是一個(gè)主資源,我們會(huì )為其分配如下URL:
1 /api/categories而對于其它主資源,如egoods網(wǎng)站中的產(chǎn)品,我們也會(huì )為其賦予一個(gè)具有類(lèi)似結構的URL:
1 /api/items這樣,每類(lèi)主資源都將擁有一個(gè)特定于該類(lèi)資源的URL。這些URL就對應著(zhù)相應資源實(shí)例的集合。
如果需要表示某個(gè)主資源類(lèi)型中的特定實(shí)例,那么我們就需要在該類(lèi)主資源所對應的URL之后添加該實(shí)例的ID。如egoods網(wǎng)站中的食品分類(lèi)的ID為1,那么其所對應的URL就將是:
1 /api/categories/1
一個(gè)較為特殊的情況則是,對于某種類(lèi)型的主資源,整個(gè)系統將有且僅有一個(gè)該類(lèi)型資源的實(shí)例。那么該資源將不再需要通過(guò)ID來(lái)訪(fǎng)問(wèn)。我能想到的一個(gè)例子就是對整個(gè)系統進(jìn)行介紹的資源。該資源實(shí)例所對應的URL將是:
1 /api/about而一個(gè)資源實(shí)例中還可能擁有子資源。這些子資源與資源實(shí)例之間的關(guān)系主要有兩種情況:資源實(shí)例包含了一個(gè)子資源的集合,以及資源實(shí)例僅僅可以包含一個(gè)子資源。對于資源實(shí)例包含了一個(gè)子資源集合的情況,我們需要將該子資源集合的URL置于該資源的相對路徑下。例如對于egoods上所銷(xiāo)售的ID為23456的商品所提供的郵遞服務(wù),我們將使用如下的URL:
1 /api/items/23456/shipments
在該URI中,/api/items/23456對應的就是商品本身,而該商品所提供的郵遞服務(wù)則是該商品的子資源。與主資源特定實(shí)例所具有的URI類(lèi)似,其中一個(gè)ID為87256的郵遞服務(wù)所對應的URI則為:
1 /api/items/23456/shipments/87256
如果資源實(shí)例僅僅可以包含一個(gè)子資源,那么對該子資源的訪(fǎng)問(wèn)也將不再需要ID。如當前商品的折扣信息:
1 /api/items/23456/discount
單數 vs. 復數
接下來(lái)要考慮的一點(diǎn)是,資源在URL中需要由單數表示還是復數表示?這在stackoverflow等眾多論壇上已經(jīng)成為了一個(gè)經(jīng)久不衰的話(huà)題。我們知道,在一個(gè)基于HTTP的REST系統中,一個(gè)資源所對應的URL實(shí)際上也就是對其進(jìn)行操作的URL。因此適當地使用單數和復數對于該系統的用戶(hù)而言有一定的指示作用。在stackoverflow上的一個(gè)常見(jiàn)觀(guān)點(diǎn)是:如果一個(gè)URL所對應的資源是使用復數表示的,那么該類(lèi)型的資源可能有多個(gè)。對該URL發(fā)送Get請求可能返回該資源的一個(gè)列表。反之,如果一個(gè)URL所對應的資源是使用單數表示的,那么該類(lèi)型的資源將只有一個(gè),因此對該URL發(fā)送Get請求將只返回該資源的一個(gè)實(shí)例。
以egoods中的商品分類(lèi)為例。由于一個(gè)網(wǎng)站所售賣(mài)的商品可能有多種類(lèi)別,因此其需要在URL中使用復數形式:/api/categories。而對于一個(gè)該網(wǎng)站的用戶(hù)而言,由于其只會(huì )有一個(gè)個(gè)人偏好設置,因此其URL則需要使用單數形式:/api/users/{user_id}/preference。
你可能會(huì )問(wèn):如果需要得到具有特定ID的某個(gè)實(shí)例時(shí),我們應該對該資源使用單數還是復數呢?答案是復數。這是因為在通過(guò)特定ID訪(fǎng)問(wèn)某個(gè)資源的實(shí)例實(shí)際上就是從該資源的集合中取出特定實(shí)例。因此表示該資源集合的URL實(shí)際上仍然需要使用復數形式,而其后所使用的ID則標明了其所訪(fǎng)問(wèn)的是資源中的單一實(shí)例,因此向這個(gè)URL發(fā)送Get請求將返回該資源的單一實(shí)例。
就以“食品”分類(lèi)為例。該分類(lèi)所對應的URL為/api/categories/1。該URL中的前半部分/api/categories表示egoods網(wǎng)站中所有分類(lèi)的集合,而1則表示在該分類(lèi)集合中的ID為1的分類(lèi)。
相對路徑 vs. 請求參數
另一個(gè)經(jīng)常導致疑惑的地方就是針對資源的某一種特征,我們到底是將其定義為URL中相對路徑的一部分還是作為請求參數。
請考慮下面一個(gè)例子。在egoods網(wǎng)站中,我們售賣(mài)的手機主要有蘋(píng)果,三星等品牌。那么在為這些手機設計URL的時(shí)候,我們是否需要按照品牌對這些手機進(jìn)行細分,從而用戶(hù)只要通過(guò)向/api/mobiles/brands/apple發(fā)送請求就能列出所有的蘋(píng)果手機?還是說(shuō),直接將手機的品牌置于請求參數中,從而通過(guò)/api/mobiles?brand=apple來(lái)列出所有的蘋(píng)果手機?
在判斷到底是使用請求參數還是相對路徑時(shí),我們一般分為下面幾步。
首先,可選參數一般都應置于請求參數中。仍以egoods中的手機為例。在選擇手機時(shí),用戶(hù)可以選擇品牌以及顏色。如果將品牌和顏色都定義在相對URL中,那么具有特定品牌和顏色的手機將可以通過(guò)兩個(gè)不同的URL訪(fǎng)問(wèn):/api/mobiles/brand/{brand}/color/{color}以及/api/mobiles/color/{color}/brand/{brand}。就用戶(hù)而言,其并無(wú)法了解這兩個(gè)URL所表示的是同一類(lèi)資源還是不同類(lèi)型的資源。當然,您可以說(shuō),我們只用/api/mobiles/brand/{brand}/color/{color}。但是該URL將無(wú)法處理用戶(hù)僅僅選擇了顏色,卻沒(méi)有選擇品牌的情況。
其次,不是所有字符都可以在URL中被使用,如漢字,標點(diǎn)。為了處理這種情況,包含這些字符的篩選條件需要置于請求參數中。
最后,如果該特征下包含子資源,那么它自身也就是一個(gè)資源,因此需要以相對路徑的方式展現它。例如在egoods網(wǎng)站中,每件商品所屬于的分類(lèi)僅僅是它的一個(gè)特征。但是一個(gè)分類(lèi)更包含了屬于它的各個(gè)品牌以及熱搜關(guān)鍵字等眾多信息。因此它其實(shí)是一個(gè)資源,需要在URI路徑中表示它。
總的來(lái)說(shuō),既然使用HTTP來(lái)構建REST系統,那么我們就需要遵守URL各組成中的含義:URL中的相對路徑將用來(lái)標示“What I want”,也既對應著(zhù)資源;而請求參數則用來(lái)標示“How I want”,即查看資源的方式。
使用合適的動(dòng)詞
在知道了如何為每種資源定義URI之后,我們來(lái)看看如何操作這些資源。
首先,在一個(gè)資源的生命周期之內常常會(huì )發(fā)生一系列通用事件(CRUD)。一開(kāi)始,一個(gè)資源并不存在。只有用戶(hù)或REST服務(wù)創(chuàng )建了該資源以后其才存在,也即是上面所列出的通用事件中的C,Create。在一個(gè)資源創(chuàng )建完畢以后,用戶(hù)可能會(huì )從服務(wù)端請求該資源的表示,也就是上面所列出的通用事件的R,Retrieve。在特定情況下,用戶(hù)可能決定要更新該資源,因此會(huì )使用上面的通用事件中的U,即Update來(lái)更新資源。而在資源不再需要的時(shí)候,用戶(hù)可能需要通過(guò)通用事件D,即Delete來(lái)刪除該資源。同時(shí)用戶(hù)有時(shí)也需要列出屬于特定類(lèi)型資源的資源實(shí)例,即通過(guò)List操作來(lái)得到屬于特定類(lèi)型的資源的列表。
在前面的講解中我們已經(jīng)提到過(guò),在REST系統中的每個(gè)資源都有一個(gè)特定的URI與之對應。HTTP協(xié)議提供了多種在URI上操作的動(dòng)詞,如GET,PUT,POST以及DELETE等。因此在一個(gè)基于HTTP的REST服務(wù)中,我們需要使用這些HTTP動(dòng)詞來(lái)表示如何對這些資源進(jìn)行CRUD操作。而在什么情況下到底使用哪個(gè)動(dòng)詞則是由這些動(dòng)詞本身在HTTP協(xié)議中的意義所決定的。
這其中GET和DELETE兩個(gè)動(dòng)詞的含義較為清晰:
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.
也就是說(shuō),在需要讀取某個(gè)資源的時(shí)候,我們向該資源所對應的URI發(fā)送一個(gè)GET請求即可。類(lèi)似的,在需要刪除一個(gè)資源的時(shí)候,我們只需要向該資源所對應的URI發(fā)送一個(gè)DELETE請求即可。而在希望得到某類(lèi)型資源的列表的時(shí)候,我們可以直接向該類(lèi)型資源所對應的URI發(fā)送一個(gè)GET請求。
而動(dòng)詞PUT和POST則是較為容易混淆的兩個(gè)動(dòng)詞。在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
也就是說(shuō),POST動(dòng)詞會(huì )在目標URI之下創(chuàng )建一個(gè)新的子資源。例如在向服務(wù)端發(fā)送下面的請求時(shí),REST系統將創(chuàng )建一個(gè)新的分類(lèi):
1 POST /api/categories2 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json5 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."
也就是說(shuō),PUT則是根據請求創(chuàng )建或修改特定位置的資源。此時(shí)向服務(wù)端發(fā)送的請求的目標URI需要包含所處理資源的ID:
1 POST /api/categories/8fa866a1-735a-4a56-b69c-d7e79896015e2 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json5 6 {7 "label" : "Electronics",8 ……9 }
可以看到,兩者都有創(chuàng )建的含義,但是意義卻不同。在決定到底是使用PUT還是POST來(lái)創(chuàng )建資源的時(shí)候,軟件開(kāi)發(fā)人員需要考慮一系列問(wèn)題:
首先就是資源的ID是如何生成的。如果希望客戶(hù)端在創(chuàng )建資源的時(shí)候顯式地指定該資源的ID,那么就需要使用PUT。而在由服務(wù)端為該資源自動(dòng)賦予ID的時(shí)候,我們就需要在創(chuàng )建資源時(shí)使用POST。在決定使用PUT創(chuàng )建資源的時(shí)候,防止資源URI與其它資源所具有的URI重復的任務(wù)需要由客戶(hù)端來(lái)保證。在這種情況下,客戶(hù)端常常使用GUID/UUID作為將資源的ID。但是到底使用GUID/UUID還是由服務(wù)端來(lái)生成ID不僅僅和REST有關(guān),更會(huì )對數據庫性能等多個(gè)方面產(chǎn)生影響。因此在決定使用它們之前要仔細地考慮清楚。
同時(shí)需要注意的是,因為REST要求客戶(hù)只可以通過(guò)服務(wù)端返回結果中所包含的信息來(lái)得到下一步操作所需要的信息,因此客戶(hù)端僅僅可以決定資源的ID,而URI中的其它部分則需要從之前得到的響應中取得。
但是軟件開(kāi)發(fā)人員常常會(huì )進(jìn)入另外一個(gè)誤區很多人認為REST服務(wù)中的HATEOAS只能通過(guò)Hyperlink完成。實(shí)際上在Roy對REST的定義中使用的是Hypermedia,即響應中的所有多媒體信息。就像Roy在其個(gè)人網(wǎng)站上所說(shuō)(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.
另外一個(gè)需要考慮的因素則是PUT的等冪性是否對REST系統的設計有所幫助。由于在同一個(gè)URI上調用兩次PUT所得到的結果相同。因此用戶(hù)在沒(méi)有接到PUT請求響應時(shí)可以放心地重復發(fā)送該響應。這在網(wǎng)絡(luò )丟包較為嚴重時(shí)是一個(gè)非常好的功能。反過(guò)來(lái),在同一個(gè)URI上調用兩次POST將可能創(chuàng )建兩個(gè)獨立的子資源。
除此之外,還需要考慮是否將資源的創(chuàng )建和更新歸結為一個(gè)API可以簡(jiǎn)化用戶(hù)對REST服務(wù)的使用。用戶(hù)可以通過(guò)PUT動(dòng)詞來(lái)同時(shí)完成創(chuàng )建和更新一個(gè)資源這兩種不同的任務(wù)。這樣的好處在于簡(jiǎn)化了REST服務(wù)所提供的接口,但是反過(guò)來(lái)也讓一個(gè)API執行了兩種不同的任務(wù),在一定程度上違反了API設計時(shí)每個(gè)API都需要有明確的意義這一原則。
因此在決定到底使用POST還是PUT來(lái)完成資源的創(chuàng )建之前,請考慮上面所列出的三條問(wèn)題,以確定到底哪個(gè)動(dòng)詞更加適合。
除此之外,另外一對類(lèi)似的動(dòng)詞則是PUT和PATCH。兩者之間的不同則在于PUT是對整個(gè)資源的更新,而PATCH則是對部分資源的更新。而該動(dòng)詞的局限性則在于對該動(dòng)詞的支持程度。畢竟在某些類(lèi)庫中并沒(méi)有提供原生的對PATCH動(dòng)詞的支持。
使用標準的狀態(tài)碼
在與REST服務(wù)進(jìn)行交互的時(shí)候,用戶(hù)需要通過(guò)服務(wù)所返回的信息決定其所發(fā)送的請求是否被適當地處理。這部分功能是由REST服務(wù)實(shí)現時(shí)所使用的協(xié)議所決定的,與REST架構無(wú)關(guān)。而在基于HTTP的REST服務(wù)中,該功能就由HTTP響應的狀態(tài)碼(Status Code)來(lái)完成。因此在設計一個(gè)REST服務(wù)時(shí),我們需要額外地注意是否返回了正確的狀態(tài)碼。
但是這些預定義的HTTP狀態(tài)碼并不能滿(mǎn)足所有的情況。有時(shí)候一個(gè)REST服務(wù)所希望返回的錯誤信息能夠更加精確地描述問(wèn)題,例如在用戶(hù)重設密碼時(shí),我們需要在用戶(hù)所輸入原密碼與系統中所記錄的密碼不匹配時(shí)返回“您所輸入的密碼有誤”這樣的消息。在HTTP協(xié)議中,我們并沒(méi)有辦法找到一個(gè)能夠精確地表示該意義的狀態(tài)碼。
因此在通常情況下,REST服務(wù)都會(huì )在響應中額外地提供一個(gè)說(shuō)明性的負載來(lái)告知用戶(hù)到底產(chǎn)生了什么問(wèn)題。例如對于上面的重設密碼失敗的情況,服務(wù)端可能會(huì )返回如下響應:
1 HTTP/1.1 400 Bad Request2 Content-Type: application/json3 Content-Length: xxx4 5 {6 "error_id" : "100045",7 "header" : "Reset password failed",8 "description" : "The original password is not correct"9 }
上面的示例響應中主要包含以下的說(shuō)明性信息:
在該錯誤中,最關(guān)鍵的當屬服務(wù)端的響應代碼。一個(gè)響應代碼不僅僅標示了請求是否成功,更有用戶(hù)該如何操作的含義。例如對于401 Unauthorized響應代碼而言,其表示該響應沒(méi)有提供一個(gè)合法的身份憑證,因此需要用戶(hù)首先執行登陸操作以得到一個(gè)合法的身份憑證,然后該資源可能就可以被訪(fǎng)問(wèn)了。而403 Forbidden響應代碼則表示當前請求已經(jīng)提供了一個(gè)合法的身份憑證,但是該身份憑證并沒(méi)有訪(fǎng)問(wèn)該資源的權限,因此使用該身份憑證登陸重新登陸系統等操作并不能解決問(wèn)題。
因此在返回錯誤信息之前,軟件開(kāi)發(fā)人員首先需要考慮清楚在響應中到底應該使用什么樣的響應代碼。而正確地選擇響應代碼則建立在軟件開(kāi)發(fā)人員對這些響應代碼擁有一個(gè)正確的理解的前提下。
當然,要將所有的響應代碼完全理解也需要大量的工作,而且REST服務(wù)的用戶(hù)也可能并沒(méi)有那么多的領(lǐng)域知識來(lái)了解所有的響應代碼的含義。因此在很多基于HTTP的REST系統中,系統在標示錯誤時(shí)只使用一系列常用的響應代碼,如400,401,403,404,405,500,503等。在用戶(hù)請求被處理時(shí),系統將返回200 OK,表示請求已經(jīng)被處理。而在處理時(shí)發(fā)生錯誤時(shí)則盡量使用這些響應代碼來(lái)表示。如果一個(gè)錯誤較為復雜,那么直接返回400或500,并在響應的負載中提供具體的錯誤信息。
不得不說(shuō)的是,這種做法有時(shí)顯得簡(jiǎn)單粗暴,尤其是對于一個(gè)開(kāi)放平臺而言則更是致命的。當一個(gè)第三方廠(chǎng)商為一個(gè)開(kāi)放平臺開(kāi)發(fā)一個(gè)應用軟件,卻每次只能得到一個(gè)400錯誤,那么其內部應用邏輯將無(wú)法判斷到底是哪里出了問(wèn)題。為了能讓用戶(hù)知道這里產(chǎn)生了錯誤,該第三方軟件只能將開(kāi)放平臺所給出的信息直接顯示給用戶(hù)。但是這些信息實(shí)際上是建立在開(kāi)放平臺這個(gè)語(yǔ)境下的,因此對于第三方廠(chǎng)商的用戶(hù)而言,這些信息晦澀難懂,甚至可能一點(diǎn)幫助也沒(méi)有。
也就是說(shuō),到底如何組織這些響應代碼需要用戶(hù)根據所編寫(xiě)的項目決定,尤其是該產(chǎn)品的使用者來(lái)決定。在定義一個(gè)平臺時(shí),盡量使用更多的HTTP響應代碼,因為用戶(hù)極有可能通過(guò)該平臺編寫(xiě)自己的第三方軟件。而在為一個(gè)普通的產(chǎn)品定義REST API時(shí),將響應代碼定得非常專(zhuān)業(yè)可能反而導致易用性的下降。
另外一點(diǎn)需要說(shuō)明的是,個(gè)人不建議使用Wikipedia查找各個(gè)狀態(tài)碼的含義,而應該使用RFC所描述的各狀態(tài)碼的定義。 IANA提供了一張各個(gè)狀態(tài)碼所對應的RFC協(xié)議的列表,從而可以很容易地找到各個(gè)狀態(tài)碼所對應的RFC協(xié)議以及其所在的章節。該列表的地址為:http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
之所以不建議使用Wikipedia的原因主要有兩點(diǎn):
選擇適當的表示結構
接下來(lái)我們要講解的就是如何為資源定義一個(gè)恰當的表示。
首先需要強調的是,REST并沒(méi)有規定其服務(wù)中需要使用什么格式來(lái)表示資源。表示資源時(shí)所可以選取的表示形式實(shí)際上是由實(shí)現REST所使用的協(xié)議決定的。而在一個(gè)基于HTTP的REST服務(wù)中,我們可以使用JSON,也可以使用XML,甚至是自定義的MIME類(lèi)型來(lái)表示資源。這些表現形式常常是等效的。相信讀者已經(jīng)看到,本系列文章會(huì )使用JSON來(lái)表示這些資源。
一個(gè)REST服務(wù)常常會(huì )同時(shí)支持多種客戶(hù)端。這些客戶(hù)端可能會(huì )使用不同的協(xié)議來(lái)與服務(wù)進(jìn)行溝通。而且就算是使用相同的協(xié)議,不同的客戶(hù)端所可以接受的負載表示形式也會(huì )有所不同。因此客戶(hù)端需要與REST服務(wù)協(xié)商在通訊過(guò)程中所使用的負載。
客戶(hù)端和服務(wù)端對所使用負載類(lèi)型的協(xié)商通常都按照協(xié)議所規定的標準協(xié)商過(guò)程來(lái)完成。例如對于一個(gè)基于HTTP的REST服務(wù),我們就需要使用Accept頭來(lái)標示客戶(hù)端所可以接受的負載類(lèi)型:
1 GET /api/categories2 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/json
而在服務(wù)端支持的情況下,返回的響應就將使用該MIME類(lèi)型組織其負載:
1 HTTP/1.1 200 OK2 Content-Type: application/json3 Content-Length: xxx
在這里我們再重復一次:REST是一種組織Web服務(wù)的架構,其只在架構方面提出了一系列約束??梢哉f(shuō),所有對REST的講解都已經(jīng)在前兩個(gè)章節,即“REST的定義”以及“資源識別”中完成了。而有關(guān)客戶(hù)端和服務(wù)端如何進(jìn)行溝通,為資源定義什么樣的URI,使用什么格式的數據進(jìn)行溝通等討論都是在闡述如何將REST架構所提出的各種約束和基于HTTP協(xié)議的Web服務(wù)結合在一起。畢竟在通常情況下,實(shí)現一個(gè)單純的技術(shù)不難,但是如何將多種技術(shù)規范自然地混合在一起,構成一個(gè)自然的,成熟穩定的解決方案才是項目開(kāi)發(fā)中的難點(diǎn)。HTTP協(xié)議并不是為REST架構所定義的,因此如何用HTTP協(xié)議來(lái)恰當地描述一個(gè)REST服務(wù)才是本文所著(zhù)重介紹的。
負載的自描述性
在前面對REST提出的幾個(gè)約束的講解中我們已經(jīng)提到過(guò),REST系統中所傳遞的各個(gè)消息的負載需要提供足夠的用于操作該資源的信息,如如何對資源進(jìn)行添加,刪除以及修改等操作,并可以根據負載中所包含的對其它各資源的引用來(lái)訪(fǎng)問(wèn)各個(gè)資源。這也對負載的自描述性提出了更高的要求。
首先讓我們回頭看看egoods電子商務(wù)網(wǎng)站對食品分類(lèi)的描述:
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" : "樂(lè )事",12 "brand_key" : "56632",13 "url" : "/api/brands/56632"14 }15 ...16 ],17 "hot_searches" : …18 }
我想讀者在看到該響應之后可能就已經(jīng)明白了很多域的含義。但還是讓我們依次對這些域進(jìn)行講解。
第一個(gè)要講解的是url域。該域用來(lái)標示該資源所對應的URL??赡苣鷷?huì )問(wèn):既然我們就是從這個(gè)URL返回的該資源,那么為什么我們還需要在該資源中保存一個(gè)它所對應的URL呢?首先這是因為在統一接口約束中要求每個(gè)資源都擁有一個(gè)資源標識。在這里我們使用URL作為標識。而另一些基于HTTP的REST系統中,用來(lái)作為資源標識的常常是該資源的ID。個(gè)人更傾向于使用URL的原因則是:在某些情況下,如對某個(gè)資源定時(shí)刷新以進(jìn)行監控的時(shí)候,URL可以直接被使用。
接下來(lái)是label域。其用來(lái)記錄用于展示給用戶(hù)的分類(lèi)名。
items_url域則用來(lái)表示取得屬于該分類(lèi)物品列表的URL。注意這里我使用了后綴_url以明確標明其是一個(gè)URL,需要通過(guò)跳轉來(lái)取得實(shí)際的數據。
下一個(gè)域brands則用來(lái)表示屬于該分類(lèi)的著(zhù)名商品品牌。這里我們使用了一個(gè)數組,而數組中的每個(gè)元素都表示了一個(gè)品牌。每個(gè)品牌的表示都包含了一個(gè)展示給用戶(hù)的label,在搜索時(shí)所使用的鍵,以及該品牌所對應的url。您可能會(huì )懷疑為什么我們僅僅提供了這么少的域。這是因為他們僅僅是對這個(gè)品牌的引用,而并非是把該資源的詳細信息都包含進(jìn)來(lái)了的緣故。在用戶(hù)希望查看該品牌的詳細信息的時(shí)候,他需要向該品牌引用中所標明的品牌的URL發(fā)送一個(gè)GET請求。
而由于hot_searches域的組成及使用基本上與brands域類(lèi)似,因此這里不再贅述。
在大致地了解了食品分類(lèi)的JSON表示中各個(gè)域的含義后,我們就將開(kāi)始講解如何自行定義資源的JSON表示。對于一個(gè)簡(jiǎn)單的,不包含任何子資源以及對其它資源的引用的資源,我們只需要通過(guò)一個(gè)包含簡(jiǎn)單屬性的JSON來(lái)表示它。例如對于一個(gè)品牌,我們可能僅僅提供了一系列描述性信息:品牌的名稱(chēng),以及對品牌的簡(jiǎn)單描述。那么它所對應的JSON表示可以表示為:
1 {2 "uri" : "/api/brands/32059",3 "label" : "Dole",4 "description" : "An American-based agricultural multinational corporation."5 }
而在另一個(gè)資源中,可能包含了對其它資源的引用。在這種情況下,我們就需要在表示對其它資源進(jìn)行引用的域中通過(guò)URL來(lái)標明被引用資源的位置。例如一件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域就是對品牌的引用。該引用中包含了該品牌的品牌名稱(chēng)以及一個(gè)指向該品牌的URL。
在一個(gè)基于HTTP的REST系統中,我們常常在資源的引用中包含一定量的描述信息。這主要因為兩點(diǎn):
當然,如果需要在展示Dole果汁的頁(yè)面中需要Dole這個(gè)品牌的完整信息,我們也可以將它直接嵌到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 }
當然,如果一個(gè)資源的表示太過(guò)復雜,而且有些屬性實(shí)際上是相互關(guān)聯(lián)的,那么我們也可以通過(guò)一個(gè)屬性將它們歸結在一起:
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來(lái)表示所有的營(yíng)養成分,而該域內部的各個(gè)子域則用來(lái)表示一系列相關(guān)的營(yíng)養成分所占比例。
另外,在不同的情況下,我們還可能對同一個(gè)資源提供不同的表現形式。例如在一個(gè)資源極為復雜,其JSON表示甚至可以達到幾百K的時(shí)候,我們可以為該資源提供一個(gè)簡(jiǎn)化版本,以在非必要的情況下減少傳輸的數據量。
例如在egoods中,我們會(huì )將某些物美價(jià)廉的商品置于它的首頁(yè)上,以吸引用戶(hù)購買(mǎi)。在用戶(hù)將鼠標移動(dòng)到某個(gè)商品上并停留一段時(shí)間時(shí),我們會(huì )為用戶(hù)展示一個(gè)Tooltip,并在該Tooltip中展示該商品的一部分信息。在這種情況下,向服務(wù)端請求該商品的所有信息以展示Tooltip便顯得有些效率低下了。
有時(shí)候,一個(gè)資源可能并不支持特定用戶(hù)執行某個(gè)操作。例如一個(gè)管理員所創(chuàng )建的資源可能對普通用戶(hù)只讀。在這種情況下,我們需要禁止普通用戶(hù)對該資源的修改和刪除。為了能明確地告知用戶(hù)他所具有的權限,我們需要一個(gè)能顯式地標示用戶(hù)可以在一個(gè)資源上所執行操作的組成。在REST響應中,這種組成被稱(chēng)為Hypermedia Controls。例如對于一個(gè)普通用戶(hù),其從egoods中所返回的分類(lèi)列表將如下所示:
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 ]
可以看到,在上面的分類(lèi)列表中,我們通過(guò)actions域顯式地標示了用戶(hù)可以在各個(gè)類(lèi)別上所能執行的操作。而對于管理員,其還可以執行修改,刪除等操作:
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 ]
而在一系列較為著(zhù)名的REST系統中,如Sun Cloud API,其更是通過(guò)Hypermedia Controls定義了除CRUD之外的動(dòng)詞。如對于一個(gè)虛擬機,其在運行狀態(tài)下可以執行停止命令,而在停止狀態(tài)下可以執行啟動(dòng)命令:
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 }
但是一個(gè)常見(jiàn)的觀(guān)點(diǎn)是:如果一個(gè)資源需要除CRUD之外的額外的動(dòng)詞,那么這種需求常常表示我們對于某個(gè)資源的定義并不是十分合理。因此在遇到這種情況時(shí),軟件開(kāi)發(fā)人員首先需要考慮為資源添加額外的動(dòng)詞是否合適。
無(wú)狀態(tài)約束
在Roy Fielding的論文中,其為REST添加了一個(gè)無(wú)狀態(tài)約束:
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.
從上面的陳述中可以看到,在一個(gè)REST系統中,用戶(hù)的狀態(tài)會(huì )隨著(zhù)請求在客戶(hù)端和服務(wù)端之間來(lái)回傳遞。這也便是REST這個(gè)縮寫(xiě)中ST(State Transfer)的來(lái)歷。
為REST系統添加這個(gè)約束有什么好處呢?主要還是基于集群擴展性的考慮。如果REST服務(wù)中記錄了用戶(hù)相關(guān)的狀態(tài),那么在集群中,這些用戶(hù)相關(guān)的狀態(tài)就需要及時(shí)地在集群中的各個(gè)服務(wù)器之間同步。對用戶(hù)狀態(tài)的同步將會(huì )是一個(gè)非常棘手的問(wèn)題:當一個(gè)用戶(hù)的相關(guān)狀態(tài)在一個(gè)服務(wù)器上發(fā)生了更改,那么在什么時(shí)候,什么情況下對這些狀態(tài)進(jìn)行同步?如果該狀態(tài)同步是同步進(jìn)行的,那么同時(shí)刷新多個(gè)服務(wù)器上的用戶(hù)狀態(tài)將導致對用戶(hù)請求的處理變得異常緩慢。如果該同步是異步的,那么用戶(hù)在發(fā)送下一個(gè)請求時(shí),其它服務(wù)器將可能由于用戶(hù)狀態(tài)不同步的原因無(wú)法正確地處理用戶(hù)的請求。除此之外,如果集群進(jìn)行了不停機的橫向擴展,那么用戶(hù)狀態(tài)的同步需要如何完成?這些實(shí)際上都是非常難以處理的問(wèn)題。
但是現有的很多較為流行的技術(shù)及規范實(shí)際上都沒(méi)有限制用戶(hù)的請求是無(wú)狀態(tài)的。相信您知道,一個(gè)技術(shù)或規范實(shí)際上都擁有一個(gè)生態(tài)圈。在該生態(tài)圈之內的各技術(shù)之間可以較好地契合在一起。尤其是,有些技術(shù)實(shí)際上就會(huì )以該生態(tài)圈中的核心技術(shù)或規范所建立的假設之上來(lái)實(shí)現自己的功能。如果希望禁止該假設,那么讓某些技術(shù)工作起來(lái)就是非常困難的事情了。
就以搭建基于HTTP的REST服務(wù)為例。在HTTP中,一個(gè)重要的功能就是Cookie和Session的使用(RFC6265)。該功能會(huì )在服務(wù)器里保留一個(gè)狀態(tài)。因此在一個(gè)基于HTTP的REST系統中,我們常常需要避免使用這些在服務(wù)器里面保留狀態(tài)的技術(shù)。但是某些技術(shù),如用戶(hù)的登陸,實(shí)際上常常需要在服務(wù)器中添加一個(gè)狀態(tài)。
所以在stackoverflow中,我們常常會(huì )看到有人問(wèn):我現在使用了這樣一種解決方案。這樣實(shí)現是不是RESTful?此時(shí)一些人就會(huì )說(shuō),這不是RESTful。但是pure RESTful和almost RESTful之間的區別主要還是在于一個(gè)是理論,一個(gè)是工程。在工程中,輕微地違反了一個(gè)準則并不一定代表這個(gè)解決方案一無(wú)是處。而是要看遵守該準則和輕微地違反了該準則之后工作量的大小以及后期的維護成本:之所以提出一系列準則,那是因為遵守該準則擁有一定的好處。如果對該準則的輕微違反可以減少大量的工作量,而且遵守準則的好處并沒(méi)有消失,或者是通過(guò)另一樣技術(shù)可以快速地重新獲得該好處,那么對準則的輕微違反是值得的。
Authentication
其實(shí)在上一節中,我們已經(jīng)提出了無(wú)狀態(tài)約束給REST實(shí)現帶來(lái)的麻煩:用戶(hù)的狀態(tài)是需要全部保存在客戶(hù)端的。當用戶(hù)需要執行某個(gè)操作的時(shí)候,其需要將所有的執行該請求所需要的信息添加到請求中。該請求將可能被REST服務(wù)集群中的任意服務(wù)器處理,而不需要擔心該服務(wù)器中是否存有用戶(hù)相關(guān)的狀態(tài)。
但是在現有的各種基于HTTP的Web服務(wù)中,我們常常使用會(huì )話(huà)來(lái)管理用戶(hù)狀態(tài),至少是用戶(hù)的登陸狀態(tài)。因此,REST系統的無(wú)狀態(tài)約束實(shí)際上并不是一個(gè)對傳統用戶(hù)登錄功能友好的約束:在傳統登陸過(guò)程中,其本身就是通過(guò)用戶(hù)所提供的用戶(hù)名和密碼等在服務(wù)端創(chuàng )建一個(gè)用戶(hù)的登陸狀態(tài),而REST的無(wú)狀態(tài)約束為了橫向擴展性卻不想要這種狀態(tài)。而這也就是為基于HTTP的REST服務(wù)添加身份驗證功能的困難之處。
為了解決該問(wèn)題,最為經(jīng)典也最符合REST規范的實(shí)現是在每次發(fā)送請求的時(shí)候都將用戶(hù)的用戶(hù)名和密碼都發(fā)送給服務(wù)器。而服務(wù)器將根據請求中的用戶(hù)名和密碼調用登陸服務(wù),以從該服務(wù)中得到用戶(hù)所對應的Identity和其所具有的權限。接下來(lái),在REST服務(wù)中根據用戶(hù)的權限來(lái)訪(fǎng)問(wèn)資源。

這里有一個(gè)問(wèn)題就是登陸的性能。隨著(zhù)系統當前的加密算法越來(lái)越復雜,登陸已經(jīng)不再是一個(gè)輕量級的操作。因此用戶(hù)所發(fā)送的每次請求都要求一次登陸對于整個(gè)系統而言就是一個(gè)巨大的瓶頸。
在當前,解決該問(wèn)題的方法主要是一個(gè)獨立的緩存系統,如整個(gè)集群唯一的登陸服務(wù)器。但是緩存系統本身所存儲的仍然是用戶(hù)的登陸狀態(tài)。因此該解決方案將仍然輕微地違反了REST的無(wú)狀態(tài)約束。
還有一個(gè)類(lèi)似的方法是通過(guò)添加一個(gè)代理來(lái)完成的。該代理會(huì )完成用戶(hù)的登陸并獲得該用戶(hù)所擁有的權限。接下來(lái),該代理會(huì )將與狀態(tài)有關(guān)的信息從請求中刪除,并添加用戶(hù)的權限信息。在經(jīng)過(guò)了這種處理之后,這些請求就可以轉發(fā)到其后的各個(gè)服務(wù)器上了。轉發(fā)目的地所在的服務(wù)器則會(huì )假設所有傳入的請求都是合法的并直接對這些請求進(jìn)行處理。

可以看到,無(wú)論是一個(gè)獨立的登陸服務(wù)器還是為整個(gè)集群添加一個(gè)代理,系統中都將有一個(gè)地方保留了用戶(hù)的登陸狀態(tài)。這實(shí)際上和在集群中對會(huì )話(huà)集中進(jìn)行管理并沒(méi)有什么不同。也就是說(shuō),我們所嘗試的通過(guò)禁止使用會(huì )話(huà)來(lái)達成完全的無(wú)狀態(tài)并不現實(shí)。因此在一個(gè)基于HTTP的REST服務(wù)中,為登陸功能使用集中管理的會(huì )話(huà)是合理的。
既然我們放松了對REST系統的無(wú)狀態(tài)約束,那么一個(gè)REST系統所可以使用的登陸機制將主要分為以下兩種:
1. 基于HTTPS的Basic Access Authentication
其好處是其易于實(shí)現,而且主流的瀏覽器都提供了對該功能的支持。但是由于登陸窗口都是由瀏覽器所提供的,因此其與產(chǎn)品外觀(guān)有很大不同。除此之外,瀏覽器都沒(méi)有提供登出的功能,也沒(méi)有提供找回密碼等功能。
2. 基于Cookie及Session的管理
在使用Cookie來(lái)管理用戶(hù)的注冊狀態(tài)的時(shí)候,其實(shí)際上就是將服務(wù)端所返回的Cookie在每次發(fā)送請求的時(shí)候添加到請求中。雖然說(shuō)這個(gè)Cookie并非存儲了用戶(hù)應用的狀態(tài),但是其實(shí)際存儲了用戶(hù)的登陸狀態(tài)。因此客戶(hù)端的角度來(lái)講,由服務(wù)端管理的Session并不符合REST所倡導的無(wú)狀態(tài)的要求。
可以說(shuō),上面的兩種方法各有優(yōu)劣??赡艿诙N方法從客戶(hù)端的角度看來(lái)并不是RESTful的,但是其優(yōu)勢則在于很多類(lèi)庫都直接提供了對該功能的支持,從而簡(jiǎn)化了會(huì )話(huà)管理服務(wù)器的實(shí)現。
在這里順便提一句,如果項目足夠大,將一些SSO產(chǎn)品集成到服務(wù)中也是不錯的選擇。
版本管理
在前面已經(jīng)提到過(guò),一個(gè)REST系統為資源所抽象出的URI實(shí)際上是對用戶(hù)的一種承諾。但反過(guò)來(lái)說(shuō),軟件開(kāi)發(fā)人員也很難預知一個(gè)資源的各方面特征如何在未來(lái)發(fā)生變化,從而提供一個(gè)永遠不變的URI。
在一個(gè)REST系統逐漸發(fā)展的過(guò)程中,新的屬性,新的資源將逐漸被添加到該系統中。在這些更改過(guò)程中,資源的URI,訪(fǎng)問(wèn)資源的動(dòng)詞,響應中的Status Code將不能發(fā)生變化。此時(shí)軟件開(kāi)發(fā)人員所做的工作就是在現有系統上維護REST API的后向兼容性。
當資源發(fā)生了過(guò)多的變化,原有的URI設計已經(jīng)很難兼容現有資源應有的定義時(shí),軟件開(kāi)發(fā)人員就需要考慮是否應該提供一個(gè)新版本的REST API。那么我們該如何對資源的版本進(jìn)行管理呢?
首先要考慮的就是,新API的版本信息是否應當包含在資源的URI中。這在各著(zhù)名論壇中仍然是一個(gè)爭議較大的話(huà)題。一種觀(guān)點(diǎn)認為在不同版本的API中,一個(gè)資源擁有不同的地址在一定程度上違反了HATEOAS:URI只是用來(lái)指定一個(gè)資源所在的位置,而不是該資源如何被抽象。如果一個(gè)資源由不同的URI標示其不同的表現形式,那么用戶(hù)將無(wú)法通過(guò)一個(gè)響應中所標示的URI得到其它URI所指向的表示形式。而且在URI中添加了有關(guān)版本的信息也就標示著(zhù)其可能會(huì )隨著(zhù)時(shí)間的推移發(fā)生變化。
一種使用獨立URI的方法是基于A(yíng)ccept頭。在一個(gè)請求中,我們常常標明了Accept頭,以標示客戶(hù)端希望得到的表現形式。在該頭中,用戶(hù)可以添加所請求的資源的版本信息:
1 GET /api/categories/12 Host: www.egoods.com3 Authorization: Basic xxxxxxxxxxxxxxxxxxx4 Accept: application/vnd.ambergarden.egoods-v3+json
而在接收到該請求之后,服務(wù)端將返回該資源的第三個(gè)版本:
1 HTTP/1.1 200 OK2 Content-Type: application/vnd.ambergarden.egoods-v3+json3 Content-Length: xxx4 5 {6 "uri" : "/api/categories/1",7 "label" : "Food",8 ……9 }
可以看到,該方法是非常嚴格地遵守REST系統所提出的約束的。但其也并不是沒(méi)有缺點(diǎn):添加一個(gè)自定義MIME類(lèi)型(Custom MIME Type)也是一個(gè)很麻煩的流程,而且在很多現有技術(shù)中都沒(méi)有很好地支持它,如HTML5中的Form。因此這種方案的缺點(diǎn)是對REST API用戶(hù)并不那么友好。
除此之外,另一種基于重定向的解決方案也被提出。該方案允許一個(gè)REST系統提供多個(gè)版本的API,并在URI中標明版本號:
1 /api/v2/categories2 /api/v1/categories
這樣用戶(hù)可以選擇使用特定版本的REST API來(lái)實(shí)現客戶(hù)端功能。由于其使用固定版本的API,因此并不存在著(zhù)一個(gè)資源有多種表示,進(jìn)而違反了HATEOAS約束的問(wèn)題。
在REST系統的API隨時(shí)間逐漸發(fā)展出眾多版本的時(shí)候,系統對API的維護也將成為一個(gè)較大的問(wèn)題。此時(shí)就需要逐漸退役一些年代久遠的API 版本。對這些版本的退役主要分為兩步:首先將其標為過(guò)期的,但是還在一段時(shí)間內支持。在這種情況下,對這些已經(jīng)過(guò)期的API的訪(fǎng)問(wèn)將得到3XX響應,如301 Moved Permanently,以通知用戶(hù)該URI所標示的資源需要使用新版本的URI進(jìn)行訪(fǎng)問(wèn)。而再經(jīng)過(guò)一段時(shí)間后,則將過(guò)期的REST API標記為廢棄的。此時(shí)用戶(hù)在訪(fǎng)問(wèn)這些URI時(shí)將返回4XX響應,如410 Gone。
接下來(lái),該REST系統還可以提供一個(gè)通用的REST API接口,并與最新版本的API保持一致:
1 /api/categories這樣用戶(hù)還可以選擇一直使用最新版本的API,只是同時(shí)也需要一直對其進(jìn)行維護,以保持與最新版本API的兼容性。在REST系統的API隨著(zhù)時(shí)間的推移逐漸發(fā)生變化的時(shí)候,該客戶(hù)端也需要逐漸更新自身的功能。
但是該方法有一個(gè)問(wèn)題:由通用URI所辨識出的各個(gè)資源需要是穩定的,不能在一定時(shí)間之后被廢棄,否則會(huì )給用戶(hù)帶來(lái)非常大的維護性的麻煩。舉例來(lái)說(shuō),假設客戶(hù)端邏輯添加了一系列操作分類(lèi)的功能。當REST系統決定不再采用分類(lèi)作為商品歸類(lèi)的標準,那么客戶(hù)端邏輯中與分類(lèi)相關(guān)的各個(gè)功能都需要進(jìn)行大幅度地修改。過(guò)于頻繁的這種改動(dòng)很容易導致用戶(hù)對該系統所提供的API失去維護的信心。因此在抽象資源時(shí)一定要努力地將各個(gè)資源的邊界辨識清楚。雖然說(shuō)這聽(tīng)起來(lái)很?chē)樔?,但是在?jīng)過(guò)仔細考慮后這種情況還是較為容易避免的。
但是反過(guò)來(lái)說(shuō),理論常常與實(shí)際有些脫鉤,更何況REST是在2000年左右提出的,無(wú)法做到能夠預見(jiàn)到十余年后所使用的各項技術(shù)。因此在盡量符合REST所提出的各約束上提供一個(gè)最直觀(guān)的,具有最高易用性的API才是王道。無(wú)限制地提供后向兼容性是一個(gè)非常困難,成本非常高的事情。因此在版本管理這一方面上來(lái)說(shuō),我們也需要盡量兼顧項目需求和完全遵從理論這兩者之間的平衡。
而在同一個(gè)版本之中,我們則需要保證API的后向兼容性。也就是說(shuō),在添加新的資源以及為資源添加新的屬性的時(shí)候,原有的對資源進(jìn)行操作的API也應該是工作的。
對于一個(gè)基于HTTP的REST服務(wù)而言,軟件開(kāi)發(fā)人員需要遵守如下的守則以保持API的后向兼容性:
而前向兼容性則顯得沒(méi)有那么重要了。REST服務(wù)的前向兼容性要求現有的服務(wù)兼容未來(lái)版本服務(wù)的客戶(hù)端。但是由于服務(wù)提供商所提供的服務(wù)常常是最新版本,因此對前向兼容性有要求的情況很少出現。另外一點(diǎn)是,為一個(gè)服務(wù)提供前向兼容性其實(shí)并不那么容易。因為這要求軟件開(kāi)發(fā)人員對產(chǎn)品的未來(lái)方向進(jìn)行非常多的假設,而且這些假設不能有錯誤。反過(guò)來(lái),這種對服務(wù)的前向兼容性的要求主要由客戶(hù)端自身通過(guò)保持后向兼容性來(lái)完成。
性能
接下來(lái)我們就來(lái)簡(jiǎn)單地說(shuō)說(shuō)基于HTTP的REST服務(wù)中的性能問(wèn)題。在基于HTTP的REST服務(wù)中,性能提升主要分為兩個(gè)方面:REST架構本身在提高性能方面做出的努力,以及基于HTTP協(xié)議的優(yōu)化。
首先要討論的就是對登陸性能的優(yōu)化。在前面我們已經(jīng)介紹過(guò),在一個(gè)基于HTTP的REST服務(wù)中,每次都將用戶(hù)的用戶(hù)名和密碼發(fā)送到服務(wù)端并由服務(wù)端驗證這些信息是否合法是一個(gè)非常消耗資源的流程。因此我們常常需要在登陸服務(wù)中使用一個(gè)緩存,或者是使用第三方單點(diǎn)登陸(SSO)類(lèi)庫。
除此之外,軟件開(kāi)發(fā)人員還可以通過(guò)為同一個(gè)資源提供不同的表現形式來(lái)減少在網(wǎng)絡(luò )上傳輸的數據量,從而提高REST服務(wù)的性能。
而在集群內部服務(wù)之間,我們則可以不再使用JSON,XML等這種用戶(hù)可以讀懂的負載格式,而是使用二進(jìn)制格式。這樣可以大大地減少內部網(wǎng)絡(luò )所需要傳輸的數據量。這在內部網(wǎng)絡(luò )交換數據頻繁并且所傳輸的數據量巨大時(shí)較為有效。
接下來(lái)就是REST系統的橫向擴展。在REST的無(wú)狀態(tài)約束的支持下,我們可以很容易地向REST系統中添加一個(gè)新的服務(wù)器。
除了這些和REST架構本身相關(guān)的性能提升之外,我們還可以在如何更高效地使用HTTP協(xié)議上努力。一個(gè)最常見(jiàn)的方法就是使用條件請求(Conditional Request)。簡(jiǎn)單地說(shuō),我們可以使用如下的HTTP頭來(lái)有條件地存取資源:
當然,這里所提到的一系列性能優(yōu)化方案實(shí)際上僅僅是比較常見(jiàn)的,與基于HTTP的REST服務(wù)關(guān)聯(lián)較大的方案。只是顧慮到過(guò)多地陳述和REST關(guān)聯(lián)不大的話(huà)題一方面顯得比較沒(méi)有效率,另一方面也是因為通過(guò)寫(xiě)另一個(gè)系列博客可以將問(wèn)題陳述得更加清楚,因此在這里我們將不再繼續討論性能相關(guān)的話(huà)題。
相關(guān)資源
AtomPub:http://atomenabled.org/。其是最為廣泛討論的并借鑒的RESTful服務(wù)。其由眾多HTTP和REST專(zhuān)家所編寫(xiě),甚至包括Roy Fielding本人也參與于其中
Roy Fielding的REST論文:http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
Roy Fielding的個(gè)人網(wǎng)站:http://roy.gbiv.com/untangled/。
RFC列表:http://www.ietf.org/rfc/
轉載請注明原文地址并標明轉載:http://www.cnblogs.com/loveis715/p/4669091.html
商業(yè)轉載請事先與我聯(lián)系:silverfox715@sina.com
聯(lián)系客服