rest

一說到REST生巡,我想大家的第一反應(yīng)就是“啊洽蛀,就是那種前后臺通信方式摹迷。”但是在要求詳細(xì)講述它所提出的各個約束郊供,以及如何開始搭建REST服務(wù)時峡碉,卻很少有人能夠清晰地說出它到底是什么,需要遵守什么樣的準(zhǔn)則颂碘。

在您將看到的這一篇文章中异赫,我們將對REST,尤其是基于HTTP的REST服務(wù)進(jìn)行詳細(xì)地介紹头岔。通過這些文章,您不僅可以了解到什么是REST鼠证,更能清晰地了解到您在編寫REST服務(wù)時所需要遵守的各個守則峡竣,設(shè)計RESTful API時需要考慮的各種因素以及實現(xiàn)過程中可能遇到的問題等內(nèi)容。

REST示例

我想量九,很多讀者可能并不太清楚REST到底是一個什么概念适掰。那么,首先讓我們來看一個簡單的基于HTTP的REST服務(wù)示例荠列。

假設(shè)用戶正在訪問一個電子商務(wù)網(wǎng)站www.egoods.com类浪。該網(wǎng)站對其所銷售的各個物品進(jìn)行了詳細(xì)分類。當(dāng)用戶登錄該網(wǎng)站進(jìn)行購物時肌似,他首先需要在該網(wǎng)站上選擇其所需要尋找物品的分類费就,進(jìn)而列出屬于該分類的各個物品。

當(dāng)然川队,雖然從業(yè)務(wù)邏輯的角度來說這個流程非常簡單力细,但實際上瀏覽器向后臺發(fā)送了多個請求:頁面邏輯在頁面加載時將首先得到所有的商品分類,并將這些分類顯示在了頁面中固额。在用戶選擇了一個分類的時候眠蚂,頁面邏輯將發(fā)送一個請求得到該分類的詳細(xì)信息,并發(fā)送另外一個請求來得到該分類的商品列表:

在通過瀏覽器的調(diào)試功能查看這些請求的時候斗躏,我們可以看到其首先向www.egoods.com/api/categories發(fā)送一個GET請求逝慧,以取得所有的商品分類:

1GET /api/categories2Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json

而服務(wù)端將返回所有的類別:

1HTTP/1.1200OK2Content-Type: application/json3Content-Length: xxx45[6{7"label":"食品",8"url":"/api/categories/1"9}, {10"label":"服裝",11"url":"/api/categories/2"12}13...14{15"label":"電子設(shè)備",16"url":"/api/categories/25"17}18]

該響應(yīng)返回了一個用JSON表示的數(shù)組。該數(shù)組中的每個元素包含了兩部分信息:用戶能夠讀懂的表示分類名稱的label以及相應(yīng)分類所對應(yīng)的URL。其中Label所記錄的分類名稱將在頁面中顯示給用戶笛臣。而在用戶根據(jù)label所標(biāo)示的分類名選擇了一個分類的時候栅干,頁面邏輯會取得該分類所對應(yīng)的URL并向該URL 發(fā)送請求,以得到該分類的詳細(xì)信息捐祠。例如在用戶點擊了“食品”這個分類的時候碱鳞,瀏覽器將會向服務(wù)器發(fā)送如下的請求:

1GET /api/categories/12Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json

這一次,頁面邏輯根據(jù)用戶對分類的選擇“食品”來得到了其所對應(yīng)的URL踱蛀,并向該URL發(fā)送了一個GET請求窿给。而該請求所得到的響應(yīng)則為:

HTTP/1.1200OK

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": …

}

該響應(yīng)略為復(fù)雜。首先率拒,響應(yīng)中的URL標(biāo)示了“食品”分類所對應(yīng)的URL崩泡。而label屬性則和前面一樣,用來在頁面上顯示分類的名稱猬膨。一個較為特殊的屬性則是items_url角撞。其用來標(biāo)示獲取屬于食品分類的各個產(chǎn)品的URL。而屬性brands則用來列出在“食品”分類中的著名品牌勃痴,例如友臣谒所,樂事等。這些品牌被組織為一個對象數(shù)組沛申,而數(shù)組中的每個對象都擁有l(wèi)abel劣领,url等屬性。在這些屬性的幫助下铁材,頁面可以列出這些著名品牌的名稱尖淘,并允許用戶通過點擊跳轉(zhuǎn)到這些品牌所對應(yīng)的頁面上。除了這些屬性之外著觉,F(xiàn)ood分類還包含了其它一系列屬性村生,如表示當(dāng)前其它用戶正在搜索的hot_searches屬性等,這里就不再贅述饼丘。

該響應(yīng)有一個問題趁桃,那就是符合用戶篩選條件的各個產(chǎn)品并沒有包含在該響應(yīng)中。這是因為頁面所列出的各個產(chǎn)品是根據(jù)用戶所設(shè)置的篩選條件葬毫,即其選擇的品牌以及搜索關(guān)鍵字而變化的镇辉。因此,頁面邏輯會根據(jù)屬性items_url以及用戶所設(shè)定的搜索條件組合成為目標(biāo)URL贴捡,再次發(fā)送請求到后臺忽肛,以請求需要在頁面中展現(xiàn)的各個物品。

例如用戶在只想瀏覽屬于樂事品牌的食品時烂斋,其可以鉤選樂事這個品牌屹逛,那么此時的URL將由食物分類的items_url以及表示按照品牌進(jìn)行篩選的URL參數(shù)共同組成:

1GET /api/items?category=1&brand_key=566322Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json

現(xiàn)在讓我們來總結(jié)一下上面所展示的基于HTTP的REST系統(tǒng)的整個運行流程础废。在開始的時候,我們拿到了所有分類的列表罕模。列表中的各個條目不僅僅包含了用戶可以看到的分類名稱等信息评腺,更擁有一個額外的URL屬性。在用戶選擇該列表中的一項時淑掌,頁面邏輯將會向?qū)?yīng)的URL發(fā)送一個請求蒿讥,以獲得該項目的詳細(xì)信息。在這個詳細(xì)信息中抛腕,一些內(nèi)容又包含了一些其它的URL芋绸,從而使得頁面邏輯又能通過該URL屬性發(fā)送請求。

您也許會說担敌,哎摔敛,這不和我們現(xiàn)有系統(tǒng)的運行流程一樣的嘛。是的全封。在上面所舉出的例子中马昙,我們也更偏重地描述了REST系統(tǒng)所需要具有的HATEOAS(Hypermedia As The Engine Of Application State)特性。正是由于這個特性已經(jīng)在大家所創(chuàng)建的系統(tǒng)里面廣泛地使用了刹悴,因此我更希望從熟悉的地方入手行楞,而不是開始就非常教條地說REST一定要這樣,一定要那樣颂跨,徒增了學(xué)習(xí)的難度敢伸。

反過來說,上面所展示的REST服務(wù)并不具有典型性恒削。在充分了解了REST后,您會發(fā)現(xiàn)尾序,REST在系統(tǒng)設(shè)計上的視角將不再把流程放在了最優(yōu)先的位置钓丰。

而在后面的章節(jié)中,我們則會逐漸展開每币,詳細(xì)地介紹如何創(chuàng)建一個純正的基于HTTP的REST服務(wù)携丁。

REST的定義

OK,現(xiàn)在讓我們來看看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ā)現(xiàn)REST其實是一種組織Web服務(wù)的架構(gòu)错妖,而并不是我們想象的那樣是實現(xiàn)Web服務(wù)的一種新的技術(shù),更沒有要求一定要使用HTTP肥橙。其目標(biāo)是為了創(chuàng)建具有良好擴展性的分布式系統(tǒng)。

反過來秸侣,作為一種架構(gòu)存筏,其提出了一系列架構(gòu)級約束宠互。這些約束有:

使用客戶/服務(wù)器模型⊥旨幔客戶和服務(wù)器之間通過一個統(tǒng)一的接口來互相通訊予跌。

層次化的系統(tǒng)。在一個REST系統(tǒng)中善茎,客戶端并不會固定地與一個服務(wù)器打交道券册。

無狀態(tài)。在一個REST系統(tǒng)中垂涯,服務(wù)端并不會保存有關(guān)客戶的任何狀態(tài)烁焙。也就是說,客戶端自身負(fù)責(zé)用戶狀態(tài)的維持集币,并在每次發(fā)送請求時都需要提供足夠的信息考阱。

可緩存。REST系統(tǒng)需要能夠恰當(dāng)?shù)鼐彺嬲埱缶瞎叮员M量減少服務(wù)端和客戶端之間的信息傳輸乞榨,以提高性能。

統(tǒng)一的接口当娱。一個REST系統(tǒng)需要使用一個統(tǒng)一的接口來完成子系統(tǒng)之間以及服務(wù)與用戶之間的交互吃既。這使得REST系統(tǒng)中的各個子系統(tǒng)可以獨自完成演化。

如果一個系統(tǒng)滿足了上面所列出的五條約束跨细,那么該系統(tǒng)就被稱為是RESTful的鹦倚。

下面我們再次通過電子商務(wù)網(wǎng)站egoods這個示例來幫助我們理解這些約束。首先冀惭,egoods是一個電子商務(wù)網(wǎng)站震叙。用戶需要通過瀏覽器,手機或者網(wǎng)站所發(fā)布的瀏覽應(yīng)用來訪問該網(wǎng)站的內(nèi)容散休。因此其使用的自然是客戶/服務(wù)器模型媒楼。而在瀏覽過程中,用戶需要訪問不同類型的數(shù)據(jù)戚丸,如商品描述划址、購物車等信息。這些信息可能由egoods網(wǎng)站服務(wù)中不同的服務(wù)器來提供的限府,因此在用戶瀏覽過程中可能需要與不止一個服務(wù)器進(jìn)行交互夺颤。如果在服務(wù)端保存了有關(guān)客戶的任何狀態(tài),那么在用戶與不同服務(wù)器進(jìn)行交互的時候胁勺,客戶的狀態(tài)就需要在這些服務(wù)之間進(jìn)行同步世澜,大大地增加了系統(tǒng)的復(fù)雜度。因此姻几,REST要求客戶端自行維護(hù)狀態(tài)宜狐,并在每次發(fā)送請求的時候提供自身所儲存的處理該請求所必需的信息势告。而恰當(dāng)?shù)厥褂镁彺孢@一條也非常容易理解。在客戶端請求一個自上次請求后沒有發(fā)生過變化的信息時抚恒,如產(chǎn)品分類列表咱台,服務(wù)端僅僅需要返回一個304響應(yīng)即可。

