天天看點

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

From:http://www.infoq.com/cn/articles/webber-rest-workflow

我們已習慣于在大型中間件平台(比如那些實作CORBA、Web服務協定棧和J2EE的平台)之上建構分布式系統了。在這篇文章裡,我們将采取另一種做法:我們把支撐Web運作的協定和文檔格式視為一種應用平台,一種可通過輕量級中間件通路的平台。我們通過一個簡單的客戶-服務互動的例子,展示了Web在應用內建中的作用。在這篇文章裡,我們以Web為主要設計理念,提煉并分享了我們下本書《GET /connected - Web-based integration》(暫定名稱)裡的一些想法。

引言

我們知道,內建領域是不斷變化的。Web的影響以及靈活實踐的潮流正在挑戰我們的關于“良好的內建由什麼構成”的觀念。內建(integration)并不是一種夾在系統之間的專業活動;與此相反,現在,內建是成功方案裡的不可缺少的一部分。

然而,仍有許多人誤解并低估Web在企業計算中的作用。即便是那些精通Web的人士,也常常要花費很大力氣才能懂得,Web不是關于支援XML over HTTP的中間件方案,也不是一種簡易的RPC機制。這是相當遺憾的,因為Web不是僅能提供簡單的點對點連接配接,它還有更大的用處;它實際上是一個健壯的內建平台。

在這篇文章裡,我們将展示Web的一些值得關注的用途,我們将視之為一種可塑的、健壯的平台,它能夠對企業系統做很“酷”的事。另外,工作流是企業軟體最具代表性的特征。

為什麼要工作流?

工作流(workflows)是企業計算的主要特征,它們基本上都是用中間件實作的(至少在計算方面)。工作流把一項工作(work)劃分為多個離散的步驟(steps)以及觸發步驟轉移的事件(events)。工作流所實作的整個業務流程常常跨越若幹企業資訊系統,這給工作流帶來很多內建問題。

星巴克:統一标準的咖啡需要統一标準的內建

Web若要成為可用于企業內建的技術,它就必須支援工作流——進而可靠地協調不同系統間的互動,以實作更大的業務能力。

要恰如其份地介紹工作流,就免不了講述一大堆跟領域相關的技術細節,而這不是本文的主旨,是以,我們選擇了Gregor Hohpe的星巴克工作流這個比較好了解的例子來舉例說明基于Web的內建的工作原理。在這篇受到大家歡迎的部落格文章裡,Gregor講述了星巴克是如何形成一個解耦合的(decoupled)盈利生産線的:

“跟大部分餐飲企業一樣,星巴克也主要緻力于将訂單處理的吞吐量最大化。顧客訂單越多,收入就越多。為此,他們采取了異步處理的辦法。你在點單時,收銀員取出一隻咖啡杯,在上面作上記号表明你點的是什麼,然後把這個杯子放到隊列裡去。這裡的隊列指的是在咖啡機前排成一列的咖啡杯。正是這個隊列将收銀員與咖啡師解耦開,進而,即便在咖啡師一時忙不過來的時候,收銀員仍然可以為顧客點單。他們可以在繁忙時段安排多個咖啡師,就像競争消費者模式(Competing Consumer)裡那樣。”

Gregor是采用EAI技術(如面向消息的中間件)來講解星巴克案例的,而我們将采用Web資源(支援統一接口的可尋址實體)來講解同一案例。實際上,我們将展示Web技術何以能夠具有跟傳統EAI工具一樣的可靠性,以及何以不僅僅是請求/響應協定之上的XML消息傳遞!

首先,我們很抱歉擅自設想了星巴克的工作流程,因為我們的目的并不是精确無誤地描述星巴克,而是用基于Web的服務來講解工作流。好的,既然講清楚了這一點,那麼我們現在開始吧。

簡明陳述

因為我們在講工作流,是以我們有必要了解構成工作流的狀态(states)以及将工作流從一個狀态轉移到另一個狀态的事件(events)。我們的例子裡有兩個工作流,我們把它們用狀态機(state machines)表達出來了。這兩個工作流是并行執行的。一個反映了顧客與星巴克服務之間的互動(如圖1),另一個刻畫了由咖啡師執行的一系列動作(如圖2)。

在顧客工作流裡,顧客為了得到某種口味的咖啡而與星巴克服務進行互動。我們假定該工作流裡包含以下動作:顧客點單,付款,然後等待飲品。在點單與付款之間,顧客通常可以修改菜單,比方說請求改用半脫脂牛奶。 /p>

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖1 顧客狀态機

盡管顧客看不見咖啡師,但咖啡師也有自己的狀态機;這個狀态機是服務實作私有的。如圖2所示,咖啡師在周而複始地等待下一個訂單,制作飲品,然後收取費用。當一個訂單被加入到咖啡師的隊列中時,一次循環執行個體就開始了。當咖啡師完成訂單并把飲品傳遞給顧客時,工作流就結束了。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖2 咖啡師的狀态機

