Roy Fielding最近這樣說道:
要抓住這類陳述的要領不是件簡單的事。如果服務器不將它自己的名字空間控制在一個固定的資源層次下,客戶端及更重要的客戶端開發(fā)者將如何知道或發(fā)現資源的URI呢?畢竟,長久以來,分布式客戶端/服務器開發(fā)的一個基本假設就是:為了構建、維護和管理這類應用, 我們需要預先對應用的接口正式描述。Roy Fielding的觀點似乎跟這個假設相沖突。 關于描述RESTful系統(tǒng)的討論并非新鮮事物。這類討論幾乎總會得出類似上述的觀點。例如,看看前年infoQ上關于爭論:REST需要描述語言么?的備忘錄,它總結了當時發(fā)生的部分討論。今天的事態(tài)并沒有什么特別的不同。 針對RESTful應用的正式描述語言,雖然有大量的贊成和反對意見,但像WADL這樣的描述語言只得到了有限的發(fā)展。然而,由于缺乏一種機器能夠解釋的“標準”語言,服務器應用所采取的最常用方法就是記錄所有URI、支持的HTTP方法和表示(representation)的結構(如,對應的XML和JSON格式),這樣客戶端應用開發(fā)者就能依賴這種文檔來編寫代碼。 但是,這種方式跟REST的一些基本原則(如Roy Fielding在上面所說的)有沖突。即便我們無視這一異議,對于那些試圖通過HTTP RESTful構建分布式應用的人來說,基本問題仍然存在。不正式地定義契約,服務器怎么可能得以脫身?沒有契約,我們如何能確定正確實現了客戶端和服務器——不僅正確實現了各自的設計規(guī)范,而且恰當地實現了其他業(yè)務/技術策略? 用HTTP作為應用協議、以RESTful方式構建的分布式應用其實有一個契約,但其性質和種類卻不相同。我們需要知道尋找的目標和位置。如果我們打算提出一種描述語言,那么它就要和Roy Fielding所說的保持一致,它不能是類似WSDL或WADL這樣的東西。在這篇文章中,我的目標是回答如下問題:
請讓我從一個示例開始。 示例任務是寫一個客戶端程序,實現同一位客戶在不同銀行賬戶間的轉賬業(yè)務。 首先讓我描述一下客戶端和服務器之間的所有交互,接著看看這個契約的可能描述。 步驟 0:用戶登錄客戶端。為了保持此次討論的重點,請讓我忽視所有安全方面的內容。 步驟 1:客戶端使用URI: 200 OK Content-Type: application/xml;charset=UTF-8 <accounts xmlns="urn:org:bank:accounts"> <account> <id>AZA12093</id> <customer-id>7t676323a</customer-id> <balance currency="USD">993.95</balance> </account> <account> <id>ADK31242</id> <customer-id>7t676323a</customer-id> <balance currency="USD">534.62</balance> </account> </accounts> 我們假設跟名字空間 步驟 2:由于客戶端知道兩個賬戶的ID,在必要情況下,它可以向以下URI提交GET請求以獲取每個賬戶的詳細信息: http:///account/AZA12093 http:///account/ADK31242 就這個示例而言,鑒于客戶端已經擁有發(fā)起賬戶轉賬所需的信息,那么就讓我忽略這些請求。 步驟 3:接著,客戶端通過提交如下POST請求發(fā)起賬戶轉賬: POST /transfers Host: Content-Type: application/xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <from>account:AZA12093</from> <to>account:ADK31242</to> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 服務器獲得賬戶的路由代碼(譯注:由美國銀行家協會在美聯儲監(jiān)管和協助下提出的金融機構識別碼,很多金融機構都有一個,主要用于銀行相關的交易,轉賬,清算等的路由確認,由9位[8位內容+1位驗證碼]組成,主要用于美國及北美地區(qū)。),把轉賬提交給執(zhí)行轉帳的后端系統(tǒng),并返回如下內容: 201 Created Content-Type: application/xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <from>account:AZA12093</from> <to>account:ADK31242</to> <id>transfer:XTA8763</id> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 轉帳并沒有結束。轉賬將在幾個工作日后異步發(fā)生(這對于銀行間交易很平常),客戶端可以使用交易ID查詢交易狀態(tài)。 步驟 4:一天后,客戶端提交GET請求來查詢狀態(tài)。 GET /check/XTA8763 Host: 200 OK Content-Type: application/xml;charset=UTF-8 <status xmlns="urn:org:bank:accounts"> <code>01</code> <message xml:lang="en">Pending</message> </status> 注意,盡管這個實現使用了資源、URI、表示和HTTP的統(tǒng)一接口,但它并非RESTful的。因為我們將在后續(xù)小節(jié)看到,這個示例并沒有利用REST的關鍵約束之一,即“超媒體即應用狀態(tài)引擎”。 在試圖使之RESTful之前,讓我先試著寫一份該示例關聯的可能用戶文檔。
這種風格的文檔在如今很普遍。它包含了客戶端將一直需要使用的所有URI。它描述了客戶端用每個URI可使用的HTTP方法。它還包含了表示的描述,即示例中的XML文檔。 但是這類文檔有兩個問題。首先,它對任何尋找機器可讀正式描述的人并沒有任何幫助。缺少機器可讀的描述,我們就無法構建能用于測試或以其他方式執(zhí)行契約的通用軟件工具。缺乏這類通用軟件工具,對于那些需要部署這類工具來管理和治理他們軟件的人來說,這實在是一個相當大的障礙。你可能會考慮使用WADL,或者甚至是WSDL 2.0來提供一個機器可讀的等價物。 其次,同時也是更重要的,用這種方式描述服務器接口,不論是像WADL或者WSDL 2.0這樣機器可讀的格式,還是人類可讀的格式,都違反了REST的兩個約束。這兩個約束要求(a)消息是自描述的,(b)超媒體為應用狀態(tài)的引擎。怎樣才能做到這些,并且為什么這樣做很重要呢? 回到約束REST的關鍵約束是(a)資源標示,(b)通過表示操控資源,(c)自描述的消息,(d)超媒體即應用狀態(tài)引擎。 在使用HTTP的RESTful應用中,消息利用兩種東西實現了自描述,其一,通過使用無狀態(tài)的統(tǒng)一接口;其二,通過使用HTTP報頭(Header),它描述了消息內容,除此之外還包括HTTP實現相關的各協議方面(如內容協商、針對緩沖的條件請求和優(yōu)化并發(fā)等等)。 通過檢查使用的HTTP方法和請求/響應報頭,像代理或緩存這樣的中間實體就能夠破譯哪部分協議(即HTTP)正在被使用以及它們是如何被使用的。這類自描述信息保證了客戶端和服務器之間的交互是可見的(如,對緩存的使用),可靠的(如檢測局部故障并從中恢復)和可伸縮的。 第四個約束,即“超媒體即應用狀態(tài)引擎”,有兩個用途。第一,它不要求協議(即HTTP)是有狀態(tài)的。第二,它使服務器可以演變(如,通過引入新的URI)并保持了客戶端跟服務器間的松耦合。 服務器要是象前一節(jié)那樣提供表示的描述,它就沒有利用HTTP自描述的特性。在HTTP中,客戶端和服務器使用“媒體類型(media type)”,或者是那些我們在請求/響應報頭中看到的Content-Type頭信息來描述消息內容,而不是XML模式。媒體類型類似于對象的類或者XML元素的模式類型(schema type)。 此外,如果服務器把所有URI都向它的客戶端描述,它就無法獨立演變,而且接口會變得脆弱。URI的任何改變都有可能讓現有客戶端無法正常工作。但是,你怎樣才能在對客戶端需要連接的URI一無所知的情況下編寫客戶端呢? 答案就是使用具有已知關系的鏈接。鏈接是一種間接機制,客戶端可以用它來在運行時發(fā)現URI。一個鏈接至少有兩個屬性——URI和關系。URI指向資源或者資源的表示,而關系則描述了鏈接的類型或種類。一個真正的RESTful服務器應用是通過在其表示中包含預定義關系的鏈接來把 URI傳給客戶端。于是,客戶端可以無需預先了解所有URI,而是在運行時從鏈接中抽取出URI。由此,服務器可以自由地改變URI,或者甚至在相同或者其他提供兼容性行為的服務器上引入新URI。 最后,通過告知客戶端隨后要做的事,服務器在表示中返回的鏈接可能是上下文相關的。 換句話說,鏈接以一種運行時工作流的形式動態(tài)地描述了客戶端和服務器之間的契約。 總而言之,對于RESTful應用來說,契約包含三個不同部分:統(tǒng)一接口、表示的媒體類型和資源的上下文相關鏈接。 聽起來有些像童話?為了實際地展示這種契約,我會重寫上面的示例。 重寫示例步驟 0:同前。 步驟 1:客戶端使用相同的URI——http:///accounts?findby=someparams搜索賬戶。這次,讓服務器返回不同類型的響應。 200 OK Content-Type: application/vnd..account+xml;charset=UTF-8 <accounts xmlns="urn:org:bank:accounts"> <account> <id>AZA12093</id> <link href="http:///account/AZA12093" rel="self"/> <link rel="http:///rel/transfer edit" type="application/vnd..transfer+xml" href="http:///transfers"/> <link rel="http:///rel/customer" type="application/vnd..customer+xml" href="http:///customer/7t676323a"/> <balance currency="USD">993.95</balance> </account> <account> <id>ADK31242</id> <link href="http:///account/ADK31242" rel="self"/> <link rel="http:///rel/transfer" type="application/vnd..customer+xml" href="http:///transfers"/> <link rel="http:///rel/customer" type="application/vnd..customer+xml" href="http:///customer/7t676323a"/> <balance currency="USD">534.62</balance> </account> </accounts> 在這個響應中,請注意Content-Type報頭的值,以及包含URI的鏈接(link)。 步驟 2:如果客戶端希望了解每個賬戶的更多內容,它可以從上述響應的“self”關系的鏈接中抽取出賬戶URI,向這些URI提交GET請求。 步驟 3:為了發(fā)起賬戶轉賬,客戶端從上述兩個賬戶中任選一個,并從具備“http:///rel/transfer”和“edit”關系的鏈接中抽取出URI,向之提交一個POST請求。 POST /transfers Host: Content-Type: application/vnd..transfer+xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <from>account:AZA12093</from> <to>account:ADK31242</to> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 同樣請注意Content-Type報頭的值。 發(fā)起賬戶轉賬之后,服務器返回如下內容: 201 Created Content-Type: application/vnd..transfer+xml;charset=UTF-8 <transfer xmlns="urn:org:bank:accounts"> <link rel="self" href="http:///transfer/XTA8763"/> <link rel="http:///rel/transfer/from" type="application/vnd..account+xml" href="http:///account/AZA12093"/> <link rel="http:///rel/transfer/to" type="application/vnd..account+xml" href="http:///account/ADK31242"/> <link rel="http:///rel/transfer/status" type="application/vnd..status+xml" href="http:///check/XTA8763"/> <id>transfer:XTA8763</id> <amount currency="USD">100</amount> <note>RESTing</note> </transfer> 步驟 4:要想查詢賬戶轉賬的狀態(tài),客戶端可以從關系為“http:///check/XTA8763”的鏈接中抽取URI,并向它提交一個GET請求。 這個實現是RESTful的,因為它使用了包含上下文相關鏈接的表示來封裝交互狀態(tài),即利用了“超媒體即應用狀態(tài)引擎”這條約束。 現在,讓我回顧并強調實現這種新交互集合所需的信息。首先,客戶端需要知道查詢賬戶的URI。接著,它需要知道各種鏈接關系的名字和語義。它還需要知道每個媒體類型的細節(jié)。它可以在運行時動態(tài)算出契約的剩余部分。因而,我們可以提供如下修訂后的文檔。
不同類型的描述我在上節(jié)所采用的描述RESTful應用的方法不僅具有某些有趣的特性,亦有些古怪。 對于那些熟悉WSDL和WADL的人來說,上節(jié)的描述可能看起來有些不合常理。我們在其中并未看到關于每個操作輸入和輸出消息的描述,而看到了媒體類型。但是,鑒于像 此外,文檔并沒有列出應用正在使用的所有的URI,而僅僅包含了賬戶轉賬客戶端需要發(fā)起交互的一個URI。注意,在不同的示例中,我們或許需要記錄 多個URI。其思想是保證預發(fā)布URI的數量最小。為什么這樣更好?原因在于,它解耦了客戶端和資源的實際URI,客戶端直到運行時才需要知道其余的 URI。 最后,上述文檔沒有包括每個URI上可用的HTTP操作。相反,我假定客戶端會向每個URI都提交一個HTTP OPTIONS以發(fā)現各種可能的操作,接著使用HTTP GET獲取資源的表示,使用HTTP POST在資源集合內創(chuàng)建一個新資源,使用HTTP PUT更新現有資源(或者如果客戶端可以為資源分配URI,就創(chuàng)建一個),使用HTTP DELETE刪除資源。 總而言之,要以RESTful方式描述契約必須:
這種描述既不完整,也不是完全機器可讀的。 說它不是完整的,是因為它僅僅包含了契約的靜態(tài)部分,讓服務器在運行時通過鏈接描述可能的工作流。 對于那些已經對REST好處深信不疑并且使用HTTP積極構建RESTful應用的人來說,缺乏完整的機器可讀描述可能無關緊要。 但是對于那些正在使用類RPC方法(使用SOAP、WSDL和WS-*)構建分布式應用以及正在考慮REST的人來說,缺乏完整機器可讀的描述可能就是個障礙了。然而,使用RESTful的機器可讀描述可做的工作量,即使存在的話,其作用也有限。這歸結于如下原因:
同樣需要注意,為遠程接口描述一個完全機器可讀的描述契約是一種謬論。用WSDL或WADL創(chuàng)建的機器可讀描述僅能描述結構和語法,而不能描述語義。但機器可讀的描述有時能降低我們作為程序員、測試員和管理員需要做的工作量。 要是我們把統(tǒng)一接口和契約的動態(tài)方面擱置一邊,我們可以用機器可讀方式描述契約的剩余部分。以下就有一個示例。注意,在這個描述中,我的意圖只是想幫助那些要監(jiān)測或測試客戶端/服務器端交互的工具和框架,當然不是要模仿WSDL或WADL。 <description xmlns:bank="urn:org:bank:accounts"> <types> <!-- Include the schema used for all representations --> <include href="bank-schema.rng"/> </types> <!-- List all media types and the corresponding XML types --> <media-types> <media-type> <name>application/vnd..accounts+xml</name> <representation>bank:account</representation> </media-type> <media-type> <name>application/vnd..transfer+xml</name> <representation>bank:transfer</representation> </media-type> ... </media-types> <relations> <relation> <documentation>This relation ...</documentation> <name>http:///rel/transfer</name> </relation> ... </relations> <resources> <resource> <name>accounts</name> <media-type-ref>application/vnd..accounts+xml</media-type-ref> <uri> <!-- This is optional --> <base>http:///accounts</base> <params> <param> <documentation>Use this parameter to ...</documentation> <name>findBy</name> </param> </params> </uri> </resource> <resource> <name>transfer</name> <media-type-ref>application/vnd..transfer+xml</media-type-ref> </resource> ... </resources> </description> 這是我在前節(jié)所描述的契約的機器可讀版本,很明顯,它并不符合任何標準。這個描述并沒有消除對于人類可讀描述的需要,因為我們仍然需要描述應用語義。 讓我強調一下這個描述中的關鍵部分:
這種描述比人類可讀的描述更有用嗎?由于缺乏可解釋這種描述的工具和框架,答案可能是否定的。 這種方法實用嗎?如果你正在編寫基于機器可讀契約(如WADL文檔)的服務器端代碼和客戶端代碼,編碼流程可能如下:
這個模型對以RESTful方式描述契約并不適用,步驟會有所不同:
我關注的大多數軟件框架都可以處理部分上述步驟(如通用接口或資源類的約定),而且還能生成創(chuàng)建或解析XML的類(這取決于你所選的編程語言)。但 是剩余部分就留給了開發(fā)者。更有甚者,這類框架多數強調服務器端編程,并在假設現有HTTP客戶端庫已經足夠使用的情況下忽略了對客戶端編程的考慮。因 而,在處理上述(4)和(5)項時,可能需要創(chuàng)建自定義代碼。 對于那些想要測試或者增強契約的軟件工具怎么辦?創(chuàng)建這種工具,讓其在運行時讀取上述機器可讀描述以完成如下工作,是可行的。
我還沒有聽說哪個軟件能以這種方式來完成以上驗證。但是,出現的機會很大。如果你讀到文章的這里,你就會明白那些機會是什么。 結論我寫這篇文章的一個目的是要闡述這樣的事實:像WSDL和WADL這樣的傳統(tǒng)契約描述并不適合描述RESTful應用。正如我在賬戶轉賬示例中所示 范的,只有部分契約能被靜態(tài)地描述,其余都是動態(tài)并上下文相關的??蛻舳丝梢酝ㄟ^在運行時查看鏈接來遵循契約的動態(tài)部分。你可以出于設計時和測試的目的試 著用某些機器可讀文檔來描述前一部分,但是讓服務器在運行時描述其余部分會大大降低客戶端和服務器之間的耦合。試圖靜態(tài)地描述完整契約無異于會使所有上下 文相關的鏈接在表示之外重復一遍。 相反的,諸如WSDL和WADL這樣的描述語言試圖用上下文無關的方式描述契約,并把用戶文檔留給客戶端開發(fā)者,以便他們能夠學習如何從那些描述中描述的各類消息交互模式合成客戶端應用。在RESTful應用中,服務器在運行時以鏈接形式提供這個信息。 總之,RESTful是有契約的。我們只需要知道如何找到并在哪兒找到該契約,同時謹記該契約是上下文相關的,就行了。 關于作者Subbu在Yahoo工作。通過他的博客可以了解關于他的更多信息。 查看英文原文:Describing RESTful Applications。 |
|