這里您可以看到俭驮,前四條約束中除了無狀態(tài)這條約束較為特別之外回溺,其它三條約束在基于HTTP的Web服務(wù)中都很常見,也較容易達(dá)成混萝。而無狀態(tài)約束在其它類型的Web服務(wù)中并不十分常見遗遵,因此如何避免違反該約束是在實現(xiàn)REST服務(wù)時最常討論的話題。其不僅僅會影響到很多功能的設(shè)計逸嘀,更是REST系統(tǒng)擴展性的關(guān)鍵车要。因此在后面的章節(jié)中,我們會對無狀態(tài)約束單獨進(jìn)行講解崭倘。

在簡單地介紹了前四個約束之后翼岁,我們就需要著重講解統(tǒng)一接口這個約束了∷竟猓可以說琅坡,前面的四個約束實際上都較為容易達(dá)成。唯一需要注意的無非是是否某些技術(shù)實現(xiàn)違反了這些約束残家。而第五條約束榆俺,統(tǒng)一接口,可以說是REST服務(wù)設(shè)計的核心所在坞淮,也是決定REST服務(wù)設(shè)計的成敗之處茴晋。在實現(xiàn)一個基于HTTP的REST服務(wù)時,軟件開發(fā)人員不僅僅需要考慮REST所設(shè)置的一系列約束回窘,更需要考慮HTTP各組成的語意,HTTP相關(guān)技術(shù)如何與REST服務(wù)約束結(jié)合毫玖,如何保持前后向兼容性以及如何進(jìn)行版本管理等問題,才能給出一個自然的凌盯,具有較高易用性和較強生命力的REST系統(tǒng)付枫。

而在介紹統(tǒng)一接口約束之前,我們則需要了解一下和REST密切相關(guān)的兩個名詞:資源和狀態(tài)驰怎〔玻可以說,資源是REST系統(tǒng)的核心概念县忌。所有的設(shè)計都會以資源為中心掂榔,包括如何對資源進(jìn)行添加继效,更新,查找以及修改等装获。而資源本身則擁有一系列狀態(tài)瑞信。在每次對資源進(jìn)行添加 ,刪除或修改的時候穴豫,資源就將從一個狀態(tài)轉(zhuǎn)移到另外一個狀態(tài)凡简。

比如說,在egoods中精肃,商品的分類就是一種資源秤涩。該資源有很多實例,包括表示食品的分類司抱,其所對應(yīng)的URL是“/api/categories/1”筐眷。同樣地,食品的品牌也是一種資源习柠。這些資源的實例都對應(yīng)著一個當(dāng)前的狀態(tài)匀谣。在修改了一個資源實例之后,比如修改了食品分類中的熱搜關(guān)鍵字津畸,那么其將對應(yīng)著一個新的狀態(tài)振定。這種狀態(tài)之間的變化被稱為是狀態(tài)的轉(zhuǎn)移。

在大概了解了REST系統(tǒng)中的資源和狀態(tài)的定義后肉拓,我們來看看統(tǒng)一接口這個約束后频。該約束又包含了四個子約束:

每個資源都擁有一個資源標(biāo)識。每個資源的資源標(biāo)識可以用來唯一地標(biāo)明該資源暖途。

消息的自描述性卑惜。在REST系統(tǒng)中所傳遞的消息需要能夠提供自身如何被處理的足夠信息。例如該消息所使用的MIME類型驻售,是否可以被緩存等露久。

資源的自描述性。一個REST系統(tǒng)所返回的資源需要能夠描述自身欺栗,并提供足夠的用于操作該資源的信息毫痕,如如何對資源進(jìn)行添加,刪除以及修改等操作迟几。也就是說消请,一個典型的REST服務(wù)不需要額外的文檔對如何操作資源進(jìn)行說明。

HATEOAS类腮。即客戶只可以通過服務(wù)端所返回各結(jié)果中所包含的信息來得到下一步操作所需要的信息臊泰,如到底是向哪個URL發(fā)送請求等。也就是說蚜枢,一個典型的REST服務(wù)不需要額外的文檔標(biāo)示通過哪些URL訪問特定類型的資源缸逃,而是通過服務(wù)端返回的響應(yīng)來標(biāo)示到底能在該資源上執(zhí)行什么樣的操作针饥。一個REST服務(wù)的客戶端也不需要知道任何有關(guān)哪里有什么樣的資源這種信息。

現(xiàn)在需频,讓我們?nèi)匀灰詄goods作為示例來解釋一下上面四個子約束丁眼。

在前面的章節(jié)中,我們已經(jīng)看到了從egoods所返回的表示食品這個分類的響應(yīng):

1HTTP/1.1200OK2Content-Type: application/json3Content-Length: xxx45{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}

首先我們看到的是贺辰,該響應(yīng)通過Content-Type響應(yīng)頭來標(biāo)示響應(yīng)中所包含的信息是按照J(rèn)SON格式來組織的户盯。在看到了該響應(yīng)頭中所標(biāo)示的格式之后,消息的接收方就可以按照J(rèn)SON的格式理解或分析該響應(yīng)中的負(fù)載饲化。這也便是消息的自描述性莽鸭。

當(dāng)然,消息的自描述性不僅僅包含如何解析其所攜帶的負(fù)載吃靠。在一個基于HTTP的REST系統(tǒng)中硫眨,我們可以通過使用大部分HTTP標(biāo)準(zhǔn)所提供的功能來提高消息的自描述性。由于這些功能已經(jīng)擁有了完備的文檔巢块,被廣大的軟件開發(fā)人員所熟知礁阁,并得到了眾多瀏覽器廠商以及Web類庫的支持,因此根據(jù)這些標(biāo)準(zhǔn)實現(xiàn)REST服務(wù)具有較高的消息自描述性族奢。舉例來說姥闭,如果在請求中標(biāo)明了If-Modified-Since頭,那么服務(wù)端將可能返回一個304 Not Modified響應(yīng)越走。在看到該響應(yīng)的時候棚品,瀏覽器或其它瀏覽工具可以從緩存中取得上一次得到的結(jié)果。因此廊敌,在一個基于HTTP的REST系統(tǒng)中铜跑,如何準(zhǔn)確地使用HTTP協(xié)議是一項非常重要的內(nèi)容。

在獲知了如何對響應(yīng)所攜帶的負(fù)載進(jìn)行解析之后骡澈,我們就來看看資源的自描述性锅纺。在上面的示例中,服務(wù)端響應(yīng)使用了JSON表示了食品分類肋殴。該表示首先通過label屬性描述了自己是一個什么分類囤锉。接下來,其通過brands屬性表示了該分類中的著名品牌护锤,并通過hot_searches標(biāo)示了在該分類中的熱搜關(guān)鍵字嚼锄。可以看到蔽豺,該負(fù)載中的所有屬性都清晰地描述了自身所表達(dá)的含義。

那在該資源表示中的url屬性是什么意思拧粪?實際上這是為子約束“每個資源都擁有一個資源標(biāo)識”所添加的一個屬性修陡。該子約束要求每個資源的資源標(biāo)識可以用來唯一地標(biāo)明該資源沧侥。對于網(wǎng)絡(luò)應(yīng)用來說,資源標(biāo)識就是URI魄鸦。而在一個基于HTTP的系統(tǒng)中宴杀,最自然的資源標(biāo)示便是URL。在表示單個資源的時候拾因,這個URL常常會包含著資源在該類資源中的ID旺罢。

在本文的其它章節(jié)中,我們就將以這種方式來區(qū)分URL和ID:URL用來指向資源所在的地址绢记,而ID則表示該資源在該類型資源中的ID扁达。請讀者一定要記得這兩個術(shù)語所對應(yīng)的不同意義,以防止理解錯誤蠢熄。

現(xiàn)在還有一部分食品分類表示中的屬性沒有被講解跪解,那就是在該表示中的各個URL。這是為子約束HATEOAS服務(wù)的签孔。在用戶看到items_url屬性時叉讥,其就可以通過向該URL發(fā)送GET消息得到屬于食品分類中的所有商品的列表。而在商品品牌的表示中也擁有一個url屬性饥追。也就是說图仓,向該URL發(fā)送一個GET請求也能夠得到相應(yīng)品牌的詳細(xì)信息。

您可能會問:既然在介紹HATEOAS時說REST服務(wù)并不需要文檔來告訴用戶哪里擁有什么樣的資源但绕,那用戶應(yīng)該如何知道向/api/categories發(fā)送GET請求就能得到所有的分類呢救崔?標(biāo)準(zhǔn)的做法則是向/api直接發(fā)送一個GET請求:

1GET /api2Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json

而在返回的響應(yīng)中將標(biāo)示出REST API的版本以及所有可以訪問的資源等信息:

1HTTP/1.1200OK2Content-Type: application/json3Content-Length: xxx45{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}

可以看到,在該響應(yīng)中列出了可以被訪問的兩種資源:表示商品分類的Categories以及表示商品的Items壁熄。在需要訪問特定類型的資源時帚豪,軟件開發(fā)人員可以通過直接向這兩種資源所對應(yīng)的URI發(fā)送GET請求即可。

OK草丧,相信現(xiàn)在讀者已經(jīng)了解了REST服務(wù)所提供的各種約束狸臣。那么在后面的章節(jié)中,我們將會逐步講解如何設(shè)計一個基于HTTP的REST服務(wù)昌执。

資源識別

在一般情況下烛亦,對資源的識別通常都是REST服務(wù)設(shè)計的第一步。在準(zhǔn)確地識別出了各資源之后懂拾,怎么用HTTP規(guī)范中的各組成來表示這些資源便是順理成章的事情煤禽。在本節(jié)中,我們將對如何識別REST系統(tǒng)中的資源進(jìn)行講解岖赋。