盡管這些看似跟基于Web的內建毫不相幹,但這兩個狀态機裡的每一個狀态遷移,都代表着與Web資源的一次互動。每一次遷移,就是通過URI對資源實施HTTP操作,進而導緻狀态的改變。

GET和HEAD屬于特例,因為它們不引起狀态遷移。它們的作用是用于檢視資源的目前狀态。

我們節奏稍快了點。了解狀态機和Web,不是那麼容易一口吃個胖子的。是以,讓我們在Web的背景下,來從頭回顧一下整個場景,逐漸慢慢深入。

顧客視角

我們将從一張簡單的故事卡片開始,它啟動整個流程:

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

這個故事裡涉及一些有用的角色與實體。首先,裡面有“顧客(Customer)”角色。顯然,它是(隐含的)星巴克服務(Starbucks Service)的消費者。其次,裡面有兩個重要的實體(“咖啡”和“訂單”),以及一個重要的互動(“點單”)——我們的工作流正是由它啟動的。

要把訂單送出給星巴克,我們隻要把訂單的表示(representation)POST給下面這個衆所周知的星巴克點單URI即可: 

http://starbucks.example.org/order

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖3 點一杯咖啡

圖3顯示了向星巴克點單的互動過程。星巴克采用自己的XML格式來表達有關實體;需要關注的是,這個格式允許客戶往裡嵌入資訊,以便進行點單——稍後我們會看到。實際送出的資料如圖4所示。

在面向人類的Web(human Web)上,消費者和服務使用HTML作為表示格式(representation format)。HTML有自己特定的語義,所有浏覽器都了解并接受這些語義,比如: 代表“一個連結到其他文檔或本文檔内部某個書簽的錨(anchor)”。消費者應用——浏覽器——隻是呈現HTML,狀态機(也就是你!)用

GET

POST

跟随連結。對于基于Web的內建也一樣,隻不過服務和消費者不僅要就互動協定達成一緻,還要就表示的格式與語義統一意見。
如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖4 POST飲品訂單

星巴克服務建立一個訂單資源,然後把這個新資源的位置放在HTTP報頭

Location

裡傳回給消費者。為友善起見,服務還要把這個新建立的訂單資源的表示(representation)也放在響應裡。發給消費者的響應如下所示。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖5 建立好了訂單,等待付款

201 Created

狀态表明星巴克已經成功接受了訂單。

Location

報頭給出了新建立訂單的URI。響應主體裡的表示(representation)包含了所點飲品及其價格。另外,這個表示裡還包含另一個資源的URI——星巴克希望我們與這個URI互動,以完成顧客工作流;我們稍後将用到它。

注意,該URI是放在

标簽、而不是标簽裡的。這裡的

在顧客工作流裡是具有特定含義的,其語義是事先定義好的。

我們已經知道

201 Created

狀态代碼表示“成功建立資源”的意思。對于這個例子以及一般的基于Web的內建,我們還需要其他一些有用的代碼:

200 OK

——它的意思是:一切正常;繼續執行。

201 Created

——我們剛剛建立了一個資源,一切正常。

202 Accepted

——服務已經接受了我們的請求,并請我們對Location響應報頭裡的URI進行輪詢(poll)。這在異步進行中相當有用。

303 See Other

——我們需要跟另一個資源互動,應該不會出錯。

400 Bad Request

——我們的請求格式有問題,應重新格式化後再送出。

404 Not Found

——服務因為偷懶(或者保密)沒有告知請求失敗的真實原因,但不管什麼原因,我們都得應付它。

409 Conflict

——伺服器拒絕了我們更新資源狀态的請求。我們需要擷取資源的目前狀态(要麼檢查響應實體主體,要麼做一次GET操作),然後再作打算。

412 Precondition Failed

——請求未被處理,因為Etag、If-Match或類似的“哨兵(guard)”報頭的值不滿足條件。我們需要考慮下一步怎麼走。

417 Expectation Failed

——幸虧核查一下,伺服器不将接受你的請求,是以别真正發送那個請求。

500 Internal Server Error

——最偷懶的響應。伺服器出錯了,而且什麼原因都沒說。祝你不要碰見它。

更新訂單

星巴克很不錯的一點就是,你可以按無數種不同的方式來定制自己的飲品。其實,考慮到某些高端客戶極高的要求,也許讓他們按化學公式來點單更好。但我們别那麼貪心——至少開始的時候。我們來看另一張故事卡片:

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

回顧圖4,顯然我們在那裡犯了一個錯誤:真正愛喝咖啡的人是不喜歡往濃咖啡裡放太多熱牛奶的。我們要改正那個問題。幸運地是,Web(或更确切地說,HTTP)以及我們的服務均為這樣的改變提供了支援。

