rest

rest

一種軟件架構風格
REST即表述性狀态傳遞(英文:Representational State Transfer,簡稱REST)是Roy Fielding博士在2000年他的博士論文中提出來的一種軟件架構風格。它是一種針對網絡應用的設計和開發方式,可以降低開發的複雜性,提高系統的可伸縮性。目前在三種主流的Web服務實現方案中,因為REST模式的Web服務與複雜的SOAP和XML-RPC對比來講明顯的更加簡潔,越來越多的web服務開始采用REST風格設計和實現。例如,Amazon.com提供接近REST風格的Web服務進行圖書查找;雅虎提供的Web服務也是REST風格的。
  • 中文名:表述性狀态轉移
  • 外文名:Representational State Transfer
  • 别名:
  • 外語縮寫:REST
  • 外文縮寫:rest
  • 提出時間:2000年

基本原則

大部分對REST的介紹是以其正式的定義和背景作為開場的。這裡提出一個簡要的定義:REST定義了Web的使用标準(這和大多數人的實際使用方式有很大不同),例如HTTP和URI。如果你在設計應用程序時能堅持REST原則,那就預示着你将會得到一個使用了優質Web架構(這将讓你受益)的系統。

REST并非始終是正确的選擇。它作為一種設計Web服務的方法而變得流行,這種方法對專有中間件(例如某個應用程序服務器)的依賴比基于SOAP和WSDL的方法更少。在某種意義上,通過強調URI和HTTP等早期Internet标準,REST是對大型應用程序服務器時代之前的Web方式的回歸。正如您已經在所謂的基于REST的接口設計原則中研究過的一樣,XML over HTTP是一個功能強大的接口,允許内部應用程序(例如基于Asynchronous JavaScript+XML(Ajax)的自定義用戶界面)輕松連接、定位和使用資源。事實上,Ajax與REST之間的完美配合已增加了當今人們對REST的注意力。

通過基于REST的API公開系統資源是一種靈活的方法,可以為不同種類的應用程序提供以标準方式格式化的數據。它可以幫助滿足集成需求(這對于構建可在其中容易地組合(Mashup)數據的系統非常關鍵),并幫助将基于REST的基本服務集擴展或構建為更大的集合。

所有“事物”定義ID

REST中的資源所指的不是數據,而是數據和表現形式的組合,比如“最新訪問的10位會員”和“最活躍的10位會員”在數據上可能有重疊或者完全相同,而由于他們的表現形式不同,所以被歸為不同的資源,這也就是為什麼REST的全名是Representational State Transfer的原因。資源标識符就是URI(Uniform Resource Identifier),不管是圖片,Word還是視頻文件,甚至隻是一種虛拟的服務,也不管你是XML(标準通用标記語言下的一個子集)格式、txt文件格式還是其它文件格式,全部通過URI對資源進行唯一的标識。

對資源使用一緻的命名規則(naming scheme)最主要的好處就是你不需要提出自己的規則——而是依靠某個已被定義,在全球範圍中幾乎完美運行,并且能被絕大多數人所理解的規則。

想一下你構建的上一個應用(假設它不是采用RESTful方式構建的)中的任意一個高級對象(high-level object),那就很有可能看到許多從使用唯一标識中受益的用例。比如,如果你的應用中包含一個對顧客的抽象,那麼我可以相當肯定,用戶會希望将一個指向某個顧客的鍊接,能通過電子郵件發送到同事那裡,或者加入到浏覽器的書簽中,甚至寫到紙上。更透徹地講:如果在一個類似于Amazon的在線商城中,沒有用唯一的ID(一個URI)标識它的每一件商品,可想而知這将是多麼可怕的業務決策。

當面對這個原則時,許多人驚訝于這是否意味着需要直接向外界暴露數據庫記錄(或者數據庫記錄ID)——自從多年以來面向對象的實踐告誡我們,要将持久化的信息作為實現細節隐藏起來之後,哪怕是剛有點想法都常會使人驚恐。