在通常的軟件開發(fā)過程中檬果,我們常常需要分析達(dá)成某個目標(biāo)所需要使用的業(yè)務(wù)邏輯,并為業(yè)務(wù)邏輯的執(zhí)行提供一系列運行接口。在一些Web服務(wù)中选脊,這些接口常常表達(dá)了某個動作杭抠,如將商品放入購物車,提交訂單等恳啥。這一系列動作組合在一起就可以組成完成目標(biāo)所需要執(zhí)行的業(yè)務(wù)邏輯偏灿。在需要調(diào)用這些接口的時候,軟件開發(fā)人員需要向這些接口所在的URL發(fā)送一個請求钝的,從而驅(qū)使服務(wù)執(zhí)行該動作翁垂。

而在REST服務(wù)中,我們所提供的各個接口則需要是一系列資源硝桩,而業(yè)務(wù)邏輯需要通過對資源的操作來完成沿猜。也就是說,REST服務(wù)中的API將不再以執(zhí)行了什么動作為中心亿柑,而是以資源為中心邢疙。一些對資源的通用操作有添加,取得望薄,修改疟游,刪除,以及對符合特定條件的資源進(jìn)行列表操作痕支。

仍然讓我們以上面所舉的“將商品放入購物車”這個操作為例颁虐。在一個REST系統(tǒng)中,購物車將被抽象為一個資源卧须,而“將商品放入購物車”這個操作將被解釋為對購物車這個資源的更新:更新購物車另绩,以使特定商品包含在購物車內(nèi)。

可能對于剛剛學(xué)習(xí)REST的各位讀者而言花嘶,這種以資源為中心的描述方法有些別扭笋籽。這種描述方法的確有別于很多Web服務(wù)那樣以動作為中心。而與之對應(yīng)的則是系統(tǒng)設(shè)計步驟的改變:我們將不再首先是別完成業(yè)務(wù)邏輯所需的各動作椭员,而是支持業(yè)務(wù)邏輯所需要的各資源车海。那么我們應(yīng)該如何抽象出這些資源呢?首先隘击,我們對某個操作不要再關(guān)注它所執(zhí)行的動作侍芝,而是關(guān)心它所操作的賓語。通常情況下埋同,該賓語就會是REST系統(tǒng)中的資源州叠。

在這里,我們就以“提交訂單”作為示例來展示如何抽象資源凶赁。

首先咧栗,在“提交訂單”這個動作中逆甜,訂單是賓語。因此對于該業(yè)務(wù)邏輯楼熄,其將作為一個資源存在忆绰。除此之外,在訂單中還需要包含一系列信息可岂,例如訂單中所包含的商品,訂單所屬人等翰灾。一旦這些都可以被該REST系統(tǒng)中的其它資源使用缕粹,那么它們也將成為獨立的資源。

但是有時候纸淮,一個動作可能并不存在著它所操作的賓語平斩。在這種情況下,我們就需要考慮該動作產(chǎn)生或消除了哪個實體咽块,或者哪個實體的狀態(tài)發(fā)生了變化绘面。這個發(fā)生了變化的實體實際上就是一種資源。例如對于登陸這一行為侈沪,其實際上在服務(wù)端創(chuàng)建了一個會話實例揭璃。該會話實例中則包含了登陸IP,登陸時間亭罪,以及登陸時所用的憑證等瘦馍。再比如對于用戶更改密碼這種行為,其所操作的資源就是用戶資料应役。

在抽象資源的過程中情组,我們需要按照自頂向下的方式,即首先辨識出系統(tǒng)中的最主要資源箩祥,然后再辨識這些主要資源的子資源院崇,并依次進(jìn)行迭代。

對主資源的抽取主要通過分析業(yè)務(wù)邏輯來完成袍祖。在得到功能需求以后底瓣,我們首先要分析這些業(yè)務(wù)邏輯所操作的賓語。這些賓語可能有兩種情況:主資源或者其它資源的子資源盲泛。主資源實際上就是能夠獨立存在的一系列資源濒持。而子資源則需要依附于主資源之上才能表達(dá)實際的意義。同時各個子資源也可能擁有自身的子資源寺滚。

判斷一個資源是否是子資源的一個方法就是看它是否能獨立地表示其具體含義柑营。例如對于一個egoods上所銷售的商品,其名稱村视,價格官套,簡介等屬性可以清晰地描述該商品到底是什么,到底如何銷售。因此這些商品實際上是一個主資源奶赔。但是每種商品所支持的郵遞服務(wù)需要是一個子資源:一個商品可以支持多種郵遞服務(wù)惋嚎。這些郵遞服務(wù)根據(jù)派送距離等需要不同的價格,也提供了不同的郵遞速度站刑。由于這些郵遞服務(wù)與商家和郵遞服務(wù)公司所達(dá)成的服務(wù)價格有關(guān)另伍,并且會由于商品重量的變化而變化,因此這些郵遞服務(wù)并不能為其它商家所提供的郵遞服務(wù)作為參考绞旅,因此其應(yīng)該作為該商品的一個子資源摆尝。

或者也可以說,如果一個資源是主資源因悲,那么其可以被不同的資源實例包含引用而不會產(chǎn)生歧義堕汞。而如果一個資源是子資源,那么被不同的資源實例引用可能會產(chǎn)生歧義晃琳。

但是需要注意的是讯检,一種資源可能有多種不同的表現(xiàn)形式。例如對于在使用列表展示各個商品的時候卫旱,egoods只需要展示商品的名稱人灼,一個對該商品的簡單描述,商品的價格以及一張商品的照片誊涯。而在用戶打開了該商品頁之后挡毅,頁面則需要顯示更詳盡的信息,如商品的重量暴构,商品所在地等等跪呈。

除此之外,資源列表也有可能擁有多種不同的表現(xiàn)形式取逾。舉例來說耗绿,如果egoods上屬于某個分類的商品太多,需要分頁顯示砾隅,那么這種分頁是否也應(yīng)該是一種資源误阻?答案是,這些分頁并不是一種資源晴埂,而其只是資源列表的一種表現(xiàn)方式究反。在每頁所包含商品數(shù)量,排序規(guī)則等條件發(fā)生變化的時候儒洛,該資源列表中所包含的各個商品也會發(fā)生變化精耐。

那么如何判斷我們?yōu)镽EST服務(wù)所定義的資源是否合理呢?一般情況下琅锻,我都使用下面的一些判斷方法:

首先卦停,我們需要考慮對該資源的CRUD是否有意義向胡,從而驗證資源的定義是否合理。就以剛剛說到的列表的分頁顯示為例惊完,我們可以想象一下如何對分頁進(jìn)行添加和刪除僵芹?一旦刪除了該分頁,那么屬于該分頁中的各個商品也應(yīng)該被刪除么小槐?而且刪除了分頁X的數(shù)據(jù)后拇派,原本X + 1分頁的數(shù)據(jù)將展示在X分頁中。很顯然凿跳,將商品的分頁定義為資源并不合理攀痊。

其次,我們需要檢查資源是否需要除CRUD之外的動詞來操作拄显。該方法用來檢查資源中是否還有子資源沒有被抽象。如果該資源還需要額外的動詞案站,那么我們就需要考慮這些操作到底引起了什么樣的狀態(tài)變化躬审,進(jìn)而抽象出該資源的子資源。

除此之外,我們還需要檢查這些資源是否是被整體使用,創(chuàng)建和刪除润努。該方法用來探測是否一個子資源應(yīng)該是一個主資源野哭。如果在刪除一個資源的時候,其子資源還可以被其它資源重用麻削,那么該子資源實際上具有較高的重用性,應(yīng)該是一個主資源。

資源的URL設(shè)計

在前面已經(jīng)提到過富岳,統(tǒng)一接口約束中的第一條子約束就是每個資源都擁有一個資源標(biāo)識。在正確地辨識出了一個資源之后拯腮,我們就需要為這些資源分配其所對應(yīng)的URI窖式。一個資源所對應(yīng)的URI可能有多種表示方式,如到底是用單數(shù)還是復(fù)數(shù)表示資源等动壤。因此在一個基于HTTP的REST系統(tǒng)中萝喘,如何組織針對各個資源的URL實際上是最重要的一部分。畢竟一個明確的琼懊,有意義并且穩(wěn)定的API接口實際上是對服務(wù)對用戶的一種承諾阁簸。

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

協(xié)議哼丈。即HTTP以及HTTPS启妹。

主機名和端口。如www.egoods.com:8421

資源的相對路徑削祈。如/api/categories翅溺。

請求參數(shù)脑漫。即由問號開始的由鍵值對組成的字符串:?page=1&page_size=20

在為一個資源設(shè)計其所對應(yīng)的URL時,我們需要著重考慮第三部分和第四部分組成咙崎。

通過URL來表示資源

在辨識出了REST系統(tǒng)中的各個資源以后优幸,我們就需要開始為這些資源設(shè)計各自所對應(yīng)的URL了。

首先要介紹的是褪猛,所有的資源都應(yīng)該存在于一個相對路徑之下网杆。請讀者回憶之前我們介紹的通過向/api發(fā)送一個GET請求得到所有可以被訪問的資源這個示例:

1GET /api2Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json56HTTP/1.1200OK7Content-Type: application/json8Content-Length: xxx910{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ā)送請求才能得到的各個主資源來說,將它們置于相對路徑/api之下是非常合理的伊滋。

除了這個原因之外碳却,API的版本更迭也是一個考慮。假如軟件開發(fā)人員需要開發(fā)一個新版本的REST API笑旺,那么他可能就需要重新抽象并定義系統(tǒng)中的各個資源昼浦。但是如果兩個版本的API中都擁有一個categories資源,并且系統(tǒng)為了保持后向兼容性同時保留了兩個版本的API筒主,那么將只有一個資源可以使用/categories這個相對路徑关噪。也正因為如此,將這些資源置于相對路徑/api之下乌妙,并在第二個版本的API出現(xiàn)之后將新的資源抽象置于/api-v2下是一種較為流行的做法使兔。

在明確了所有的資源都應(yīng)該置于/api這樣一個相對路徑下之后,我們就來講解如何為資源定義對應(yīng)的URL藤韵。一個最簡單的情況是:指定主資源所對應(yīng)的URL虐沥。由于主資源是一類獨立的資源,因此它應(yīng)該直接置于/api下泽艘。例如egoods網(wǎng)站中的產(chǎn)品分類就是一個主資源欲险,我們會為其分配如下URL:

1/api/categories

而對于其它主資源,如egoods網(wǎng)站中的產(chǎn)品悉盆,我們也會為其賦予一個具有類似結(jié)構(gòu)的URL:

1/api/items

這樣盯荤,每類主資源都將擁有一個特定于該類資源的URL。這些URL就對應(yīng)著相應(yīng)資源實例的集合焕盟。

如果需要表示某個主資源類型中的特定實例秋秤,那么我們就需要在該類主資源所對應(yīng)的URL之后添加該實例的ID。如egoods網(wǎng)站中的食品分類的ID為1脚翘,那么其所對應(yīng)的URL就將是:

1/api/categories/1

一個較為特殊的情況則是灼卢,對于某種類型的主資源,整個系統(tǒng)將有且僅有一個該類型資源的實例来农。那么該資源將不再需要通過ID來訪問鞋真。我能想到的一個例子就是對整個系統(tǒng)進(jìn)行介紹的資源。該資源實例所對應(yīng)的URL將是:

1/api/about

而一個資源實例中還可能擁有子資源沃于。這些子資源與資源實例之間的關(guān)系主要有兩種情況:資源實例包含了一個子資源的集合涩咖,以及資源實例僅僅可以包含一個子資源海诲。對于資源實例包含了一個子資源集合的情況,我們需要將該子資源集合的URL置于該資源的相對路徑下檩互。例如對于egoods上所銷售的ID為23456的商品所提供的郵遞服務(wù)特幔,我們將使用如下的URL:

1/api/items/23456/shipments

在該URI中,/api/items/23456對應(yīng)的就是商品本身闸昨,而該商品所提供的郵遞服務(wù)則是該商品的子資源蚯斯。與主資源特定實例所具有的URI類似,其中一個ID為87256的郵遞服務(wù)所對應(yīng)的URI則為:

1/api/items/23456/shipments/87256

如果資源實例僅僅可以包含一個子資源饵较,那么對該子資源的訪問也將不再需要ID拍嵌。如當(dāng)前商品的折扣信息:

1/api/items/23456/discount

單數(shù)?vs. 復(fù)數(shù)

接下來要考慮的一點是,資源在URL中需要由單數(shù)表示還是復(fù)數(shù)表示循诉?這在stackoverflow等眾多論壇上已經(jīng)成為了一個經(jīng)久不衰的話題横辆。我們知道,在一個基于HTTP的REST系統(tǒng)中茄猫,一個資源所對應(yīng)的URL實際上也就是對其進(jìn)行操作的URL龄糊。因此適當(dāng)?shù)厥褂脝螖?shù)和復(fù)數(shù)對于該系統(tǒng)的用戶而言有一定的指示作用。在stackoverflow上的一個常見觀點是:如果一個URL所對應(yīng)的資源是使用復(fù)數(shù)表示的募疮,那么該類型的資源可能有多個。對該URL發(fā)送Get請求可能返回該資源的一個列表僻弹。反之阿浓,如果一個URL所對應(yīng)的資源是使用單數(shù)表示的,那么該類型的資源將只有一個蹋绽,因此對該URL發(fā)送Get請求將只返回該資源的一個實例芭毙。

以egoods中的商品分類為例。由于一個網(wǎng)站所售賣的商品可能有多種類別卸耘,因此其需要在URL中使用復(fù)數(shù)形式:/api/categories退敦。而對于一個該網(wǎng)站的用戶而言,由于其只會有一個個人偏好設(shè)置蚣抗,因此其URL則需要使用單數(shù)形式:/api/users/{user_id}/preference侈百。

你可能會問:如果需要得到具有特定ID的某個實例時,我們應(yīng)該對該資源使用單數(shù)還是復(fù)數(shù)呢翰铡?答案是復(fù)數(shù)钝域。這是因為在通過特定ID訪問某個資源的實例實際上就是從該資源的集合中取出特定實例。因此表示該資源集合的URL實際上仍然需要使用復(fù)數(shù)形式锭魔,而其后所使用的ID則標(biāo)明了其所訪問的是資源中的單一實例例证,因此向這個URL發(fā)送Get請求將返回該資源的單一實例。

就以“食品”分類為例迷捧。該分類所對應(yīng)的URL為/api/categories/1织咧。該URL中的前半部分/api/categories表示egoods網(wǎng)站中所有分類的集合胀葱,而1則表示在該分類集合中的ID為1的分類。

相對路徑 vs. 請求參數(shù)

另一個經(jīng)常導(dǎo)致疑惑的地方就是針對資源的某一種特征笙蒙,我們到底是將其定義為URL中相對路徑的一部分還是作為請求參數(shù)抵屿。

請考慮下面一個例子。在egoods網(wǎng)站中手趣,我們售賣的手機主要有蘋果晌该,三星等品牌。那么在為這些手機設(shè)計URL的時候绿渣,我們是否需要按照品牌對這些手機進(jìn)行細(xì)分朝群,從而用戶只要通過向/api/mobiles/brands/apple發(fā)送請求就能列出所有的蘋果手機?還是說中符,直接將手機的品牌置于請求參數(shù)中姜胖,從而通過/api/mobiles?brand=apple來列出所有的蘋果手機?

在判斷到底是使用請求參數(shù)還是相對路徑時淀散,我們一般分為下面幾步右莱。

首先,可選參數(shù)一般都應(yīng)置于請求參數(shù)中档插。仍以egoods中的手機為例慢蜓。在選擇手機時,用戶可以選擇品牌以及顏色郭膛。如果將品牌和顏色都定義在相對URL中晨抡,那么具有特定品牌和顏色的手機將可以通過兩個不同的URL訪問:/api/mobiles/brand/{brand}/color/{color}以及/api/mobiles/color/{color}/brand/{brand}。就用戶而言则剃,其并無法了解這兩個URL所表示的是同一類資源還是不同類型的資源耘柱。當(dāng)然,您可以說棍现,我們只用/api/mobiles/brand/{brand}/color/{color}调煎。但是該URL將無法處理用戶僅僅選擇了顏色,卻沒有選擇品牌的情況己肮。

其次士袄,不是所有字符都可以在URL中被使用,如漢字谎僻,標(biāo)點窖剑。為了處理這種情況,包含這些字符的篩選條件需要置于請求參數(shù)中戈稿。

最后西土,如果該特征下包含子資源,那么它自身也就是一個資源鞍盗,因此需要以相對路徑的方式展現(xiàn)它需了。例如在egoods網(wǎng)站中跳昼,每件商品所屬于的分類僅僅是它的一個特征。但是一個分類更包含了屬于它的各個品牌以及熱搜關(guān)鍵字等眾多信息肋乍。因此它其實是一個資源鹅颊,需要在URI路徑中表示它。

總的來說墓造,既然使用HTTP來構(gòu)建REST系統(tǒng)堪伍,那么我們就需要遵守URL各組成中的含義:URL中的相對路徑將用來標(biāo)示“What I want”,也既對應(yīng)著資源觅闽;而請求參數(shù)則用來標(biāo)示“How I want”帝雇,即查看資源的方式。

使用合適的動詞

在知道了如何為每種資源定義URI之后蛉拙,我們來看看如何操作這些資源尸闸。

首先,在一個資源的生命周期之內(nèi)常常會發(fā)生一系列通用事件(CRUD)孕锄。一開始吮廉,一個資源并不存在。只有用戶或REST服務(wù)創(chuàng)建了該資源以后其才存在畸肆,也即是上面所列出的通用事件中的C宦芦,Create。在一個資源創(chuàng)建完畢以后轴脐,用戶可能會從服務(wù)端請求該資源的表示踪旷,也就是上面所列出的通用事件的R,Retrieve豁辉。在特定情況下,用戶可能決定要更新該資源舀患,因此會使用上面的通用事件中的U徽级,即Update來更新資源。而在資源不再需要的時候聊浅,用戶可能需要通過通用事件D餐抢,即Delete來刪除該資源。同時用戶有時也需要列出屬于特定類型資源的資源實例低匙,即通過List操作來得到屬于特定類型的資源的列表旷痕。

在前面的講解中我們已經(jīng)提到過,在REST系統(tǒng)中的每個資源都有一個特定的URI與之對應(yīng)顽冶。HTTP協(xié)議提供了多種在URI上操作的動詞欺抗,如GET,PUT强重,POST以及DELETE等绞呈。因此在一個基于HTTP的REST服務(wù)中贸人,我們需要使用這些HTTP動詞來表示如何對這些資源進(jìn)行CRUD操作。而在什么情況下到底使用哪個動詞則是由這些動詞本身在HTTP協(xié)議中的意義所決定的佃声。

這其中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.

也就是說艺智,在需要讀取某個資源的時候,我們向該資源所對應(yīng)的URI發(fā)送一個GET請求即可圾亏。類似的夭问,在需要刪除一個資源的時候埠胖,我們只需要向該資源所對應(yīng)的URI發(fā)送一個DELETE請求即可谋竖。而在希望得到某類型資源的列表的時候韧骗,我們可以直接向該類型資源所對應(yīng)的URI發(fā)送一個GET請求政模。

而動詞PUT和POST則是較為容易混淆的兩個動詞纽什。在HTTP規(guī)范中让蕾,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動詞會在目標(biāo)URI之下創(chuàng)建一個新的子資源。例如在向服務(wù)端發(fā)送下面的請求時,REST系統(tǒng)將創(chuàng)建一個新的分類:

1POST /api/categories2Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json56{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則是根據(jù)請求創(chuàng)建或修改特定位置的資源奄容。此時向服務(wù)端發(fā)送的請求的目標(biāo)URI需要包含所處理資源的ID:

1POST /api/categories/8fa866a1-735a-4a56-b69c-d7e79896015e2Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json56{7"label":"Electronics",8……9}

可以看到,兩者都有創(chuàng)建的含義归斤,但是意義卻不同。在決定到底是使用PUT還是POST來創(chuàng)建資源的時候呛讲,軟件開發(fā)人員需要考慮一系列問題:

首先就是資源的ID是如何生成的返奉。如果希望客戶端在創(chuàng)建資源的時候顯式地指定該資源的ID贝搁,那么就需要使用PUT。而在由服務(wù)端為該資源自動賦予ID的時候衡瓶,我們就需要在創(chuàng)建資源時使用POST徘公。在決定使用PUT創(chuàng)建資源的時候,防止資源URI與其它資源所具有的URI重復(fù)的任務(wù)需要由客戶端來保證哮针。在這種情況下关面,客戶端常常使用GUID/UUID作為將資源的ID。但是到底使用GUID/UUID還是由服務(wù)端來生成ID不僅僅和REST有關(guān)十厢,更會對數(shù)據(jù)庫性能等多個方面產(chǎn)生影響等太。因此在決定使用它們之前要仔細(xì)地考慮清楚。

同時需要注意的是蛮放,因為REST要求客戶只可以通過服務(wù)端返回結(jié)果中所包含的信息來得到下一步操作所需要的信息缩抡,因此客戶端僅僅可以決定資源的ID,而URI中的其它部分則需要從之前得到的響應(yīng)中取得包颁。

但是軟件開發(fā)人員常常會進(jìn)入另外一個誤區(qū)很多人認(rèn)為REST服務(wù)中的HATEOAS只能通過Hyperlink完成瞻想。實際上在Roy對REST的定義中使用的是Hypermedia,即響應(yīng)中的所有多媒體信息娩嚼。就像Roy在其個人網(wǎng)站上所說(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系統(tǒng)的設(shè)計有所幫助蘑险。由于在同一個URI上調(diào)用兩次PUT所得到的結(jié)果相同。因此用戶在沒有接到PUT請求響應(yīng)時可以放心地重復(fù)發(fā)送該響應(yīng)岳悟。這在網(wǎng)絡(luò)丟包較為嚴(yán)重時是一個非常好的功能佃迄。反過來泼差,在同一個URI上調(diào)用兩次POST將可能創(chuàng)建兩個獨立的子資源。

除此之外呵俏,還需要考慮是否將資源的創(chuàng)建和更新歸結(jié)為一個API可以簡化用戶對REST服務(wù)的使用堆缘。用戶可以通過PUT動詞來同時完成創(chuàng)建和更新一個資源這兩種不同的任務(wù)。這樣的好處在于簡化了REST服務(wù)所提供的接口普碎,但是反過來也讓一個API執(zhí)行了兩種不同的任務(wù)吼肥,在一定程度上違反了API設(shè)計時每個API都需要有明確的意義這一原則。

因此在決定到底使用POST還是PUT來完成資源的創(chuàng)建之前随常,請考慮上面所列出的三條問題潜沦,以確定到底哪個動詞更加適合。

除此之外绪氛,另外一對類似的動詞則是PUT和PATCH唆鸡。兩者之間的不同則在于PUT是對整個資源的更新,而PATCH則是對部分資源的更新枣察。而該動詞的局限性則在于對該動詞的支持程度争占。畢竟在某些類庫中并沒有提供原生的對PATCH動詞的支持。

使用標(biāo)準(zhǔn)的狀態(tài)碼

在與REST服務(wù)進(jìn)行交互的時候序目,用戶需要通過服務(wù)所返回的信息決定其所發(fā)送的請求是否被適當(dāng)?shù)靥幚肀酆邸_@部分功能是由REST服務(wù)實現(xiàn)時所使用的協(xié)議所決定的,與REST架構(gòu)無關(guān)猿涨。而在基于HTTP的REST服務(wù)中握童,該功能就由HTTP響應(yīng)的狀態(tài)碼(Status Code)來完成。因此在設(shè)計一個REST服務(wù)時叛赚,我們需要額外地注意是否返回了正確的狀態(tài)碼澡绩。

但是這些預(yù)定義的HTTP狀態(tài)碼并不能滿足所有的情況。有時候一個REST服務(wù)所希望返回的錯誤信息能夠更加精確地描述問題俺附,例如在用戶重設(shè)密碼時肥卡,我們需要在用戶所輸入原密碼與系統(tǒng)中所記錄的密碼不匹配時返回“您所輸入的密碼有誤”這樣的消息。在HTTP協(xié)議中事镣,我們并沒有辦法找到一個能夠精確地表示該意義的狀態(tài)碼步鉴。

因此在通常情況下,REST服務(wù)都會在響應(yīng)中額外地提供一個說明性的負(fù)載來告知用戶到底產(chǎn)生了什么問題璃哟。例如對于上面的重設(shè)密碼失敗的情況氛琢,服務(wù)端可能會返回如下響應(yīng):

1HTTP/1.1400Bad Request2Content-Type: application/json3Content-Length: xxx45{6"error_id":"100045",7"header":"Reset password failed",8"description":"The original password is not correct"9}

上面的示例響應(yīng)中主要包含以下的說明性信息:

服務(wù)端響應(yīng)的狀態(tài)碼。頁面邏輯可以通過判斷該狀態(tài)碼是否是4XX或5XX來判斷是否請求出錯随闪,從而在頁面中展示一個警告對話框阳似。

服務(wù)所提供的內(nèi)部錯誤ID。通常情況下蕴掏,該內(nèi)部錯誤ID也需要在警告對話框中展示出來障般。從而允許軟件用戶根據(jù)內(nèi)部錯誤ID來獲取支持服務(wù)。

錯誤的標(biāo)題及簡述盛杰。通過該錯誤的標(biāo)題及簡述挽荡,軟件用戶能夠了解系統(tǒng)內(nèi)部到底發(fā)生了什么,并在是用戶輸入錯誤的時候允許用戶自行修改錯誤并重新發(fā)送正確的請求即供。

在該錯誤中定拟,最關(guān)鍵的當(dāng)屬服務(wù)端的響應(yīng)代碼。一個響應(yīng)代碼不僅僅標(biāo)示了請求是否成功逗嫡,更有用戶該如何操作的含義青自。例如對于401 Unauthorized響應(yīng)代碼而言,其表示該響應(yīng)沒有提供一個合法的身份憑證驱证,因此需要用戶首先執(zhí)行登陸操作以得到一個合法的身份憑證延窜,然后該資源可能就可以被訪問了。而403 Forbidden響應(yīng)代碼則表示當(dāng)前請求已經(jīng)提供了一個合法的身份憑證抹锄,但是該身份憑證并沒有訪問該資源的權(quán)限逆瑞,因此使用該身份憑證登陸重新登陸系統(tǒng)等操作并不能解決問題。

因此在返回錯誤信息之前伙单,軟件開發(fā)人員首先需要考慮清楚在響應(yīng)中到底應(yīng)該使用什么樣的響應(yīng)代碼获高。而正確地選擇響應(yīng)代碼則建立在軟件開發(fā)人員對這些響應(yīng)代碼擁有一個正確的理解的前提下。

當(dāng)然吻育,要將所有的響應(yīng)代碼完全理解也需要大量的工作念秧,而且REST服務(wù)的用戶也可能并沒有那么多的領(lǐng)域知識來了解所有的響應(yīng)代碼的含義。因此在很多基于HTTP的REST系統(tǒng)中布疼,系統(tǒng)在標(biāo)示錯誤時只使用一系列常用的響應(yīng)代碼摊趾,如400,401缎除,403严就,404,405器罐,500梢为,503等。在用戶請求被處理時轰坊,系統(tǒng)將返回200 OK铸董,表示請求已經(jīng)被處理。而在處理時發(fā)生錯誤時則盡量使用這些響應(yīng)代碼來表示肴沫。如果一個錯誤較為復(fù)雜粟害,那么直接返回400或500,并在響應(yīng)的負(fù)載中提供具體的錯誤信息颤芬。

不得不說的是悲幅,這種做法有時顯得簡單粗暴套鹅,尤其是對于一個開放平臺而言則更是致命的。當(dāng)一個第三方廠商為一個開放平臺開發(fā)一個應(yīng)用軟件汰具,卻每次只能得到一個400錯誤卓鹿,那么其內(nèi)部應(yīng)用邏輯將無法判斷到底是哪里出了問題。為了能讓用戶知道這里產(chǎn)生了錯誤留荔,該第三方軟件只能將開放平臺所給出的信息直接顯示給用戶吟孙。但是這些信息實際上是建立在開放平臺這個語境下的,因此對于第三方廠商的用戶而言聚蝶,這些信息晦澀難懂杰妓,甚至可能一點幫助也沒有。

也就是說碘勉,到底如何組織這些響應(yīng)代碼需要用戶根據(jù)所編寫的項目決定巷挥,尤其是該產(chǎn)品的使用者來決定。在定義一個平臺時验靡,盡量使用更多的HTTP響應(yīng)代碼句各,因為用戶極有可能通過該平臺編寫自己的第三方軟件。而在為一個普通的產(chǎn)品定義REST API時晴叨,將響應(yīng)代碼定得非常專業(yè)可能反而導(dǎo)致易用性的下降凿宾。

另外一點需要說明的是,個人不建議使用Wikipedia查找各個狀態(tài)碼的含義兼蕊,而應(yīng)該使用RFC所描述的各狀態(tài)碼的定義初厚。 IANA提供了一張各個狀態(tài)碼所對應(yīng)的RFC協(xié)議的列表,從而可以很容易地找到各個狀態(tài)碼所對應(yīng)的RFC協(xié)議以及其所在的章節(jié)孙技。該列表的地址為:http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

之所以不建議使用Wikipedia的原因主要有兩點:

描述不夠詳細(xì)产禾。在RFC定義中,每個狀態(tài)碼都對應(yīng)著一段或多段文字牵啦,并且解釋非常清晰亚情。而在Wikipedia中,每個狀態(tài)碼常常只有一句話哈雏。

不夠準(zhǔn)確楞件。在Wikipedia的Reference節(jié)中,我們可以看到一系列特定平臺所定義的狀態(tài)碼裳瘪,如Spring Framework所定義的420 Method Failure等土浸。這非常具有誤導(dǎo)性。

選擇適當(dāng)?shù)谋硎窘Y(jié)構(gòu)

接下來我們要講解的就是如何為資源定義一個恰當(dāng)?shù)谋硎尽?/p>

首先需要強調(diào)的是彭羹,REST并沒有規(guī)定其服務(wù)中需要使用什么格式來表示資源黄伊。表示資源時所可以選取的表示形式實際上是由實現(xiàn)REST所使用的協(xié)議決定的。而在一個基于HTTP的REST服務(wù)中派殷,我們可以使用JSON还最,也可以使用XML墓阀,甚至是自定義的MIME類型來表示資源。這些表現(xiàn)形式常常是等效的拓轻。相信讀者已經(jīng)看到岂津,本系列文章會使用JSON來表示這些資源。