首先,我們要确認我們仍然可以修改訂單。有時咖啡師動作很快,在我們想修改訂單之前,他們就已經把咖啡做好了——于是,我們隻有慢慢享用這杯熱咖啡風味的牛奶了。不過,有時咖啡師會比較慢,這樣我們就可以在訂單得到咖啡師處理之前修改它了。為了知道我們是否還能修改訂單,我們通過HTTP動詞OPTIONS來向訂單資源查詢它接受哪些操作(如圖6)。

請求 響應

OPTIONS /order/1234 HTTP 1.1 Host: starbucks.example.org

200 OK Allow: GET, PUT

圖6 看看有哪些選擇(OPTIONS)

從圖6我們可以知道,訂單資源既是可讀的(支援GET)、也是可更新的(支援PUT)。作為好網民,我們可以拿我們的新表示來做一次試驗性的PUT操作,在真正PUT之前先用

Expect

報頭來試一試(如圖7)。

請求 響應

PUT /order/1234 HTTP 1.1 Host: starbucks.example.com Expect: 100-Continue

100 Continue

圖7 看好再做(Look before you leap)

若我們不能修改訂單了,那麼對圖7所示請求的響應将是

417 Expectation Failed

。不過,假定我們現在得到的響應是

100 Continue

,也就是說,我們可以用

PUT

來更新訂單資源(如圖8)。用

PUT

方法來送出更新後的資源表示(representation),實際上就相當于修改現有資源。在這個例子中,PUT請求裡的新描述包含一個

元素,其中包含我們的更新,即外加一杯濃咖啡。

盡管部分更新(partial updates)屬于REST社群裡比較難懂的理念争論之一,但這裡我們采取一種實用的做法,我們假定:增加一杯濃咖啡的請求,是在現有資源狀态的上下文中被處理的。是以,我們沒必要在網絡上傳送整個資源表示,我們隻要傳送變化的部分即可。
如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖8 更新資源狀态

如果我們能夠成功送出(

PUT

)更新,那麼我們會從伺服器得到響應代碼

200

,如圖9所示。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖9 成功更新資源狀态

檢查

OPTIONS

和采用

Expect

報頭并不能令我們避免碰到“後續的修改請求失敗”的情況。是以,我們并不強制使用它。作為好網民,我們會以某種方式來應付

405

409

響應。

OPTIONS和

Expect

報頭的使用應當被視為可選步驟。

盡管我們明智地使用

Expect

OPTIONS

,但有時

PUT

仍将失敗;畢竟咖啡師也在一刻不停地工作——有時他們動作很靈活!

若我們落後于咖啡師,我們在試圖用

PUT

操作把更新送出給資源時會被告知。圖10顯示的就是一個常見的更新失敗的響應。

409 Conflict

狀态代碼表明,若接受更新,将導緻資源處于不一緻的狀态,是以沒有進行更新。響應主體裡顯示出了我們試圖

PUT

的表示(representation)與服務端資源狀态之間的差異。按咖啡制作的話說,加得太晚了——咖啡師已經把熱牛奶倒進去了。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖10 慢了一步

我們已經講述了使用

Expect

OPTIONS

來盡量防止競争條件。除此以外,我們還可以給我們的

PUT

請求加上

If-Unmodified-Since

If-Match

報頭,以表達我們對服務的期望條件。

If-Unmodified-Since

采用時間戳,而

If-Match

采用原始訂單的ETag1 。若訂單狀态自從被我們建立以來還沒有改變過——也就是說,咖啡師還沒有開始制作我們的咖啡——那麼更新可以處理。若訂單狀态已經發生改變,那麼我們會得到

412 Precondition Failed

響應。雖然我們因為慢了咖啡師一步而隻能享用牛奶咖啡,但至少我們沒有把資源轉移到不一緻的狀态。

用Web進行一緻的狀态更新可以采取很多種模式。HTTP PUT是幂等的(idempotent),這樣我們在進行狀态更新時就用不着處理一些複雜事務了,不過仍有一些選擇需要我們決定。下面是正确進行狀态更新的一些方法:

1. 通過發送

OPTIONS

請求,查詢服務是否接受

PUT

操作。這一步是可選的。它可以告知用戶端,此刻伺服器允許對該資源做哪些操作,不過這無法保證伺服器将永遠支援那些操作。

2. 使用

If-Unmodified-Since

If-Match

報頭,以避免伺服器執行不必要的

PUT

操作。假如

PUT

後來失敗了,那麼你會得到

412 Precondition Failed

。此方法要求:要麼資源是緩慢更新的,要麼支援ETag;對于前者就用

If-Unmodified-Since

,對于後者就用

If-Match

3. 立即用

PUT

操作送出更新,并應付可能出現的

409 Conflict

