轉 http://www.infoq.com/cn/articles/rest-anti-patterns
人們在試驗REST時,通常會四處尋找樣例——而他們往往不僅能找到一大堆自稱“符合REST”或標榜為“REST API”的樣例,還會發現許多關于某個自稱符合REST的特定服務名不副實的討論。
為什么會這樣?HTTP雖不是什么新事物,但人們使用它的方式卻五花八門。其中有些做法符合Web設計者的初衷,但許多并非如此。要為你的HTTP
應用(無論是面向人類、還是計算機、或同時面向這兩者使用的)應用REST原則,意味著你要恰好反過來:盡量“正確地”使用Web,或者說按符合REST
的方式使用Web(倘若你不喜歡用對或錯來評判的話)。對許多人來說,這的確是一種嶄新的方式方法。
我經常在文章里作同樣的聲明:REST、Web和HTTP是不同的事物;REST可以用多種不同技術來實現,而HTTP只是一種恰好符合REST架
構風格的具體架構。所以,其實我應該小心區分“REST”與“REST式HTTP”這兩個概念的。但我沒有這么做,在本文剩余部分,我們姑且認為它們是相
同的事物。
跟任何新的方式方法一樣,發掘一些共同的模式是有益的。在本系列的第一和第二篇
文章中,我已經講述了一些基礎——比如集合資源的概念、將計算結果轉換為資源本身、以及用聚合(syndication)來模仿事件。后續文章將進一步講
述這些及其他模式。不過在本文中,我想主要說說反模式(anti-patterns)——即那些力求符合REST式HTTP、但未能成功而造成問題的典型
做法。
首先我們來看看我發掘了哪些反模式:
- 全部采用GET
- 全部采用POST
- 忽視緩存
- 忽視響應代碼
- 誤用cookies
- 忘記超媒體
- 忽視MIME類型
- 破壞自描述性
下面我們來逐個詳細說明。
全部采用GET
在許多人看來,REST僅僅意味著用HTTP暴露一些應用功能。HTTP GET是最重要的基本操作(operation
)(嚴格地講,用“動詞(verb)”或“方法(method)”這樣的術語比較好)。GET方法應當用于獲取由URI標識的資源的一個表示
(representation),而許多(即便談不上所有)現有的HTTP庫和服務器編程API不是將URI視為一種資源標識符(resource
identifier),而是將之視為一種傳遞參數的便利手段。這導致了以下這種URIs的出現:
http://example.com/some-api?method=deleteCustomer&id=1234
實際上,你無法根據構成URI的字符獲知關于給定系統的“REST性(RESTfulness)”的任何信息,不過對于上面那個URI,我們可以判
斷該
GET操作不是“安全的(safe)”——也就是說,調用者很可能要為結果(刪除一個客戶)負責,盡管規范里說在這種情況下使用GET方法是錯誤的。
這種做法唯一有利的方面在于它編程起來容易,而且在瀏覽器中調試也簡單——你只要把URI粘貼到瀏覽器地址欄里、然后調整一些“參數”就行了。這種反模式主要存在以下問題:
- URI沒有被用作資源標識符,而是被用于傳遞操作及其參數了。
- HTTP方法(HTTP method)不一定跟語義相符。
- 這種鏈接一般不可加入書簽。
- 有“爬蟲”造成非預期副作用的風險。
注意:符合這一反模式的APIs沒準最終碰巧符合REST原則。這里有個例子:
http://example.com/some-api?method=findCustomer&id=1234
這個URI是標識操作及其參數呢,還是標識一個資源呢?兩種情況都有可能:它可以是一個完全合法的、可加入書簽的URI;對它做GET操作也許是“
安全的
”;它也許會根據Accept報頭返回不同的格式,并支持復雜的緩存機制。在很多情況下,這將是偶然的。API經常在剛開始時采用這種方式來暴露一個“讀
”接口,但當開發者要增添“寫”功能時就有問題了(因為你無法通過對上述URI做PUT操作來更新一個客戶——開發者得構造另一個URI)。
全部采用POST
這一反模式跟前一個頗為相似,只不過這里用的是POST方法而已。POST除了攜帶一個URI,還攜帶一個實體主體(entity
body)。一個典型的場景是:將單個URI作為POST請求的目標、通過發送不同的消息來表達不同的意圖。實際上,SOAP 1.1
Web服務就是這樣做的,它把HTTP當作一種“傳輸協議”來用。服務器根據SOAP消息(可能還包括一些WS-Addressing
SOAP報頭)決定做什么。
可能有人認為“全部采用POST”跟“全部采用GET”存在的問題完全一樣,只是它更難用一些,而且不能利用緩存(甚至連偶爾的機會都沒有),且無法支持書簽。事實上,它并不是違反了哪條REST原則,而是根本忽視了REST原則。
忽視緩存
即使你按各個動詞的原本意圖來使用它們,你仍可以輕易禁止緩存機制。最簡單的做法就是在你的HTTP響應里增加這樣一個報頭:
Cache-control: no-cache
這樣可以禁止緩存機制發揮作用。當然,這也許正是你想要做的,然而通常這只是你的Web框架規定的一個缺省設置。不過,對高效的緩存與再驗證
(caching and re-validation)的支持,是采用REST式HTTP的諸多關鍵優點之一。Sam
Ruby表示,在判斷是否符合REST原則時的一個關鍵問題就是“你支持ETag嗎”?
(ETag是HTTP
1.1里引入的一種機制,它允許客戶端通過加密的校驗和來驗證一個被緩存的表示是否仍然有效)。要生成正確的報頭,最簡單的做法就是把這個任務交給一個“
知道”怎樣做的基礎設施——例如通過在Web服務器(比如Apache HTTPD)的目錄里生成一個文件。
當然,這也要涉及到客戶端一方:你在為一個REST式服務實現程序客戶端時,你應充分利用現有的緩存機制,以免每次都重新獲取表示。例如,服務器也
許已經發出信息:初次返回的表示在600秒內都可被認為是“新的”(比方說因為后端系統每30分鐘才輪詢一次)。這樣的話,短時間內重復請求同一信息就完
全沒必要了。在客戶端設置一個代理緩存(比如Squid)也許比自行構建相應邏輯更好。
HTTP的緩存機制強大而復雜;Mark Nottingham的《緩存指南(Cache Tutorial)》是一個很好的指南。
忽視響應代碼
HTTP提供了一組豐富的應用級狀態代碼,
它們可用于應付不同場合,不過許多Web開發者對此并不知曉。大部分人對200(“OK”)、404(“Not
found”)和500(“Internal server
error”)這些狀態代碼是比較熟悉的。但除此以外還有很多其他狀態代碼,正確使用這些狀態代碼意味著客戶端與服務器可以在一個具備較豐富語義的層次上
進行溝通。
例如,201(“Created”)響應代碼表明已經創建了一個新的資源,其URI在Location響應報頭里。409(“Conflict”)
告訴客戶端存在沖突,比如隨PUT請求發送的是基于老版本資源的數據。再如,412(“Precondition
Failed”)表明服務器不能滿足客戶端的預期。
正確使用狀態代碼的另一方面涉及客戶端:應該根據一種統一的總體方法對不同類別的狀態代碼(例如所有2xx段代碼、所有5xx段代碼)作不同處理——例如,即便客戶端不具備處理特定代碼的邏輯,但至少應把所有2xx段代碼視為成功信號。
許多聲稱符合REST的應用僅僅返回200或500,甚至只返回200(并在響應實體主體里給出錯誤文本——SOAP就是這樣的)。你要是愿意,可
以稱之為“通過狀態代碼200傳達錯誤”,但無論你覺得采用哪個術語好,假如你不利用HTTP狀態代碼豐富的應用語義,那么你將錯失提高重用性、增強互操
作性和提升松耦合性的機會。
誤用cookies
利用cookies來傳播某個服務端會話狀態的鍵(key)是另一種REST反模式。
Cookies表明肯定哪個地方不符合REST了。是這樣嗎?不;不一定。REST的關鍵思想之一是無狀態性(statelessness)——不
是說一個服務器不能保存任何數據:倘若是資源狀態(resource state)或客戶端狀態(client
state),那是可以的。服務器不能保存的是會話狀態(session
state),因為那會造成可伸縮性、可靠性及耦合方面的問題。Cookies的最典型的用法是:保存一個跟“某個保存在服務端內存里的數據結構”相關聯
的鍵(key)。這意味著,瀏覽器隨各次請求發出去的cookie是被用于構建會話狀態的。
如果一個cookie被用于保存一些“服務器不依賴于會話狀態即可驗證”的信息(比如認證令牌),那么這樣的cookies是完全符合REST原則
的——
不過有一點需要注意:如果有其他更為標準的方式來傳遞一則信息(比如放在URI里、放在某個標準報頭里、或較少見地放在消息主體里),那就不應將之放在
cookie里。例如,按REST式HTTP的觀點來使用HTTP認證就比較好。
忘記超媒體
最不易接受的REST思想就是標準的方法集合。REST理論并沒有規定標準集合由哪些方法組成,它只是規定必須有一組適用于所有資源的方法集合。對
于
HTTP來說,這組集合是GET、PUT、POST和DELETE(至少起初是這樣),你需要一定適應時間才能掌握如何將所有應用語義投射到這四個動詞
上。但你一旦適應了,就可以開始運用這個REST的子集——一種基于Web的CRUD(Create、Read、Update、
Delete)架構——了。暴露這種反模式的應用不是真正的“非REST式”應用(假如存在這種事物的話),它們只是未能利用一個REST核心概念——“
超媒體即應用狀態引擎(hypermedia as the engine of application state)”。
超媒體(hypermedia)是一個把事物鏈接起來的概念,正是它造就了Web這個網——一個互聯的資源集合,應用通過跟隨鏈接從一個狀態進入另一個狀態。這聽上去也許有點深奧,不過其實遵從這一原則是有正當理由的。
“忘記超媒體”反模式的首要表現就是:表示(representation)里缺少鏈接。盡管通常客戶端可以根據一定的規則來構造URI,但是因為
服務器沒有發送任何鏈接,所以客戶端將無法跟隨鏈接。一種較好的做法是:即支持構造URI,又支持跟隨鏈接——這里的鏈接通常反映了下層數據模型中的關
系。但最好的情況是:客戶端應該只需知道一個URI;其他URI(各個URI及其構造模式,如:各種查詢字符串)應該通過超媒體(作為資源表示里的鏈接)
來傳達。 Atom發布協議(Atom Publishing Protocol)就是一個好例子,它有一個服務文檔(service
documents)的概念,服務文檔為它所描述的域內的各個集合提供具名元素(named
elements)。最后,應用可能經歷的狀態遷移應該是動態傳播的,客戶端應該可以不用掌握多少知識就可以跟隨它們。HTML就是一個好榜樣,它包含足
夠的信息,以便瀏覽器可以向用戶提供一個完全動態的接口。
我本想增加一個“人類可讀的URI”反模式的。但我沒那么做,因為我跟其他人一樣也喜歡可讀的、好“篡改”的URI。但是當人們采用REST時,他
們經常浪費許多時間來討論“正確的”URI設計,而忘記了超媒體方面。所以,我建議你不要花太多時間來尋找正確的URI設計(畢竟,它們只是字符串而
已),而是多花一些精力在表示里尋找提供鏈接的正確地方。
忽視MIME類型
HTTP有個內容協商(content
negotiation)的概念,它允許客戶端根據需要獲取資源的不同表示(representations)。例如,一個資源也許有不同格式的表示(如
XML、JSON或YAML等)以便于用各種不同語言(如Java、JavaScript及Ruby)實現的消費者所使用。再如,一個資源可能即有面向人
類的PDF或JPEG版表示,又有“機器可讀的”XML版表示。還有,一個資源可能同時支持v1.1版和v1.2版的自定義表示格式。不管怎樣,也許可以
為“只有一個表示格式”找到理由,但這常常意味著丟掉某種機會。
顯然,若一個服務能為更多未預見到的客戶端所用(或重用)那更好。因此,依靠現有、預定義、廣為人知的格式,要好過發明私有格式——這會導致本文講述的最后一個反模式。
破壞自描述性
這種反模式是如此普遍,以至于幾乎在每個、甚至那些由所謂的“REST狂熱者們”(包括我在內)創建的REST應用里都可以看到:違反自描述性約束
(這一努力目標并不像人們最初想象的那樣跟人工智能科幻小說有多大牽連)。理想情況下,一個消息(HTTP
請求或HTTP響應,包括報頭與主體)應該包含足夠信息,以便任何通用客戶端、服務器或媒介(intermediary)能夠處理它。例如,當你的瀏覽器
獲取某個受保護資源的PDF表示(representation)時,你可以看到由標準達成的協定是如何起作用的:有些HTTP認證交換發生,可能會發生
一些緩存(caching)和/或再驗證(revalidation),服務器發送的content-type報頭(application/pdf)觸發了你系統里注冊的PDF閱讀器,最后你得以在自己的屏幕上閱讀該PDF。所有用戶都可以用他們自己的基礎設施來執行同樣的請求。若服務器開發者另外增加一種內容類型,那么服務器的客戶端(或服務的消費者)只需確保他們安裝了正確的閱讀器即可。
你要是發明自己的報頭、格式或協議,那就一定程度上破壞了自描述性約束。極端地講,所有沒有被某個標準化組織官方標準化的東西都違反此約束,因而可
被認為符合本反模式。在實踐中,你應努力做到盡可能遵循標準,并懂得“某些協定可能只在一個較小的領域(比方說,你的服務和客戶端是專門針對它開發的)中
適用” 的道理。
總結
自從“四人組(Gang of Four)”出版了書籍、
掀起模式運動的開端以來,許多人誤解了它,并試圖在盡可能多的場合下應用模式——這已被其他人所取笑。模式應當僅在符合上下文時才被應用。同樣地,可能有
人會不遺余力地在所有場合下虔誠地努力避免所有反模式。許多時候,你有充分理由違反某一規則,或者按REST的術語放松某一約束。這么做是沒問題的——但
了解實際情況、作出知情決策是有益的。
但愿本文能有助于你在開始首個REST項目時避免落入這些常見的陷阱。
非常感謝Javier Botana和Burkhard Neppert對本文初稿的反饋。
Stefan Tilkov是InfoQ SOA社區的首席編輯,以及位于德國/瑞士的innoQ公司的合伙人、首席顧問和主要的REST狂熱主義者。