但是這條原則與隐藏實現細節兩者之間并沒有任何沖突:通常,值得被URI标識的事物——資源——要比數據庫記錄抽象的多。例如,一個訂單資源可以由訂單項、地址以及許多其它方面(可能不希望作為單獨标識的資源暴露出來)組成。标識所有值得标識的事物,領會這個觀念可以進一步引導你創造出在傳統的應用程序設計中不常見的資源:一個流程或者流程步驟、一次銷售、一次談判、一份報價請求——這都是應該被标識的事物的示例。同樣,這也會導緻創建比非RESTful設計更多的持久化實體。

下面是一些你可能想到的URI的例子:

注:網址中的“*”代表“.”

http://example*com/customers/1234

http://example*com/orders/2007/10/776654

http://example*com/products/4554

http://example*com/processes/salary-increase-234

正如我選擇了創建便于閱讀的URI——這是個有用的觀點,盡管不是RESTful設計所必須的——應該能十分容易地推測出URI的含義:它們明顯地标識着單一“數據項”。但是再往下看:

http://example*com/orders/2007/11

http://example*com/products?color=green

首先,這兩個URI看起來與之前的稍有不同——畢竟,它們不是對一件事物的标識,而是對一類事物集合的标識(假定第一個URI标識了所有在2007年11月份提交的定單,第二個則是綠顔色産品的集合)。但是這些集合自身也是事物(資源),也應該被标識。

注意,使用唯一、全局統一的命名規則的好處,既适用于浏覽器中的Web應用,也适用于機對機(machine-to-machine,m2m)通信。

所有鍊接一起

接下來要讨論的原則有一個有點令人害怕的正式描述:“超媒體被當作應用狀态引擎(Hypermedia as the engine of application state)”,有時簡寫為HATEOAS。嚴格地說這個描述的核心是超媒體概念,換句話說:是鍊接的思想。鍊接是我們在HTML(标準通用标記語言下的一個應用)中常見的概念,但是它的用處絕不局限于此(用于人們網絡浏覽)。

應用程序(已經檢索過文檔)如何“跟随”鍊接檢索更多的信息。當然,如果使用一個遵守專用命名規範的簡單“id”屬性作為鍊接,也是可行的——但是僅限于應用環境之内。使用URI表示鍊接的優雅之處在于,鍊接可以指向由不同應用、不同服務器甚至位于另一個大陸上的不同公司提供的資源——因為URI命名規範是全球标準,構成Web的所有資源都可以互聯互通。

超媒體原則還有一個更重要的方面——應用“狀态”。簡而言之,實際上服務器端(如果你願意,也可以叫服務提供者)為客戶端(服務消費者)提供一組鍊接,使客戶端能通過鍊接将應用從一個狀态改變為另一個狀态。

對此原則總結如下:任何可能的情況下,使用鍊接指引可以被标識的事物(資源)。

使用标準方法

在前兩個原則的讨論中暗含着一個假設:接收URI的應用程序可以通過URI明确地做一些有意義的事情。如果你在公共汽車上看到一個URI,你可以将它輸入浏覽器的地址欄中并回車——但是你的浏覽器如何知道需要對這個URI做些什麼呢?