響應。就算我們使用了(1)和(2),我們可能仍得應付這些響應,因為我們的“哨兵”和檢查本質上都是樂觀的。

關于檢測和處理不一緻的更新,W3C有一個非規範性文檔,該文檔推薦采用ETag。ETags也是我們推薦采用的方法。

在完成那些更新咖啡訂單的艱苦工作之後,按理說我們應當得到額外那杯濃咖啡了。是以我們現在假定已設法得到了額外那杯濃咖啡。當然,我們要付過款後星巴克才會把咖啡遞給我們(其實他們也已經暗示過了!),是以我們還需要一張故事卡片:

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

還記得最初那個針對原始訂單的響應嗎?其中有個

元素。星巴克在訂單資源的表示裡面嵌入了有關另一個資源的資訊。我們前面看過那個标簽,但當時因為顧于修改訂單就沒有具體講。現在我們應該進一步探讨它了:

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

關于

next

元素,有幾點是值得指出的。首先,它處于一個不同的名稱空間之下,因為狀态遷移并不是隻有星巴克需要。在這裡,我們決定把這種用于狀态遷移的URI放在一個公共的名稱空間裡,以便于重用(或甚至最終的标準化)。

其次,

rel

屬性裡嵌入了一則語義資訊(你樂意的話,也可以稱之為一種私有的微格式)。能夠了解

http://starbucks.example.org/payment

這串文字的消費者,可以使用由

uri

屬性辨別的資源轉移到工作流裡的下一狀态(付款)。

元素裡的

uri

指向的是一個付款資源。根據

type

屬性,我們已經知道預期的資源表示(representation)是XML格式的。我們可以向這個付款資源發送

OPTIONS

請求,看看它支援哪些HTTP操作。

微格式(microformat)是一種在現有文檔裡嵌入結構化、語義豐富的資料的方式。微格式在人類可讀的Web上相當常見,它們用于往網頁裡增加結構化資訊(如日程表)的 表示(representations)。不過,它們同樣也可以友善地被用于內建。微格式術語是在微格式社群裡達成一緻的,不過我們也可以自由建立自己的 私有微格式,用于特定領域的語義标記。

盡管它們看上去沒多大用,但如圖10裡那樣的簡單連結正是REST社群所呼籲的“将超媒體作為應用狀态的引擎(hypermedia as the engine of application state)”的關鍵。更簡單地說,URI代表了狀态機裡的狀态遷移。正如我們在文章開始時所看到的,用戶端是通過跟随連結的方式來操作應用程式的狀态 機的。

如果你一時不能了解,不要感到奇怪。這一模型的最不可思議之處在于:狀态機和工作流不是像WS-BPEL或WS-CDL那樣事先描述好的,而是在你 經曆各個狀态的過程中逐漸得到描述的。不過,一旦你的想明白了,你就會發現,跟随連結(following links)這種方式使得我們可以在應用的各種狀态下向前推進。每次狀态遷移時,目前資源的表示裡都包含了指向可能的下一狀态的連結以及它們所代表的狀 态。另外,由于這些代表下一狀态的資源是Web資源,是以我們知道如何使用它們。

在顧客工作流裡,我們下一步要做的是為咖啡付款。我們可以由訂單裡的

元素得知總金額,但在我們向星巴克付款之前,我們想向付款資源查詢一下我們應當如何與之互動(如圖11)。

消費者需要事先掌握多少關于一個服務的知識呢?我們已經說過了,服務和消費者在互動之前需要就它們将會交換的表示(representations)的語 義達成一緻。可以将這些表示格式(representation formats)看成一組可能的狀态和遷移。在消費者與服務互動時,服務選擇可用的狀态和遷移,并構造下一個表示。步向目标的過程是 動态發現的,而把這一過程中的各個部分串起來的方式是事先達成一緻的。

在設計與開發過程中,消費者會就表示和遷移的語義與伺服器達成一緻。但誰也不能保證服務在其演化過程中會不會采用一種用戶端預期之外的表示和遷移 (不過用戶端還是知道如何處理它的)——那是Web松耦合的本質特性。盡管如此,在這些情況下就資源格式和表示達成一緻超出了本文的範圍。

我們下一步要做的是為咖啡付款。我們可以由訂單表示的

元素得知總金額,是以我們要做的就是付款給星巴克,然後咖啡師把飲品交給我們。首先,我們向付款資源查詢我們應當如何與之互動(如圖11)。

請求 響應

OPTIONS/payment/order/1234 HTTP 1.1 Host: starbucks.example.com

Allow: GET, PUT

圖11 獲知如何付款

伺服器傳回的響應告訴我們,我們既可以讀取付款(通過

GET

)、也可以更新它(通過

PUT

)。既然知道了金額,那麼接下來,我們就把款項

PUT

給那個由付款連結辨別的資源。當然,付款金額屬于秘密資訊,是以我們将通過認證2來保護該資源。