一個REST服務(wù)常常會同時支持多種客戶端悦即。這些客戶端可能會使用不同的協(xié)議來與服務(wù)進(jìn)行溝通。而且就算是使用相同的協(xié)議橱乱,不同的客戶端所可以接受的負(fù)載表示形式也會有所不同辜梳。因此客戶端需要與REST服務(wù)協(xié)商在通訊過程中所使用的負(fù)載。

客戶端和服務(wù)端對所使用負(fù)載類型的協(xié)商通常都按照協(xié)議所規(guī)定的標(biāo)準(zhǔn)協(xié)商過程來完成泳叠。例如對于一個基于HTTP的REST服務(wù)作瞄,我們就需要使用Accept頭來標(biāo)示客戶端所可以接受的負(fù)載類型:

1GET /api/categories2Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/json

而在服務(wù)端支持的情況下,返回的響應(yīng)就將使用該MIME類型組織其負(fù)載:

1HTTP/1.1200OK2Content-Type: application/json3Content-Length: xxx

在這里我們再重復(fù)一次:REST是一種組織Web服務(wù)的架構(gòu)危纫,其只在架構(gòu)方面提出了一系列約束宗挥。可以說种蝶,所有對REST的講解都已經(jīng)在前兩個章節(jié)契耿,即“REST的定義”以及“資源識別”中完成了。而有關(guān)客戶端和服務(wù)端如何進(jìn)行溝通螃征,為資源定義什么樣的URI搪桂,使用什么格式的數(shù)據(jù)進(jìn)行溝通等討論都是在闡述如何將REST架構(gòu)所提出的各種約束和基于HTTP協(xié)議的Web服務(wù)結(jié)合在一起。畢竟在通常情況下盯滚,實現(xiàn)一個單純的技術(shù)不難踢械,但是如何將多種技術(shù)規(guī)范自然地混合在一起,構(gòu)成一個自然的魄藕,成熟穩(wěn)定的解決方案才是項目開發(fā)中的難點内列。HTTP協(xié)議并不是為REST架構(gòu)所定義的,因此如何用HTTP協(xié)議來恰當(dāng)?shù)孛枋鲆粋€REST服務(wù)才是本文所著重介紹的背率。

負(fù)載的自描述性

在前面對REST提出的幾個約束的講解中我們已經(jīng)提到過话瞧,REST系統(tǒng)中所傳遞的各個消息的負(fù)載需要提供足夠的用于操作該資源的信息,如如何對資源進(jìn)行添加寝姿,刪除以及修改等操作移稳,并可以根據(jù)負(fù)載中所包含的對其它各資源的引用來訪問各個資源。這也對負(fù)載的自描述性提出了更高的要求会油。

首先讓我們回頭看看egoods電子商務(wù)網(wǎng)站對食品分類的描述:

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}

我想讀者在看到該響應(yīng)之后可能就已經(jīng)明白了很多域的含義个粱。但還是讓我們依次對這些域進(jìn)行講解。

第一個要講解的是url域翻翩。該域用來標(biāo)示該資源所對應(yīng)的URL都许〉巨保可能您會問:既然我們就是從這個URL返回的該資源,那么為什么我們還需要在該資源中保存一個它所對應(yīng)的URL呢胶征?首先這是因為在統(tǒng)一接口約束中要求每個資源都擁有一個資源標(biāo)識塞椎。在這里我們使用URL作為標(biāo)識芒篷。而另一些基于HTTP的REST系統(tǒng)中巫俺,用來作為資源標(biāo)識的常常是該資源的ID。個人更傾向于使用URL的原因則是:在某些情況下卷雕,如對某個資源定時刷新以進(jìn)行監(jiān)控的時候钱雷,URL可以直接被使用骂铁。

接下來是label域。其用來記錄用于展示給用戶的分類名罩抗。

items_url域則用來表示取得屬于該分類物品列表的URL拉庵。注意這里我使用了后綴_url以明確標(biāo)明其是一個URL,需要通過跳轉(zhuǎn)來取得實際的數(shù)據(jù)套蒂。

下一個域brands則用來表示屬于該分類的著名商品品牌钞支。這里我們使用了一個數(shù)組,而數(shù)組中的每個元素都表示了一個品牌操刀。每個品牌的表示都包含了一個展示給用戶的label烁挟,在搜索時所使用的鍵,以及該品牌所對應(yīng)的url骨坑。您可能會懷疑為什么我們僅僅提供了這么少的域信夫。這是因為他們僅僅是對這個品牌的引用,而并非是把該資源的詳細(xì)信息都包含進(jìn)來了的緣故卡啰。在用戶希望查看該品牌的詳細(xì)信息的時候静稻,他需要向該品牌引用中所標(biāo)明的品牌的URL發(fā)送一個GET請求。

而由于hot_searches域的組成及使用基本上與brands域類似匈辱,因此這里不再贅述振湾。

在大致地了解了食品分類的JSON表示中各個域的含義后,我們就將開始講解如何自行定義資源的JSON表示亡脸。對于一個簡單的押搪,不包含任何子資源以及對其它資源的引用的資源,我們只需要通過一個包含簡單屬性的JSON來表示它浅碾。例如對于一個品牌大州,我們可能僅僅提供了一系列描述性信息:品牌的名稱,以及對品牌的簡單描述垂谢。那么它所對應(yīng)的JSON表示可以表示為:

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

而在另一個資源中厦画,可能包含了對其它資源的引用。在這種情況下,我們就需要在表示對其它資源進(jìn)行引用的域中通過URL來標(biāo)明被引用資源的位置根暑。例如一件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系統(tǒng)中,我們常常在資源的引用中包含一定量的描述信息淳地。這主要因為兩點:

提高性能怖糊。在一個對資源的引用中添加了用于顯示的屬性后,客戶端頁面可以避免再次通過url發(fā)送請求得到資源的具體描述颇象,以得到用于顯示的信息伍伤。

自描述性的要求。如果一個資源中包含了一個對其它資源進(jìn)行引用的數(shù)組夯到,那么用戶就需要通過該標(biāo)簽來決定到底訪問哪個被引用的資源。

當(dāng)然饮亏,如果需要在展示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}

當(dāng)然,如果一個資源的表示太過復(fù)雜路幸,而且有些屬性實際上是相互關(guān)聯(lián)的荐开,那么我們也可以通過一個屬性將它們歸結(jié)在一起:

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來表示所有的營養(yǎng)成分简肴,而該域內(nèi)部的各個子域則用來表示一系列相關(guān)的營養(yǎng)成分所占比例晃听。

另外,在不同的情況下砰识,我們還可能對同一個資源提供不同的表現(xiàn)形式能扒。例如在一個資源極為復(fù)雜,其JSON表示甚至可以達(dá)到幾百K的時候辫狼,我們可以為該資源提供一個簡化版本初斑,以在非必要的情況下減少傳輸?shù)臄?shù)據(jù)量。

例如在egoods中膨处,我們會將某些物美價廉的商品置于它的首頁上见秤,以吸引用戶購買。在用戶將鼠標(biāo)移動到某個商品上并停留一段時間時真椿,我們會為用戶展示一個Tooltip鹃答,并在該Tooltip中展示該商品的一部分信息。在這種情況下突硝,向服務(wù)端請求該商品的所有信息以展示Tooltip便顯得有些效率低下了测摔。

有時候,一個資源可能并不支持特定用戶執(zhí)行某個操作解恰。例如一個管理員所創(chuàng)建的資源可能對普通用戶只讀避咆。在這種情況下舟肉,我們需要禁止普通用戶對該資源的修改和刪除。為了能明確地告知用戶他所具有的權(quán)限查库,我們需要一個能顯式地標(biāo)示用戶可以在一個資源上所執(zhí)行操作的組成路媚。在REST響應(yīng)中,這種組成被稱為Hypermedia Controls樊销。例如對于一個普通用戶整慎,其從egoods中所返回的分類列表將如下所示:

1HTTP/1.1200OK2Content-Type: application/json3Content-Length: xxx45[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域顯式地標(biāo)示了用戶可以在各個類別上所能執(zhí)行的操作裤园。而對于管理員,其還可以執(zhí)行修改剂府,刪除等操作:

1HTTP/1.1200OK2Content-Type: application/json3Content-Length: xxx45[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系統(tǒng)中拧揽,如Sun Cloud API,其更是通過Hypermedia Controls定義了除CRUD之外的動詞腺占。如對于一個虛擬機淤袜,其在運行狀態(tài)下可以執(zhí)行停止命令,而在停止?fàn)顟B(tài)下可以執(zhí)行啟動命令:

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之外的額外的動詞衰伯,那么這種需求常常表示我們對于某個資源的定義并不是十分合理铡羡。因此在遇到這種情況時,軟件開發(fā)人員首先需要考慮為資源添加額外的動詞是否合適意鲸。

無狀態(tài)約束

在Roy Fielding的論文中烦周,其為REST添加了一個無狀態(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.

從上面的陳述中可以看到,在一個REST系統(tǒng)中怎顾,用戶的狀態(tài)會隨著請求在客戶端和服務(wù)端之間來回傳遞读慎。這也便是REST這個縮寫中ST(State Transfer)的來歷。

為REST系統(tǒng)添加這個約束有什么好處呢槐雾?主要還是基于集群擴展性的考慮贪壳。如果REST服務(wù)中記錄了用戶相關(guān)的狀態(tài),那么在集群中蚜退,這些用戶相關(guān)的狀態(tài)就需要及時地在集群中的各個服務(wù)器之間同步闰靴。對用戶狀態(tài)的同步將會是一個非常棘手的問題:當(dāng)一個用戶的相關(guān)狀態(tài)在一個服務(wù)器上發(fā)生了更改,那么在什么時候钻注,什么情況下對這些狀態(tài)進(jìn)行同步蚂且?如果該狀態(tài)同步是同步進(jìn)行的,那么同時刷新多個服務(wù)器上的用戶狀態(tài)將導(dǎo)致對用戶請求的處理變得異常緩慢幅恋。如果該同步是異步的杏死,那么用戶在發(fā)送下一個請求時,其它服務(wù)器將可能由于用戶狀態(tài)不同步的原因無法正確地處理用戶的請求。除此之外淑翼,如果集群進(jìn)行了不停機的橫向擴展腐巢,那么用戶狀態(tài)的同步需要如何完成?這些實際上都是非常難以處理的問題玄括。

但是現(xiàn)有的很多較為流行的技術(shù)及規(guī)范實際上都沒有限制用戶的請求是無狀態(tài)的冯丙。相信您知道,一個技術(shù)或規(guī)范實際上都擁有一個生態(tài)圈遭京。在該生態(tài)圈之內(nèi)的各技術(shù)之間可以較好地契合在一起胃惜。尤其是,有些技術(shù)實際上就會以該生態(tài)圈中的核心技術(shù)或規(guī)范所建立的假設(shè)之上來實現(xiàn)自己的功能哪雕。如果希望禁止該假設(shè)船殉,那么讓某些技術(shù)工作起來就是非常困難的事情了。

就以搭建基于HTTP的REST服務(wù)為例斯嚎。在HTTP中利虫,一個重要的功能就是Cookie和Session的使用(RFC6265)。該功能會在服務(wù)器里保留一個狀態(tài)堡僻。因此在一個基于HTTP的REST系統(tǒng)中糠惫,我們常常需要避免使用這些在服務(wù)器里面保留狀態(tài)的技術(shù)。但是某些技術(shù)苦始,如用戶的登陸寞钥,實際上常常需要在服務(wù)器中添加一個狀態(tài)慌申。

所以在stackoverflow中陌选,我們常常會看到有人問:我現(xiàn)在使用了這樣一種解決方案。這樣實現(xiàn)是不是RESTful蹄溉?此時一些人就會說咨油,這不是RESTful。但是pure RESTful和almost RESTful之間的區(qū)別主要還是在于一個是理論柒爵,一個是工程役电。在工程中,輕微地違反了一個準(zhǔn)則并不一定代表這個解決方案一無是處棉胀。而是要看遵守該準(zhǔn)則和輕微地違反了該準(zhǔn)則之后工作量的大小以及后期的維護(hù)成本:之所以提出一系列準(zhǔn)則法瑟,那是因為遵守該準(zhǔn)則擁有一定的好處。如果對該準(zhǔn)則的輕微違反可以減少大量的工作量唁奢,而且遵守準(zhǔn)則的好處并沒有消失霎挟,或者是通過另一樣技術(shù)可以快速地重新獲得該好處,那么對準(zhǔn)則的輕微違反是值得的麻掸。

Authentication

其實在上一節(jié)中酥夭,我們已經(jīng)提出了無狀態(tài)約束給REST實現(xiàn)帶來的麻煩:用戶的狀態(tài)是需要全部保存在客戶端的。當(dāng)用戶需要執(zhí)行某個操作的時候,其需要將所有的執(zhí)行該請求所需要的信息添加到請求中熬北。該請求將可能被REST服務(wù)集群中的任意服務(wù)器處理疙描,而不需要擔(dān)心該服務(wù)器中是否存有用戶相關(guān)的狀態(tài)。

但是在現(xiàn)有的各種基于HTTP的Web服務(wù)中讶隐,我們常常使用會話來管理用戶狀態(tài)起胰,至少是用戶的登陸狀態(tài)。因此整份,REST系統(tǒng)的無狀態(tài)約束實際上并不是一個對傳統(tǒng)用戶登錄功能友好的約束:在傳統(tǒng)登陸過程中待错,其本身就是通過用戶所提供的用戶名和密碼等在服務(wù)端創(chuàng)建一個用戶的登陸狀態(tài),而REST的無狀態(tài)約束為了橫向擴展性卻不想要這種狀態(tài)烈评。而這也就是為基于HTTP的REST服務(wù)添加身份驗證功能的困難之處火俄。

為了解決該問題,最為經(jīng)典也最符合REST規(guī)范的實現(xiàn)是在每次發(fā)送請求的時候都將用戶的用戶名和密碼都發(fā)送給服務(wù)器讲冠。而服務(wù)器將根據(jù)請求中的用戶名和密碼調(diào)用登陸服務(wù)瓜客,以從該服務(wù)中得到用戶所對應(yīng)的Identity和其所具有的權(quán)限。接下來竿开,在REST服務(wù)中根據(jù)用戶的權(quán)限來訪問資源谱仪。

這里有一個問題就是登陸的性能。隨著系統(tǒng)當(dāng)前的加密算法越來越復(fù)雜否彩,登陸已經(jīng)不再是一個輕量級的操作疯攒。因此用戶所發(fā)送的每次請求都要求一次登陸對于整個系統(tǒng)而言就是一個巨大的瓶頸。

在當(dāng)前列荔,解決該問題的方法主要是一個獨立的緩存系統(tǒng)敬尺,如整個集群唯一的登陸服務(wù)器。但是緩存系統(tǒng)本身所存儲的仍然是用戶的登陸狀態(tài)贴浙。因此該解決方案將仍然輕微地違反了REST的無狀態(tài)約束砂吞。

還有一個類似的方法是通過添加一個代理來完成的。該代理會完成用戶的登陸并獲得該用戶所擁有的權(quán)限崎溃。接下來蜻直,該代理會將與狀態(tài)有關(guān)的信息從請求中刪除,并添加用戶的權(quán)限信息袁串。在經(jīng)過了這種處理之后概而,這些請求就可以轉(zhuǎn)發(fā)到其后的各個服務(wù)器上了。轉(zhuǎn)發(fā)目的地所在的服務(wù)器則會假設(shè)所有傳入的請求都是合法的并直接對這些請求進(jìn)行處理囱修。

可以看到赎瑰,無論是一個獨立的登陸服務(wù)器還是為整個集群添加一個代理,系統(tǒng)中都將有一個地方保留了用戶的登陸狀態(tài)蔚袍。這實際上和在集群中對會話集中進(jìn)行管理并沒有什么不同乡范。也就是說配名,我們所嘗試的通過禁止使用會話來達(dá)成完全的無狀態(tài)并不現(xiàn)實。因此在一個基于HTTP的REST服務(wù)中晋辆,為登陸功能使用集中管理的會話是合理的渠脉。

既然我們放松了對REST系統(tǒng)的無狀態(tài)約束,那么一個REST系統(tǒng)所可以使用的登陸機制將主要分為以下兩種:

1.?? 基于HTTPS的Basic Access Authentication

其好處是其易于實現(xiàn)瓶佳,而且主流的瀏覽器都提供了對該功能的支持芋膘。但是由于登陸窗口都是由瀏覽器所提供的,因此其與產(chǎn)品外觀有很大不同霸饲。除此之外为朋,瀏覽器都沒有提供登出的功能,也沒有提供找回密碼等功能厚脉。

2.?? 基于Cookie及Session的管理

在使用Cookie來管理用戶的注冊狀態(tài)的時候习寸,其實際上就是將服務(wù)端所返回的Cookie在每次發(fā)送請求的時候添加到請求中。雖然說這個Cookie并非存儲了用戶應(yīng)用的狀態(tài)傻工,但是其實際存儲了用戶的登陸狀態(tài)霞溪。因此客戶端的角度來講,由服務(wù)端管理的Session并不符合REST所倡導(dǎo)的無狀態(tài)的要求中捆。

可以說鸯匹,上面的兩種方法各有優(yōu)劣⌒刮保可能第二種方法從客戶端的角度看來并不是RESTful的殴蓬,但是其優(yōu)勢則在于很多類庫都直接提供了對該功能的支持,從而簡化了會話管理服務(wù)器的實現(xiàn)蟋滴。

在這里順便提一句染厅,如果項目足夠大,將一些SSO產(chǎn)品集成到服務(wù)中也是不錯的選擇脓杉。

版本管理

在前面已經(jīng)提到過糟秘,一個REST系統(tǒng)為資源所抽象出的URI實際上是對用戶的一種承諾简逮。但反過來說球散,軟件開發(fā)人員也很難預(yù)知一個資源的各方面特征如何在未來發(fā)生變化,從而提供一個永遠(yuǎn)不變的URI散庶。

在一個REST系統(tǒng)逐漸發(fā)展的過程中蕉堰,新的屬性,新的資源將逐漸被添加到該系統(tǒng)中悲龟。在這些更改過程中屋讶,資源的URI,訪問資源的動詞须教,響應(yīng)中的Status Code將不能發(fā)生變化皿渗。此時軟件開發(fā)人員所做的工作就是在現(xiàn)有系統(tǒng)上維護(hù)REST API的后向兼容性斩芭。

當(dāng)資源發(fā)生了過多的變化,原有的URI設(shè)計已經(jīng)很難兼容現(xiàn)有資源應(yīng)有的定義時乐疆,軟件開發(fā)人員就需要考慮是否應(yīng)該提供一個新版本的REST API划乖。那么我們該如何對資源的版本進(jìn)行管理呢?

首先要考慮的就是挤土,新API的版本信息是否應(yīng)當(dāng)包含在資源的URI中琴庵。這在各著名論壇中仍然是一個爭議較大的話題。一種觀點認(rèn)為在不同版本的API中仰美,一個資源擁有不同的地址在一定程度上違反了HATEOAS:URI只是用來指定一個資源所在的位置迷殿,而不是該資源如何被抽象。如果一個資源由不同的URI標(biāo)示其不同的表現(xiàn)形式咖杂,那么用戶將無法通過一個響應(yīng)中所標(biāo)示的URI得到其它URI所指向的表示形式庆寺。而且在URI中添加了有關(guān)版本的信息也就標(biāo)示著其可能會隨著時間的推移發(fā)生變化。

一種使用獨立URI的方法是基于Accept頭诉字。在一個請求中止邮,我們常常標(biāo)明了Accept頭,以標(biāo)示客戶端希望得到的表現(xiàn)形式奏窑。在該頭中导披,用戶可以添加所請求的資源的版本信息:

1GET /api/categories/12Host: www.egoods.com3Authorization: Basic xxxxxxxxxxxxxxxxxxx4Accept: application/vnd.ambergarden.egoods-v3+json

而在接收到該請求之后,服務(wù)端將返回該資源的第三個版本:

1HTTP/1.1200OK2Content-Type: application/vnd.ambergarden.egoods-v3+json3Content-Length: xxx45{6"uri":"/api/categories/1",7"label":"Food",8……9}

可以看到埃唯,該方法是非常嚴(yán)格地遵守REST系統(tǒng)所提出的約束的撩匕。但其也并不是沒有缺點:添加一個自定義MIME類型(Custom MIME Type)也是一個很麻煩的流程,而且在很多現(xiàn)有技術(shù)中都沒有很好地支持它墨叛,如HTML5中的Form止毕。因此這種方案的缺點是對REST API用戶并不那么友好。

除此之外漠趁,另一種基于重定向的解決方案也被提出扁凛。該方案允許一個REST系統(tǒng)提供多個版本的API,并在URI中標(biāo)明版本號:

1/api/v2/categories2/api/v1/categories

這樣用戶可以選擇使用特定版本的REST API來實現(xiàn)客戶端功能闯传。由于其使用固定版本的API谨朝,因此并不存在著一個資源有多種表示,進(jìn)而違反了HATEOAS約束的問題甥绿。

在REST系統(tǒng)的API隨時間逐漸發(fā)展出眾多版本的時候字币,系統(tǒng)對API的維護(hù)也將成為一個較大的問題。此時就需要逐漸退役一些年代久遠(yuǎn)的API 版本共缕。對這些版本的退役主要分為兩步:首先將其標(biāo)為過期的洗出,但是還在一段時間內(nèi)支持。在這種情況下图谷,對這些已經(jīng)過期的API的訪問將得到3XX響應(yīng)翩活,如301 Moved Permanently阱洪,以通知用戶該URI所標(biāo)示的資源需要使用新版本的URI進(jìn)行訪問。而再經(jīng)過一段時間后菠镇,則將過期的REST API標(biāo)記為廢棄的澄峰。此時用戶在訪問這些URI時將返回4XX響應(yīng),如410 Gone辟犀。

接下來俏竞,該REST系統(tǒng)還可以提供一個通用的REST API接口,并與最新版本的API保持一致:

1/api/categories

這樣用戶還可以選擇一直使用最新版本的API堂竟,只是同時也需要一直對其進(jìn)行維護(hù)魂毁,以保持與最新版本API的兼容性。在REST系統(tǒng)的API隨著時間的推移逐漸發(fā)生變化的時候出嘹,該客戶端也需要逐漸更新自身的功能席楚。

但是該方法有一個問題:由通用URI所辨識出的各個資源需要是穩(wěn)定的,不能在一定時間之后被廢棄税稼,否則會給用戶帶來非常大的維護(hù)性的麻煩烦秩。舉例來說,假設(shè)客戶端邏輯添加了一系列操作分類的功能郎仆。當(dāng)REST系統(tǒng)決定不再采用分類作為商品歸類的標(biāo)準(zhǔn)只祠,那么客戶端邏輯中與分類相關(guān)的各個功能都需要進(jìn)行大幅度地修改。過于頻繁的這種改動很容易導(dǎo)致用戶對該系統(tǒng)所提供的API失去維護(hù)的信心扰肌。因此在抽象資源時一定要努力地將各個資源的邊界辨識清楚抛寝。雖然說這聽起來很嚇人,但是在經(jīng)過仔細(xì)考慮后這種情況還是較為容易避免的曙旭。

但是反過來說盗舰,理論常常與實際有些脫鉤,更何況REST是在2000年左右提出的桂躏,無法做到能夠預(yù)見到十余年后所使用的各項技術(shù)钻趋。因此在盡量符合REST所提出的各約束上提供一個最直觀的,具有最高易用性的API才是王道剂习。無限制地提供后向兼容性是一個非常困難蛮位,成本非常高的事情。因此在版本管理這一方面上來說进倍,我們也需要盡量兼顧項目需求和完全遵從理論這兩者之間的平衡土至。

而在同一個版本之中购对,我們則需要保證API的后向兼容性猾昆。也就是說,在添加新的資源以及為資源添加新的屬性的時候骡苞,原有的對資源進(jìn)行操作的API也應(yīng)該是工作的垂蜗。

對于一個基于HTTP的REST服務(wù)而言楷扬,軟件開發(fā)人員需要遵守如下的守則以保持API的后向兼容性:

不能在請求中添加新的必須的參數(shù)。

不能更改操作資源的動詞贴见。

不能更改響應(yīng)的HTTP status烘苹。

而前向兼容性則顯得沒有那么重要了。REST服務(wù)的前向兼容性要求現(xiàn)有的服務(wù)兼容未來版本服務(wù)的客戶端片部。但是由于服務(wù)提供商所提供的服務(wù)常常是最新版本镣衡,因此對前向兼容性有要求的情況很少出現(xiàn)。另外一點是档悠,為一個服務(wù)提供前向兼容性其實并不那么容易廊鸥。因為這要求軟件開發(fā)人員對產(chǎn)品的未來方向進(jìn)行非常多的假設(shè),而且這些假設(shè)不能有錯誤辖所。反過來惰说,這種對服務(wù)的前向兼容性的要求主要由客戶端自身通過保持后向兼容性來完成。

性能

接下來我們就來簡單地說說基于HTTP的REST服務(wù)中的性能問題缘回。在基于HTTP的REST服務(wù)中吆视,性能提升主要分為兩個方面:REST架構(gòu)本身在提高性能方面做出的努力,以及基于HTTP協(xié)議的優(yōu)化酥宴。

首先要討論的就是對登陸性能的優(yōu)化啦吧。在前面我們已經(jīng)介紹過,在一個基于HTTP的REST服務(wù)中拙寡,每次都將用戶的用戶名和密碼發(fā)送到服務(wù)端并由服務(wù)端驗證這些信息是否合法是一個非常消耗資源的流程丰滑。因此我們常常需要在登陸服務(wù)中使用一個緩存,或者是使用第三方單點登陸(SSO)類庫倒庵。

除此之外褒墨,軟件開發(fā)人員還可以通過為同一個資源提供不同的表現(xiàn)形式來減少在網(wǎng)絡(luò)上傳輸?shù)臄?shù)據(jù)量,從而提高REST服務(wù)的性能擎宝。