它知道如何去處理URI的原因在于所有的資源都支持同樣的接口,一套同樣的方法(隻要你樂意,也可以稱為操作)集合。在HTTP中這被叫做動詞(verb),除了兩個大家熟知的(GET和POST)之外,标準方法集合中還包含PUT、DELETE、HEAD和OPTIONS。這些方法的含義連同行為許諾都一起定義在HTTP規範之中。如果你是一名OO開發人員,就可以想象到RESTful HTTP方案中的所有資源都繼承自類似于這樣的一個類(采用類Java、C#的僞語法描述,請注意關鍵的方法):

class Resource{

Resource(URI u);

Response get();

Response post(Request r);

Response put(Request r);

Response delete();

}

由于所有資源使用了同樣的接口,你可以依此使用GET方法檢索一個表述(representation)——也就是對資源的描述。因為規範中定義了GET的語義,所以可以肯定當你調用它的時候不需要對後果負責——這就是為什麼可以“安全”地調用此方法。GET方法支持非常高效、成熟的緩存,所以在很多情況下,你甚至不需要向服務器發送請求。還可以肯定的是,GET方法具有幂等性[譯注:指多個相同請求返回相同的結果]——如果你發送了一個GET請求沒有得到結果,你可能不知道原因是請求未能到達目的地,還是響應在反饋的途中丢失了。幂等性保證了你可以簡單地再發送一次請求解決問題。

幂等性同樣适用于PUT(基本的含義是“更新資源數據,如果資源不存在的話,則根據此URI創建一個新的資源”)和DELETE(你完全可以一遍又一遍地操作它,直到得出結論——删除不存在的東西沒有任何問題)方法。POST方法,通常表示“創建一個新資源”,也能被用于調用任意過程,因而它既不安全也不具有幂等性。

如果你采用RESTful的方式暴露應用功能(如果你樂意,也可以稱為服務功能),那這條原則和它的約束同樣也适用于你。如果你已經習慣于另外的設計方式,則很難去接受這條原則——畢竟,你很可能認為你的應用包含了超出這些操作表達範圍的邏輯。請允許我花費一些時間來讓你相信不存在這樣的情況。

來看下面這個簡單的采購方案例子:

可以看到,例子中定義了兩個服務程序(沒有包含任何實現細節)。這些服務程序的接口都是為了完成任務(正是我們讨論的OrderManagement和CustomerManagement服務)而定制的。如果客戶端程序試圖使用這些服務,那它必須針對這些特定接口進行編碼——不可能在這些接口定義之前,使用客戶程序去有目的地和接口協作。這些接口定義了服務程序的應用協議(application protocol)。

在RESTful HTTP方式中,你将通過組成HTTP應用協議的通用接口訪問服務程序。你可能會想出像這樣的方式:

可以看到,服務程序中的特定操作被映射成為标準的HTTP方法——為了消除歧義,我創建了一組全新的資源。标識一個顧客的URI上的GET方法正好相當于getCustomerDetails操作。有人用三角形形象化地說明了這一點:

把三個頂點想象為你可以調節的按鈕。可以看到在第一種方法中,你擁有許多操作,許多種類的數據以及固定數量的“實例”(本質上和你擁有的服務程序數量一緻)。在第二種方法中,你擁有固定數量的操作,許多種類的數據和許多調用固定方法的對象。它的意義在于,證明了通過這兩種方式,你基本上可以表示任何你喜歡的事情。

為什麼使用标準方法如此重要?從根本上說,它使你的應用成為Web的一部分——應用程序為Web變成Internet上最成功的應用所做的貢獻,與它添加到Web中的資源數量成比例。采用RESTful方式,一個應用可能會向Web中添加數以百萬計的客戶URI;如果采用CORBA技術并維持應用的原有設計方式,那它的貢獻大抵隻是一個“端點(endpoint)”——就好比一個非常小的門,僅僅允許有鑰匙的人進入其中的資源域。

統一接口也使得所有理解HTTP應用協議的組件能與你的應用交互。通用客戶程序(generic client)就是從中受益的組件的例子,例如curl、wget、代理、緩存、HTTP服務器、網關還有Google、Yahoo!、MSN等等。

總結如下:為使客戶端程序能與你的資源相互協作,資源應該正确地實現默認的應用協議(HTTP),也就是使用标準的GET、PUT、POST和DELETE方法。

資源多重表述

客戶程序如何知道該怎樣處理檢索到的數據,比如作為GET或者POST請求的結果?原因是,HTTP采取的方式是允許數據處理和操作調用之間關系分離的。換句話說,如果客戶程序知道如何處理一種特定的數據格式,那就可以與所有提供這種表述格式的資源交互。讓我們再用一個例子來闡明這個觀點。利用HTTP内容協商(content negotiation),客戶程序可以請求一種特定格式的表述:

GET/customers/1234 HTTP/1.1

Host:example*com

Accept:application/vnd.mycompany.customer+xml請求的結果可能是一些由公司專有的XML(标準通用标記語言下的一個子集)格式表述的客戶信息。假設客戶程序發送另外一個不同的請求,就如下面這樣:

GET/customers/1234 HTTP/1.1

Host:example*com

Accept:text/x-vcard結果則可能是VCard格式的客戶地址。(在這裡我沒有展示響應的内容,在其HTTP Content-type頭中應該包含着關于數據類型的元數據。)這說明為什麼理想的情況下,資源表述應該采用标準格式——如果客戶程序對HTTP應用協議和一組數據格式都有所“了解”,那麼它就可以用一種有意義的方式與世界上任意一個RESTful HTTP應用交互。

不幸的是,我們不可能拿到所有東西的标準格式,但是,或許我們可以想到在公司或者一些合作夥伴中使用标準格式來營造一個小環境。當然以上情況不僅适用于從服務器端到客戶端的數據,反之既然——倘若從客戶端傳來的數據符合應用協議,那麼服務器端就可以使用特定的格式處理數據,而不去關心客戶端的類型。

在實踐中,資源多重表述還有着其它重要的好處:如果你為你的資源提供标準通用标記語言下的子集HTML和XML兩種表述方式,那這些資源不僅可以被你的應用所用,還可以被任意标準Web浏覽器所用——也就是說,你的應用信息可以被所有會使用Web的人獲取到。

資源多重表述還有另外一種使用方式:你可以将應用的Web UI納入到Web API中——畢竟,API的設計通常是由UI可以提供的功能驅動的,而UI也是通過API執行動作的。将這兩個任務合二為一帶來了令人驚訝的好處,這使得使用者和調用程序都能得到更好的Web接口。總結:針對不同的需求提供資源多重表述。

無狀态通信

無狀态通信是最後一個原則。首先,需要着重強調的是,雖然REST包含無狀态性(statelessness)的觀念,但這并不是說暴露功能的應用不能有狀态——事實上,在大部分情況下這會導緻整個做法沒有任何用處。無狀态-從客戶端到服務器的每個請求都必須包含理解請求所需的所有信息,并且不能利用服務器上任何存儲的上下文。因此,會話狀态完全保留在客戶端上。

REST要求狀态要麼被放入資源狀态中,要麼保存在客戶端上。或者換句話說,服務器端不能保持除了單次請求之外的,任何與其通信的客戶端的通信狀态。這樣做的最直接的理由就是可伸縮性——如果服務器需要保持客戶端狀态,那麼大量的客戶端交互會嚴重影響服務器的内存可用空間(footprint)。(注意,要做到無狀态通信往往需要需要一些重新設計——不能簡單地将一些session狀态綁縛在URI上,然後就宣稱這個應用是RESTful。)

但除此以外,其它方面可能顯得更為重要:無狀态約束使服務器的變化對客戶端是不可見的,因為在兩次連續的請求中,客戶端并不依賴于同一台服務器。一個客戶端從某台服務器上收到一份包含鍊接的文檔,當它要做一些處理時,這台服務器宕掉了,可能是硬盤壞掉而被拿去修理,可能是軟件需要升級重啟——如果這個客戶端訪問了從這台服務器接收的鍊接,它不會察覺到後台的服務器已經改變了。

MVC

第一個問題假設REST是我們應該采用的架構,然後讨論如何使用;第二個問題則要說明REST和當前最普遍應用的MVC是什麼關系,互補還是取代?

我們先來談談第一個問題,如何使用REST。我感覺,REST除了給我們帶來了一個嶄新的架構以外,還有一個重要的貢獻是在開發系統過程中的一種新的思維方式:通過url來設計系統的結構。根據REST,每個url都代表一個resource,而整個系統就是由這些resource組成的。因此,如果url是設計良好的,那麼系統的結構就也應該是設計良好的。對于非高手級的開發人員來說,考慮一個系統如何架構總是一個很抽象的問題。敏捷開發所提倡的Test Driven Development,其好處之一(我覺得是最大的好處)就是可以通過testcase直觀地設計系統的接口。

比如在還沒有創建一個class的時候就編寫一個testcase,雖然設置不能通過編譯,但是testcase中的方法調用可以很好地從class使用者的角度反映出需要的接口,從而為class的設計提供了直觀的表現。這與在REST架構中通過url設計系統結構非常類似。雖然我們連一個功能都沒有實現,但是我們可以先設計出我們認為合理的url,這些url甚至不能連接到任何page或action,但是它們直觀地告訴我們:系統對用戶的訪問接口就應該是這樣。根據這些url,我們可以很方便地設計系統的結構。

讓我在這裡重申一遍:REST允許我們通過url設計系統,就像Test Driven Development允許我們使用testcase設計class接口一樣。

OK,既然url有這樣的好處,那我們就着重讨論一下如何設計url。網絡應用通常都是有hierarchy的,像棵大樹。我們通常希望url也能反映出資源的層次性。比如對于一個blog應用:/articles表示所有的文章,/articles/1表示id為1的文章,這都比較直觀。遺憾的是,網絡應用的資源結構永遠不會如此簡單。因此人們常常會問這樣一個問題:RESTful的url能複蓋所有的用戶請求嗎?比如,login如何RESTful?search如何RESTful?

從REST的概念上來看,所有可以被抽象為資源的東東都可以使用RESTful的url。因此對于上面的兩個問題,如果login和search可以被抽象為資源,那麼就可以使用RESTful的url。search比較簡單,因為它會返回搜索結果,因此可以被抽象為資源,并且隻實現index方法就可以了(隻需要顯示搜索結果,沒有create、destory之類的東西)。然而這裡面也有一個問題:search的關鍵字如何傳給server?

index方法應該使用HTTP GET,這會把關鍵字加到url後面,當然不符合REST的風格。要解決這個問題,可以把每次search看作一個資源,因此要創建create和index方法,create用來在用戶點擊“搜索”按鈕是通過HTTP POST把關鍵字傳給server,然後index則用來顯示搜索結果。這樣一來,我們還可以記錄用戶的搜索曆史。使用同樣的方法,我們也可以對login應用REST,即每次login動作是一個資源。

一開始可能想到的是/category/ruby/articles,這種想法很直觀。但是我覺得裡面的category是不需要的,我們可以直接把“/ruby”理解為“category是ruby”,也就是說“ruby”出現的位置說明了它指的就是category。OK,/ruby/articles,單單從這個url上看,我們能獲得多少關于category的信息?顯然category隐藏在了url後面,這樣做到底好不好,應該是仁者見仁,智者見智了。另外還有一種url形式,它對應到程序中的繼承關系。

比如product是一個父類,book和computer是其子類。那麼所有産品的url應該是/products,所有書籍的url應該是/books,所有電腦的url應該是/computers。這一想法就比較直觀了,而且再次驗證了url可以幫助我們進行設計的論點。

讓我再說明一下我的想法:如果每個用戶需求都可以抽象為資源,那麼就可以完全使用REST。

由此看來,使用REST的關鍵是如何抽象資源,抽象得越精确,對REST的應用就越好。

有了對第一個問題的讨論,第二個問題就容易讨論多了。REST會取代MVC嗎?還是彼此是互補關系(就像AOP對于OOP)?答案是It depends!如果我們可以把所有的用戶需求都可以抽象為資源,那麼MVC就可以退出曆史的舞台了。如果情況相反,那麼我們就需要混合使用REST和MVC。

當然,這是非常理想的論斷。可能我們無法找到一種方法可以把所有的用戶需求都抽象為資源,因為保證這種抽象的完整性(即真的是所有需求都可以)需要形式化的證明。而且即使被證明出來了,由于開發人員的能力和喜好不同,MVC肯定也會成為不少人的首選。但是對于希望擁抱REST的人來說,這些都沒有關系。隻要你開發的系統所設計的問題域可以被合理地抽象為資源,那麼REST就會成為你的開發利器。

相關詞條

相關搜索

其它詞條