請求

PUT /payment/order/1234 HTTP 1.1 Host: starbucks.example.com Content-Type: application/xml Content-Length: ... Authorization: Digest username="Jane Doe" realm="starbucks.example.org“  nonce="..." uri="payment/order/1234" qop=auth nc=00000001 cnonce="..." reponse="..." opaque="..."    123456789    07/07    John Citizen    4.00

響應

201 Created Location: https://starbucks.example.com/payment/order/1234 Content-Type: application/xml Content-Length: ...    123456789    07/07    John Citizen    4.00

圖12 付款

為成功完成付款,我們隻需按圖12進行互動即可。一旦經認證的

PUT

傳回一個

201 Created

響應,我們就可以慶祝付款成功、并拿到我們的飲品了。

不過事情也有出錯的時候。當資金處于危險狀态時,我們希望要麼沒出錯、要麼可以挽救錯誤3。付款時可能出現很多種容易想象的出錯情況:

  • 由于伺服器當機或其他原因,我們無法連接配接上伺服器了;
  • 在互動過程中,與伺服器的連接配接被切斷了;
  • 伺服器傳回一個

    4xx

    5xx

    範圍的錯誤狀态。

幸運地是,Web可以幫助我們應付以上這些情況。對前兩種情況(假定連接配接問題是瞬間的),我們可以反複做

PUT

請求,直至我們收到成功響應為止。如果前次

PUT

操作已經得到了成功處理,那麼我們将收到一個

200

響應(本質上是一個來自伺服器的空操作确認);如果本次

PUT

操作成功完成了付款,那麼我們将收到一個

201

響應。在第三種情況中,如果伺服器傳回的響應代碼是

500

503

504

,那麼也可以做同樣處理。

4xx

範圍的狀态代碼比較難處理,不過它們仍然指出了下一步怎麼辦。例如,

400

響應表明我們通過

PUT

請求送出的内容無法被伺服器所了解,我們需要糾正後重新發送

PUT

請求。

403

響應則相反,它表明伺服器能夠了解我們的請求,但不知道如何履行(fulfil)它,而且伺服器希望我們不要重試。對于這些情況,我們得在響應的有效負載(payload)裡尋找其他的狀态遷移(連結),換其他推進狀态的路線。

在這個例子中,我們已經多次使用狀态代碼來指引用戶端步向下一個互動了。狀态代碼是具有豐富語義的确認資訊。讓服務傳回有意義狀态代碼,并且令客戶 端懂得如何處理狀态代碼,這樣一來,我們便給HTTP簡單的請求響應機制增加了一層協調協定,進而提高了分布式系統的健壯性和可靠性。

一旦我們為自己的飲品買了單,我們這個工作流就算完成了,有關顧客的故事也就到此結束了。不過整個故事還沒有完。現在我們進入到服務裡面,看看星巴克的内部實作。

咖啡師視角

作為顧客,我們樂于把自己放在咖啡世界的中央,不過我們并不是咖啡服務的唯一消費者。從與咖啡師的“時間競賽”中我們已經得知,咖啡服務還為包括咖啡師在内的其他一些相關方面提供服務。按照我們循序漸進的介紹方式,現在該推出另一張故事卡片了。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

用Web的格式與協定來描述飲品清單是件很容易的事。用Atom提要(feeds)來表達清單之類的東西是相當不錯的選擇,它幾乎可描述任何清單(比如未完成的咖啡訂單),是以這裡我們可以也采用它。咖啡師可以通過向該Atom提要的URI發送

GET

請求來通路它,對于未完成的訂單,URI是

http://starbucks.example.org/orders

(如圖13)。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖13 待制作飲品的Atom提要

星巴克是家相當繁忙的店,位于

/orders

的Atom提要更新相當頻繁,是以咖啡師要不斷輪詢這個提要才能保證掌握最新資訊。輪詢通常被認為可伸縮性很差;但是,Web支援可伸縮性極強的輪詢機制——我們稍後會看到。另外,由于星巴克每分鐘要制作很多咖啡,是以承受住負荷是個重要問題。

這裡我們有兩個相抵觸的需求。一方面,我們希望咖啡師通過經常輪詢訂單提要,以不斷掌握最新資訊;另一方面,我們又不希望給服務增添負擔、或者徒然增加網 絡流量。為防止我們的服務因過載而崩潰,我們将在我們服務之外,用一個逆向代理(reverse proxy)來緩存并提供被頻繁通路的資源表示(如圖14所示)。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖14 通過緩存提升可伸縮性

對于大多數資源(尤其是那些會被很多人通路的資源,如傳回飲品清單的Atom提要),在宿主服務之外緩存它們是合理的。這樣可以降低伺服器負 載,提升可伸縮性。我們在架構裡增設了Web緩存(逆向代理),再加上有緩存中繼資料,這樣用戶端擷取資源時就不會給原伺服器增添很大負擔了。

