如何獲取(GET)一杯咖啡——星巴克REST案例分析

來源: InfoQ  發布時間: 2014-07-28 22:57  閱讀: 5280 次  推薦: 8   原文鏈接   [收藏]  

  英文原文:How to GET a Cup of Coffee

  我們已習慣于在大型中間件平臺(比如那些實現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)。

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

圖1 顧客狀態機

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

圖2 咖啡師的狀態機

  盡管這些看似跟基于Web的集成毫不相干,但這兩個狀態機里的每一個狀態遷移,都代表著與Web資源的一次交互。每一次遷移,就是通過URI對資源實施HTTP操作,從而導致狀態的改變。

GET和HEAD屬于特例,因為它們不引起狀態遷移。它們的作用是用于查看資源的當前狀態。

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

  顧客視角

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

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

  要把訂單提交給星巴克,我們只要把訂單的表示(representation)POST給下面這個眾所周知的星巴克點單URI即可: http://starbucks.example.org/order

圖3 點一杯咖啡

  圖3顯示了向星巴克點單的交互過程。星巴克采用自己的XML格式來表達有關實體;需要關注的是,這個格式允許客戶往里嵌入信息,以便進行點單——稍后我們會看到。實際提交的數據如圖4所示。

在面向人類的Web(human Web)上,消費者和服務使用HTML作為表示格式(representation format)。HTML有自己特定的語義,所有瀏覽器都理解并接受這些語義,比如:代表“一個鏈接到其他文檔或本文檔內部某個書簽的錨(anchor)”。消費者應用——瀏覽器——只是呈現HTML,狀態機(也就是你!)用GETPOST跟隨鏈接。對于基于Web的集成也一樣,只不過服務和消費者不僅要就交互協議達成一致,還要就表示的格式與語義統一意見。

圖4 POST飲品訂單

  星巴克服務創建一個訂單資源,然后把這個新資源的位置放在HTTP報頭Location里返回給消費者。為方便起見,服務還要把這個新創建的訂單資源的表示(representation)也放在響應里。發給消費者的響應如下所示。

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

  201 Created狀態表明星巴克已經成功接受了訂單。Location報頭給出了新創建訂單的URI。響應主體里的表示(representation)包含了所點飲品及其價格。另外,這個表示里還包含另一個資源的URI——星巴克希望我們與這個URI交互,以完成顧客工作流;我們稍后將用到它。

  注意,該URI是放在標簽<next/>中、而不是標簽<a/>中。這里的在顧客工作流里是具有特定含義的,其語義是事先定義好的。

我們已經知道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 —— You did the right thing by checking, but please don't try to send that request for real.
500 Internal Server Error —— 最偷懶的響應。服務器出錯了,而且什么原因都沒說。祝你不要碰見它。

  更新訂單

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

  回顧圖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社區里比較難懂的理念爭論之一,但這里我們采取一種實用的做法,我們假定:增加一杯濃咖啡的請求,是在現有資源狀態的上下文中被處理的。因此,我們沒必要在網絡上傳送整個資源表示,我們只要傳送變化的部分即可。

圖8 更新資源狀態

  如果我們能夠成功提交(PUT)更新,那么我們會從服務器得到響應代碼200,如圖9所示。

圖9 成功更新資源狀態

  檢查OPTIONS和采用Expect報頭并不能令我們避免碰到“后續的修改請求失敗”的情況。因此,我們并不強制使用它。作為好網民,我們會以某種方式來應付405409響應。

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

  盡管我們明智地使用ExpectOPTIONS,但有時PUT仍將失敗;畢竟咖啡師也在一刻不停地工作——有時他們動作很敏捷!

  若我們落后于咖啡師,我們在試圖用PUT操作把更新提交給資源時會被告知。圖10顯示的就是一個常見的更新失敗的響應。409 Conflict狀態代碼表明,若接受更新,將導致資源處于不一致的狀態,所以沒有進行更新。響應主體里顯示出了我們試圖PUT的表示(representation)與服務端資源狀態之間的差異。按咖啡制作的話說,加得太晚了——咖啡師已經把熱牛奶倒進去了。

圖10 慢了一步

  我們已經講述了使用ExpectOPTIONS來盡量防止競爭條件。除此以外,我們還可以給我們的PUT請求加上If-Unmodified-SinceIf-Match報頭,以表達我們對服務的期望條件。If-Unmodified-Since采用時間戳,而If-Match采用原始訂單的ETag1 。若訂單狀態自從被我們創建以來還沒有改變過——也就是說,咖啡師還沒有開始制作我們的咖啡——那么更新可以處理。若訂單狀態已經發生改變,那么我們會得到412 Precondition Failed響應。雖然我們因為慢了咖啡師一步而只能享用牛奶咖啡,但至少我們沒有把資源轉移到不一致的狀態。

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

1. 通過發送OPTIONS請求,查詢服務是否接受PUT操作。這一步是可選的。它可以告知客戶端,此刻服務器允許對該資源做哪些操作,不過這無法保證服務器將永遠支持那些操作。

2. 使用If-Unmodified-SinceIf-Match報頭,以避免服務器執行不必要的PUT操作。假如PUT后來失敗了,那么你會得到412 Precondition Failed。此方法要求:要么資源是緩慢更新的,要么支持ETag;對于前者就用If-Unmodified-Since,對于后者就用If-Match