而在集群內(nèi)部服務(wù)之間郁妈,我們則可以不再使用JSON,XML等這種用戶可以讀懂的負(fù)載格式绍申,而是使用二進(jìn)制格式噩咪。這樣可以大大地減少內(nèi)部網(wǎng)絡(luò)所需要傳輸?shù)臄?shù)據(jù)量。這在內(nèi)部網(wǎng)絡(luò)交換數(shù)據(jù)頻繁并且所傳輸?shù)臄?shù)據(jù)量巨大時較為有效极阅。

接下來就是REST系統(tǒng)的橫向擴展胃碾。在REST的無狀態(tài)約束的支持下,我們可以很容易地向REST系統(tǒng)中添加一個新的服務(wù)器筋搏。

除了這些和REST架構(gòu)本身相關(guān)的性能提升之外仆百,我們還可以在如何更高效地使用HTTP協(xié)議上努力。一個最常見的方法就是使用條件請求(Conditional Request)奔脐。簡單地說俄周,我們可以使用如下的HTTP頭來有條件地存取資源:

ETag:一個對用戶不透明的用來標(biāo)示資源實例的哈希值

Data-Modified:資源被更改的時間

If-Modified-Since:根據(jù)資源的更改時間有條件地Get資源吁讨。這將允許客戶端對未更改的資源使用本地緩存。

If-None-Match:根據(jù)ETag的值有條件地Get資源峦朗。

If-Unmodified-Since:根據(jù)資源的更改時間有條件地Put或Delete資源建丧。

If-Match:根據(jù)ETag的值有條件地Put或Delete資源。


http://www.cnblogs.com/loveis715/p/4669091.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末波势,一起剝皮案震驚了整個濱河市翎朱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尺铣,老刑警劉巖闭翩,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異迄埃,居然都是意外死亡疗韵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門侄非,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕉汪,“玉大人,你說我怎么就攤上這事逞怨≌甙蹋” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵叠赦,是天一觀的道長驹马。 經(jīng)常有香客問我,道長除秀,這世上最難降的妖魔是什么糯累? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮册踩,結(jié)果婚禮上泳姐,老公的妹妹穿的比我還像新娘。我一直安慰自己暂吉,他們只是感情好胖秒,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著慕的,像睡著了一般阎肝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肮街,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天风题,我揣著相機與錄音,去河邊找鬼。 笑死俯邓,一個胖子當(dāng)著我的面吹牛骡楼,可吹牛的內(nèi)容都是我干的熔号。 我是一名探鬼主播稽鞭,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼引镊!你這毒婦竟也來了朦蕴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤弟头,失蹤者是張志新(化名)和其女友劉穎吩抓,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赴恨,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡疹娶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了伦连。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雨饺。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惑淳,靈堂內(nèi)的尸體忽然破棺而出额港,到底是詐尸還是另有隱情,我是刑警寧澤歧焦,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布移斩,位于F島的核電站,受9級特大地震影響绢馍,放射性物質(zhì)發(fā)生泄漏向瓷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一舰涌、第九天 我趴在偏房一處隱蔽的房頂上張望风罩。 院中可真熱鬧,春花似錦舵稠、人聲如沸超升。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽室琢。三九已至,卻和暖如春落追,著一層夾襖步出監(jiān)牢的瞬間盈滴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巢钓,地道東北人病苗。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像症汹,于是被迫代替她去往敵國和親硫朦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理背镇,服務(wù)發(fā)現(xiàn)咬展,斷路器,智...
    卡卡羅2017閱讀 134,659評論 18 139
  • REST本身是一個高度抽象化的架構(gòu)風(fēng)格瞒斩,因而總是很難對它有一個比較深入且印象深刻的理解破婆。寫這篇文章的目的,是自己對...
    vito1994閱讀 2,858評論 0 26
  • “媽胸囱,我不去相親祷舀!這個月都相親29個了,今天你就不能讓我休息一下芭氡省裳扯!”徐語書手插著腰,嘟著嘴不滿地說箕宙。儼然一副潑婦...
    幕子兮閱讀 182評論 1 0