緩存的有利一面是,它屏蔽掉了伺服器的間隙性故障,并通過提高資源可用率來幫助災難恢複。也就是說,即便星巴克服務出現了故障,咖啡師仍然可以繼續工 作,因為訂單資訊是被代理緩存起來的。而且,假如咖啡師漏了某個訂單的話(錯誤),恢複也很容易進行,因為訂單具有很高的可用率。

是的,緩存可以把舊訂單多保留一段時間,但對于像星巴克這樣吞吐量很高的商戶而言,這是不太理想的。為了把太舊的訂單從緩存中清除,星巴克服務用

Expires

報頭來聲明一個響應可以被緩存多久。任何介于消費者與服務之間的緩存都應當服從這一訓示,拒絕提供過期訂單4,而是把請求轉發到星巴克服務上,以擷取最新的訂單資訊。

圖13所示的響應對Atom提要的

Expires

報頭進行了相應的設定,令飲品清單在10秒鐘後過期。由于這 種緩存行為,伺服器每分鐘最多隻要響應6次請求,其餘請求将由緩存機制代勞。即便對于性能比較糟糕的服務,每分鐘6個請求也屬于容易處理的工作量了。在最 愉快的情況下(對星巴克服務來說),咖啡師的輪詢請求是由本地緩存響應的,這樣就不會給增加網絡活動或伺服器負荷了。

在我們的例子中,我們隻設定了一個緩存來幫助提升主咖啡清單的可伸縮性。然而,在真實的基于Web的場景中,我們可以從多層緩存中受益。要在大規模環境中提升可伸縮性,利用現有Web緩存的優點是至關重要的。

Web以延遲換取了高度的可伸縮性。假如你的問題對延遲很敏感的話(比如外彙交易),那麼就不太适合采用基于Web的方案了。但是,假如你可以接受“秒”數量級上的延遲,那麼Web也許是個不錯的平台。

既然我們已經成功解決了可伸縮性問題,那麼我們繼續來實作更多的功能。當咖啡師開始為你制作咖啡時,應當修改訂單狀态,以達到禁止更新的目的。從顧客的角度來看,這相當于我們無法再對我們的訂單執行

PUT

操作了(如圖6、7、8、9、10所示)。

幸運地是,我們可以利用一個已經定義好的協定——Atom釋出協定(Atom Publishing Protocol,簡稱APP或AtomPub)——來實作這一目标。AtomPub是一個以Web中心(基于URI)的協定,用于管理Atom提要裡的 條目(entries)。我們來仔細看看Atom提要(

/orders

)裡代表咖啡的條目。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖15 咖啡訂單對應的Atom條目

在圖15所示的XML裡,有幾點值得注意。首先,它将我們的訂單與Atom提要裡的其他訂單區分開了。其次,其中包含訂單本身,即咖啡師制作咖啡所需的全部資訊——包括我們要求增加一杯濃咖啡的重要資訊!該訂單對應的

entry

元素裡有個

link

元素,它聲明了本條目(

entry

)的編輯URI(edit)。這個編輯URI指向的是一個可以通過HTTP編輯的訂單資源。(這裡,可編輯資源的位址剛好跟訂單資源本身的位址一樣,不過這不是必須的。)

如果咖啡師要鎖定訂單資源、禁止它被修改,就可以通過該編輯URI來改變訂單資源的狀态。具體地講,咖啡師可以用

PUT

請求把經修改的資源狀态送出給這個編輯URI(如圖16所示)。

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

圖16 通過AtomPub設定訂單狀态

伺服器一旦處理了如圖16所示的

PUT

請求,它就會拒絕對位于

/orders/1234

的訂單資源做除

GET

以外的操作。

現在訂單處于穩定狀态了,咖啡師可以毫無顧慮地繼續制作咖啡了。當然,咖啡師隻有知道我們已經付過款才會把咖啡給我們,是以咖啡師還要查詢我們是否已經完 成付款。在真實的星巴克裡,情況會略有不同:一般來說,我們是點單後立即付款的;然後,其他顧客站在周圍,以免你拿走别人點的飲品。但在我們計算機化的版 本裡,增加這一檢查并不麻煩,是以我們來看倒數第二張故事卡片:

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

咖啡師隻要向付款資源(該資源的URI在訂單表示裡給出了)發送

GET

請求,即可查詢付款狀态。

這裡,顧客和咖啡師是通過訂單表示裡給出的連結得知付款資源的URI的。但有時,通過URI模版來通路資源也很友善。

URI模版(URI template)是一種描述知名URI的格式。它允許消費者通過修改URI裡的部分字元來通路不同的資源。

Amazon的S3存儲服務就是基于URI模版的。使用者可以對由以下模版生成的URIs進行HTTP操作,進而對已儲存的制品進行操作:

http://s3.amazonaws.com/{bucket_name}/{key_name}

為友善咖啡師(或其他經授權的星巴克系統)不用周遊所有訂單即可通路各個付款資源,我們可以在我們的模型裡設計一個類似的URL模版方案:

http://starbucks.example.org/payment/order/{order_id}

URI模版就像與消費者訂立的契約,服務提供者須在服務演化過程中注意維持它們的穩定。由于這一潛在的耦合,有些Web內建工作者會有意避免采用URI模版。我們的建議是,僅當可推斷的URIs(inferable URIs)很有幫助而且不會改變時才使用。

對于我們的例子,另一種辦法是在

/payments

處暴露一個提要,用它提供包含指向各個付款資源的(不可推斷的)連結。該提要隻有經授權的系統才能讀取。

最終,URI模版是不是一個相對超媒體來說安全而有效的捷徑,要由服務設計者來決定。我們的建議是:要保守地使用URI模版!

當然,不是人人都可以檢視付款資訊的。我們不想讓咖啡社群裡會動歪腦筋的人檢視他人的信用卡詳細資訊,是以,跟其他敏感的Web系統一樣,我們利用請求認證來保護敏感資源。

如有未認證的使用者或系統試圖擷取一個具體的付款資訊,那麼伺服器會質詢(challenge)它、要求它提供證書。(如圖17)

請求 響應

GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org

401 Unauthorized WWW-Authenticate: Digest realm="starbucks.example.org", qop="auth", nonce="ab656...", opaque="b6a9..."

圖17 對付款資源的非授權通路受到質詢

401

狀态(及其認證中繼資料)告訴我們,我們應當在請求裡附上正确的證書、然後重新發送請求。重新用正确的證書發送請求(圖18)後,我們得到了付款資訊,并将之與代表訂單總金額的資源

http://starbucks.example.org/total/order/1234

進行比較。

請求 響應

GET /payment/order/1234 HTTP 1.1 Host: starbucks.example.org Authorization: Digest username="barista joe" realm="starbucks.example.org“ nonce="..." uri="payment/order/1234" qop=auth nc=00000001 cnonce="..." reponse="..." opaque="..."

200 OK Content-Type: application/xml Content-Length: ...    123456789    07/07    John Citizen    4.00

圖18 授權通路付款資源

一旦咖啡師制作好、交出咖啡并完成收款,他們就要在待處理飲品清單中删除相應的訂單。如同前面一樣,我們采用一個故事來講解這個回合:

如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分

因為訂單提要裡的各個條目(

entry

)都辨別着一個可編輯資源,而且有自己的URI,是以我們可以對各個訂單資源做HTTP操作。如圖19所示,咖啡師隻要對相關條目(

entry

)所引用的資源做

DELETE

操作即可将它從清單中删除。

請求 響應

DELETE /order/1234 HTTP 1.1 Host: starbucks.example.org

200 OK

圖19 删除已完成的訂單

在條目被删除(

DELETE

)之後,再對訂單提要做

GET

操作的話,傳回的表示裡将不再包含已删除(

DELETE

)的資源。假定我們的緩存工作正常、且我們已經設定了合理的緩存過期中繼資料的話,那麼當你試圖擷取(

GET

)那個訂單條目時将直接得到

404 Not Found

響應。

也許你已經注意到了,Atom釋出協定可以滿足我們對星巴克這個問題的大部分需求。如果我們想直接把位于

/orders

的Atom提要暴露給顧客的話,顧客就可以用Atom釋出協定來向該提要釋出飲品訂單、甚至修改訂單了。

演化:Web上的現實情況

因為我們的咖啡店是基于自描述的狀态機(state machines)建構起來的,是以我們可以友善地根據業務需要改造我們的工作流。例如,星巴克也許會提供一種免費的網上促銷活動:

  • 7月——我們的星巴克店開業,并提供标準的工作流以及我們前面提到的狀态遷移和表示(representation)。消費者知道用這些格式與表示跟我們的服務進行互動。
  • 8月——星巴克新推出了一種免費網上促銷的表示(representation)。我們的咖啡工作流将進行更新,以包含指向該網上促銷資源的連結。由于URI的特性,連結可以是指向第三方的——這跟指向星巴克内部的資源一樣簡單。
  • 如何擷取(GET)一杯咖啡——星巴克REST案例分析作者 Jim Webber, Savas Parastatidis & Ian Robinson 譯者 徐涵 釋出于 2008年12月3日 下午7時28分
    因為表示裡仍然包含原來的遷移點,是以現有消費者仍然可以實作它們的目标,隻不過它們可能無法享受促銷而已,因為這部分還沒有寫進它們的代碼裡去。
  • 9月——消費者應用和服務都進行了有關更新,以便能夠了解并使用免費的網上促銷。