3. 立即用PUT操作提交更新,并應付可能出現的409 Conflict響應。就算我們使用了(1)和(2),我們可能仍得應付這些響應,因為我們的“哨兵”和檢查本質上都是樂觀的。

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

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

  還記得最初那個針對原始訂單的響應嗎?其中有個元素。星巴克在訂單資源的表示里面嵌入了有關另一個資源的信息。我們前面看過那個標簽,但當時因為顧于修改訂單就沒有具體講。現在我們應該進一步探討它了:

  關于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。付款時可能出現很多種容易想象的出錯情況:

  • 由于服務器宕機或其他原因,我們無法連接上服務器了;
  • 在交互過程中,與服務器的連接被切斷了;
  • 服務器返回一個4xx5xx范圍的錯誤狀態。

  幸運地是,Web可以幫助我們應付以上這些情況。對于前兩種情況(假定連接問題是瞬間的),我們可以反復做PUT請求,直至我們收到成功響應為止。如果前次PUT操作已經得到了成功處理,那么我們將收到一個200響應(本質上是一個來自服務器的空操作確認);如果本次PUT操作成功完成了付款,那么我們將收到一個201響應。在第三種情況中,如果服務器返回的響應代碼是500503504,那么也可以做同樣處理。

  4xx范圍的狀態代碼比較難處理,不過它們仍然指出了下一步怎么辦。例如,400響應表明我們通過PUT請求提交的內容無法被服務器所理解,我們需要糾正后重新發送PUT請求。403響應則相反,它表明服務器能夠理解我們的請求,但不知道如何履行(fulfil)它,而且服務器希望我們不要重試。對于這些情況,我們得在響應的有效負載(payload)里尋找其他的狀態遷移(鏈接),換其他推進狀態的路線。

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

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

  咖啡師視角

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

  用Web的格式與協議來描述飲品列表是件很容易的事。用Atom feed來表達列表之類的東西是相當不錯的選擇,它幾乎可描述任何列表(比如未完成的咖啡訂單),所以這里我們可以也采用它。咖啡師可以通過向該Atom提要的URI發送GET請求來訪問它,對于未完成的訂單,URI是http://starbucks.example.org/orders(如圖13)。

圖13 待制作飲品的Atom提要

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

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

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

  對于大多數資源(尤其是那些會被很多人訪問的資源,如返回飲品列表的Atom feed),在宿主服務之外緩存它們是合理的。這樣可以降低服務器負載,提升可伸縮性。我們在架構里增設了Web緩存(反向代理),再加上有緩存元數據,這樣客戶端獲取資源時就不會給原服務器增添很大負擔了。

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

  是的,緩存可以把舊訂單多保留一段時間,但對于像星巴克這樣吞吐量很高的商戶而言,這是不太理想的。為了把太舊的訂單從緩存中清除,星巴克服務用Expires報頭來聲明一個響應可以被緩存多久。任何介于消費者與服務之間的緩存都應當服從這一指示,拒絕提供過期訂單4,而是把請求轉發到星巴克服務上,以獲取最新的訂單信息。

  圖13所示的響應對Atom feed的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 feed里的條目(entries)。我們來仔細看看Atom提要(/orders)里代表咖啡的條目。

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

  在圖15所示的XML里,有幾點值得注意。首先,它將我們的訂單與Atom feed里的其他訂單區分開了。其次,其中包含訂單本身,即咖啡師制作咖啡所需的全部信息——包括我們要求增加一杯濃咖啡的重要信息。該訂單對應的entry元素里有個link元素,它聲明了本條目(entry)的編輯URI(edit)。這個編輯URI指向的是一個可以通過HTTP編輯的訂單資源。(這里,可編輯資源的地址剛好跟訂單資源本身的地址一樣,不過這不是必須的。)

  如果咖啡師要鎖定訂單資源、禁止它被修改,就可以通過該編輯URI來改變訂單資源的狀態。具體地講,咖啡師可以用PUT請求把經修改的資源狀態提交給這個編輯URI(如圖16所示)。

圖16 通過AtomPub設置訂單狀態

  服務器一旦處理了如圖16所示的PUT請求,它就會拒絕對位于/orders/1234的訂單資源做除GET以外的操作。

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

  咖啡師只要向付款資源(該資源的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處暴露一個feed,用它提供包含指向各個付款資源的(不可推斷的)鏈接。該提要只有經授權的系統才能讀取。

最終,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 授權訪問付款資源

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

  因為訂單feed里的各個條目(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的特性,鏈接可以是指向第三方的——這跟指向星巴克內部的資源一樣簡單。

    • 因為表示里仍然包含原來的遷移點,所以現有消費者仍然可以實現它們的目標,只不過它們可能無法享受促銷而已,因為這部分還沒有寫進它們的代碼里去。
  • 9月——消費者應用和服務都進行了有關升級,以便能夠理解并使用免費的網上促銷。

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

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

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

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

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

  甚至在非功能性方面,Web也是有益的。在我們碰到臨時故障時,HTTP操作(GETPUTDELETE)的冪等性質令我們可以進行安全的重試;內在的緩存機制既屏蔽了故障,又有助于災難恢復(通過增強的可用率);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-agemax-stalemax-fresh,它們允許客戶端指出愿意接受緩存里多舊的數據。

8
0
 
標簽:REST http
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()