成功進行演化的關鍵在于,服務的消費者們要能夠預料到改變。在每一步,服務不是直接跟資源綁定(例如通過URI模版),而是提供指向具名資源(named resources)的URIs,以便消費者與之互動。這些具名資源,有些是消費者不認識的、将被忽略的,有些是消費者已知的、想采用的狀态遷移點。不管 采用哪種方式,這種方案使得服務可以優雅地演化,同時還能維持與消費者相容。

你将使用的是一個相當熱門的技術

傳遞咖啡是我們工作流的最後一步。我們已經點了單、修改了訂單(也可能無法修改)、付過款并最終拿到了我們的咖啡。在櫃台另一側,星巴克也已經同樣完成了收款和訂單處理。

我們可以用Web來描述所有必需的互動。我們可以利用現有的Web模型處理一些簡單的不愉快的事(例如無法修改進行中或已處理完畢的訂單),而不必 自己發明新的異常或錯誤處理機制——我們所需的一切都是HTTP現成提供的。而且,即便發生了那些不愉快的事,用戶端仍然可以向它們的目标邁進。

HTTP提供的特性起初看來是無關緊要的。但這個協定現在已經取得廣泛的一緻、并得到廣泛的部署了,而且所有的軟體與硬體都能一定程度上了解它。當 我們看到其他分布式計算技術(如WS-*)處于割據狀态的格局時,我們意識到了HTTP享有的巨大成功,以及它在系統間內建方面的潛力。

甚至在非功能性方面,Web也是有益的。在我們碰到臨時故障時,HTTP操作(

GET

PUT

DELETE

)的幂等性質令我們可以進行安全的重試;内在的緩存機制既屏蔽了故障,又有助于災難恢複(通過增強的可用率);HTTPS和HTTP認證有助于基本的安全需求。

盡管我們的問題域是人為制造的,但我們所強調的技術同樣可以應用于分布式計算環境。我們不會僞稱Web很簡單(除非你是天才),Web可以解決一切問題 (除非你是超級樂觀的人,或受到REST信仰的感染),但事實上,在局部、企業級和Internet級進行系統內建,Web是個健壯的架構。

緻謝

本文作者要向英國卡迪夫大學(Cardiff University)的Andrew Harrison表示感謝,是他啟發了我們就Web上的“對話描述”進行讨論。

About the Authors

Jim Webber博士是ThoughtWorks公司的專業服務主管,他的工作是為全球客戶進行可靠的分布式系統架構設計。此 前,Jim擔任英國E-Science計劃進階研究員,從事将Web服務實踐及可靠面向服務計算的架構模式應用于網格計算的戰略設計工作,他在Web及 Web服務架構與 開發方面具有廣泛的經驗。Jim還擔任過惠普公司和Arjuna公司的架構師,他是業界首個Web服務事務方案的首席開發者。Jim是一位活躍的演說家, 他經常受邀出席 國際會議并發言。他還是一位活躍的作家,除了《Developing Enterprise Web Services - An Architect's Guide》這本書外,目前他正在撰寫一本關于基于Web的內建的新書。Jim獲得英國紐卡斯爾大學(University of Newcastle)的計算機科學學士學位和并行計算博士學位。他的部落格位址是:http://jim.webber.name。

Savas Parastatidis是一位軟體思想家,他的思考領域涉及系統和軟體。他研究技術在eResearch裡的運用,他尤其對雲計算、知識表示與管理、社會網絡感興趣。他目前任職于微軟研究院合作研究部。Savas喜歡在http://savas.parastatidis.name上寫部落格。

Ian Robinson幫助客戶們建立可持續的面向服務的能力,令業務與IT從開始到實際營運始終保持齊合。他為微軟公司寫過關于采用微軟技術實作面向服務系統的指南,還發表過文章講述消費者驅動的服務契約及其在軟體開發生命周期中的作用——該文章可以在《ThoughtWorks文集(The ThoughtWorks Anthology)》(Pragmatic Programmers,2008)及InfoQ中文站上找到。他經常在會議上做有關REST式企業開發及面向服務傳遞的測試驅動基礎的講演。

1 ETag(Entity Tag的簡寫)是資源狀态的唯一辨別符。一個資源的ETag通常是根據該資源的資料得到的MD5校驗和或SHA1哈希值。

2 我們将從稍後的星巴克例子中了解認證的工作原理。

3 當然,如果安全性遭到威脅,我們隻要防止事情不要錯得更厲害就行了!但得到咖啡并不是一項攸關安全的任務,盡管每天早晨我的同僚們可能會這麼認為!

4 HTTP 1.1提供了一些有用的請求指令,比如

max-age

max-stale

max-fresh

,它們允許用戶端指出願意接受緩存裡多舊的資料。

檢視英文原文:How to GET a Cup of Coffee。

繼續閱讀