基於REST的Web服務:基礎

代表性狀態傳輸(Representational State Transfer,REST)在Web領域已經得到了廣泛的接受,是基於SOAP和Web服務描述語言(Web Services Description Language,WSDL)的Web服務的更爲簡單的替代方法。接口設計方面這一轉變的關鍵證據是主流Web 2.0服務提供者(包括Yahoo、Google和Facebook)對REST的採用,這些提供者棄用或放棄了基於SOAP和WSDL的接口,而採用了更易於使用、面向資源的模型來公開其服務。在本文中,Alex Rodriguez將向您介紹REST的基本原理。

  基礎

  REST定義了一組體系架構原則,您可以根據這些原則設計以系統資源爲中心的Web 服務,包括使用不同語言編寫的客戶端如何通過HTTP處理和傳輸資源狀態。如果考慮使用它的Web服務的數量,REST近年來已經成爲最主要的Web服務設計模型。事實上,REST對Web的影響非常大,由於其使用相當方便,已經普遍地取代了基於SOAP和WSDL的接口設計。

  REST這個概念於2000年由Roy Fielding在就讀加州大學歐文分校期間在學術論文“Architectural Styles and the Design of Network-based Software Architectures”首次提出,他的論文中對使用Web服務作爲分佈式計算平臺的一系列軟件體系結構原則進行了分析,而其中提出的REST概念並沒有獲得現在這麼多關注。多年以後的今天,REST的主要框架已經開始出現,但仍然在開發中,因爲它已經被廣泛接納到各個平臺中,例如通過JSR-311 成爲了Java? 6不可或缺的部分。

  本文認爲,對於今天正在吸引如此多注意力的最純粹形式的REST Web服務,其具體實現應該遵循四個基本設計原則:

  ·顯式地使用HTTP方法。
  ·無狀態。
  ·公開目錄結構式的URI。
  ·傳輸XML、JavaScript Object Notation(JSON),或同時傳輸這兩者。

  下面幾個部分將詳述這四個原則,並提供技術原理解釋,說明爲什麼這些原則對 REST Web服務設計人員非常重要。

  顯式地使用HTTP方法

  基於REST的Web服務的主要特徵之一是以遵循RFC 2616定義的協議的方式顯式使用HTTP方法。例如,HTTP GET被定義爲數據產生方法,旨在由客戶端應用程序用於檢索資源以從Web服務器獲取數據,或者執行某個查詢並預期Web服務器將查找某一組匹配資源然後使用該資源進行響應。

  REST要求開發人員顯式地使用 HTTP方法,並且使用方式與協議定義一致。這個基本REST設計原則建立了創建、讀取、更新和刪除(create, read, update, and delete,CRUD)操作與HTTP方法之間的一對一映射。根據此映射:

  ·若要在服務器上創建資源,應該使用POST方法。
  ·若要檢索某個資源,應該使用GET方法。
  ·若要更改資源狀態或對其進行更新,應該使用PUT方法。
  ·若要刪除某個資源,應該使用DELETE方法。

  許多Web API中所固有的一個令人遺憾的設計缺陷在於將HTTP方法用於非預期用途。例如,HTTP GET請求中的請求URI通常標識一個特定的資源。或者,請求URI中的查詢字符串包括一組參數,這些參數定義服務器用於查找一組匹配資源的搜索條件。至少,HTTP/1.1 RFC是這樣描述GET方法的。但是在許多情況下,不優雅的Web API使用HTTP GET來觸發服務器上的事務性操作——例如,向數據庫添加記錄。在這些情況下,GET請求URI屬於不正確使用,或者至少不是以基於REST的方式使用。如果Web API使用GET調用遠程過程,則應該類似如下:

   GET /adduser?name=Robert HTTP/1.1

  這不是非常優雅的設計,因爲上面的Web方法支持通過HTTP GET進行狀態更改操作。換句話說,該HTTP GET請求具有副作用。如果處理成功,則該請求的結果是向基礎數據存儲區添加一個新用戶——在此例中爲Robert。這裏的問題主要在語義上。Web服務器旨在通過檢索與請求URI中的路徑(或查詢條件)匹配的資源,並在響應中返回這些資源或其表示形式,從而響應HTTP GET請求,而不是向數據庫添加記錄。從該協議方法的預期用途的角度看,然後再從與HTTP/1.1兼容的Web服務器的角度看,以這種方式使用GET是不一致的。

  除了語義之外,GET的其他問題在於,爲了觸發數據庫中的記錄的刪除、修改或添加,或者以某種方式更改服務器端狀態,它請求Web緩存工具(爬網程序)和搜索引擎簡單地通過對某個鏈接進行爬網處理,從而意外地做出服務器端更改。克服此常見問題的簡單方法是將請求URI上的參數名稱和值轉移到XML標記中。這樣產生的標記是要創建的實體的XML表示形式,可以在HTTP POST的正文中進行發送,此HTTP POST的請求URI是該實體的預期父實體(請參見清單1和2):

  清單1. 之前

  GET /adduser?name=Robert HTTP/1.1
           
  清單2. 之後

  POST /users HTTP/1.1
  Host: myserver
  Content-Type: application/xml
  <?xml version="1.0"?>
  <user>
    <name>Robert</name>
  </user>
           
  上述方法是基於REST的請求的範例:正確使用HTTP POST並將有效負載包括在請求的正文中。在接收端,可以通過將正文中包含的資源添加爲請求URI中標識的資源的從屬資源,從而處理該請求;在此例下,應該將新資源添加爲/users的子項。POST請求中指定的這種新實體與其父實體之間的包含關係類似於某個文件從屬於其父目錄的方式。客戶端設置實體與其父實體之間的關係,並在POST請求中定義新實體的URI。

  然後客戶端應用程序可以使用新的URI獲取資源的表示形式,並至少邏輯地指明該資源位於/users之下,如清單3所示。

  清單3. HTTP GET請求

  GET /users/Robert HTTP/1.1
  Host: myserver
  Accept: application/xml
 
  以這種方式使用GET是顯式的,因爲GET僅用於數據檢索。GET是應該沒有副作用的操作,即所謂的等冪性屬性。

  當支持通過HTTP GET執行更新操作時,也需要應用類似的Web方法重構,如清單4所示。

  清單4. 通過HTTP GET進行更新

  GET /updateuser?name=Robert&newname=Bob HTTP/1.1
 
  這更改了資源的name特性(或屬性)。雖然可以將查詢字符串用於此類操作,清單4就是一個簡單的例子,但是在用於較複雜的操作時,這種將查詢字符串作爲方法簽名的模式往往會崩潰。由於您的目標是顯式使用HTTP方法,鑑於上述的相同原因(請參見清單5),更符合REST的方法是發送HTTP PUT請求以更新資源,而不是發送HTTP GET。

  清單5. HTTP PUT請求

  PUT /users/Robert HTTP/1.1
  Host: myserver
  Content-Type: application/xml
   <?xml version="1.0"?>
  <user>
    <name>Bob</name>
  </user>
 
  使用PUT取代原始資源可以提供更清潔的接口,這樣的接口與REST的原則以及與HTTP方法的定義一致。清單5中的PUT請求是顯式的,因爲它通過在請求URI中標識要更新的資源來指向該資源,並且它在PUT請求的正文中將資源的新表示形式從客戶端傳輸到服務器,而不是在請求URI上將資源屬性作爲參數名稱和值的鬆散集合進行傳輸。清單5還具有將資源從Robert重命名爲Bob的效果,這樣做會將其URI更改爲/users/Bob。在REST Web服務中,使用舊的URI針對該資源的後續請求會產生標準的404 Not Found錯誤。

  作爲一般設計原則,通過在URI中使用名詞而不是動詞,對於遵循有關顯式使用 HTTP方法的REST指導原則是有幫助的。在基於REST的Web服務中,協議已經對動詞(POST、GET、PUT和DELETE)進行了定義。在理想的情況下,爲了保持接口的通用化,並允許客戶端明確它們調用的操作,Web服務不應該定義更多的動詞或遠程過程,例如/adduser或 /updateuser。這條通用設計原則也適用於HTTP請求的正文,後者旨在用於傳輸資源狀態,而不是用於攜帶要調用的遠程方法或遠程過程的名稱。

  無狀態

  REST Web服務需要擴展以滿足日益提高的性能要求。具有負載平衡和故障轉移功能、代理和網關的服務器集羣通常以形成服務拓撲的方式進行組織,從而允許根據需要將請求從一個服務器路由到另一個服務器,以減少Web服務調用的總體響應時間。要使用中間服務器擴大規模,REST Web服務需要發送完整、獨立的請求;也就是說,發送的請求包括所有需要滿足的數據,以便中間服務器中的組件能夠進行轉發、路由和負載平衡,而不需要在請求之間在本地保存任何狀態。

  完整、獨立的請求不要求服務器在處理請求時檢索任何類型的應用程序上下文或狀態。REST Web服務應用程序(或客戶端)在HTTP Header和請求正文中包括服務器端組件生成響應所需要的所有參數、上下文和數據。這種意義上的無狀態可以改進Web服務性能,並簡化服務器端組件的設計和實現,因爲服務器上沒有狀態,從而消除了與外部應用程序同步會話數據的需要。

  圖1演示了一個有狀態的服務,某個應用程序可能向其請求多頁結果集中的下一個頁面,並假設該服務跟蹤應用程序在結果集中導航時的離開位置。在這個有狀態的設計中,該服務遞增並在某個位置存儲previousPage變量,以便能夠響應針對下一個頁面的請求。

            

  圖1. 有狀態的設計
 
  類似如此的有狀態的服務變得複雜化了。在Java Platform, Enterprise Edition(Java EE)環境中,有狀態的服務需要大量的預先考慮,以高效地存儲會話數據和支持整個Java EE容器集羣中的會話數據同步。在此類環境中,存在一個Servlet/JavaServer Pages(JSP)和Enterprise JavaBeans(EJB)開發人員非常熟悉的問題,他們經常在會話複製過程中艱難地查找引發 java.io.NotSerializableException的根源。無論該異常是由Servlet容器在HttpSession複製過程中引發的,還是由EJB容器在有狀態的EJB複製過程中引發的,這都是個問題,會耗費開發人員幾天的時間,嘗試在構成服務器狀態並且有時非常複雜的對象圖表中查明沒有實現Serializable的對象。此外,會話同步增加了開銷,從而影響服務器性能。

  另一方面,無狀態的服務器端組件不那麼複雜,很容易跨進行負載平衡的服務器進行設計、編寫和分佈。無狀態的服務不僅性能更好,而且還將大部分狀態維護職責轉移給客戶端應用程序。在基於REST的Web服務中,服務器負責生成響應,並提供使客戶端能夠獨自維護應用程序狀態的接口。例如,在針對多頁結果集的請求中,客戶端應該包括要檢索的實際頁編號,而不是簡單地要求檢索下一頁(請參見圖 2)。

      

  圖2. 無狀態的設計
 
  無狀態的Web服務生成的響應鏈接到結果集中的下一個頁編號,並允許客戶端完成所需的相關工作以便保留此值。可以作爲大致的分離將基於REST的Web 服務設計的這個方面劃分爲兩組職責,以闡明如何維護無狀態的服務:

  服務器

  ·生成響應,其中包括指向其他資源的鏈接,以使得應用程序可以在相關資源之間導航。此類響應嵌入了鏈接。類似地,如果請求是針對父或容器資源,則基於REST的典型響應還可能包括指向父資源的子資源或從屬資源的鏈接,以便這些資源保持連接在一起。
  ·生成響應,其中指明瞭是否可緩存,以通過減少針對重複資源的請求數量或通過完全消除某些請求來改進性能。服務器通過包括Cache-Control和Last-Modified(日期值)HTTP響應 Header實現此目的。

  客戶端應用程序

  ·使用Cache-Control響應Header確定是否緩存資源(創建資源的本地副本)。客戶端還讀取Last-Modified響應Header,並在If-Modified-Since Header中發回日期值,以向服務器詢問資源是否已更改。這稱爲條件GET(Conditional GET),兩個Header同時進行,因爲服務器的響應爲標準304代碼(Not Modified),如果請求的資源自從該時間以後尚未更改,則省略實際的資源。HTTP響應代碼304意味着客戶端可以安全地將資源表示形式的緩存本地副本作爲最新版本使用,從而實際上跳過了後續GET請求,直到資源更改爲止。
  ·發送可獨立於其他請求得到服務的完整請求。這要求客戶端充分利用Web服務接口指定的HTTP Header,並在請求正文中發送完整的資源表示形式。客戶端發送的請求極少對先前的請求、某個會話在服務器上的存在性、服務器向請求添加上下文的能力或請求之間保留的應用程序狀態做出假設。

  客戶端應用程序與服務之間的這種協作對於基於REST的Web服務中的無狀態性極爲重要。它通過節省帶寬和最小化服務器端應用程序狀態改進了性能。

  公開目錄結構式的URI

  從對資源尋址的客戶端應用程序的角度看,URI決定了REST Web服務將具有的直觀程度,以及服務是否將以設計人員能夠預測的方式被使用。基於REST的Web服務的第三個特徵完全與URI相關。

  REST Web服務URI的直觀性應該達到很容易猜測的程度。將URI看作是自身配備文檔說明的接口,開發人員只需很少(如果有的話)的解釋或參考資料即可瞭解它指向什麼,並獲得相關的資源。爲此,URI的結構應該簡單、可預測且易於理解。

  實現這種級別的可用性的方法之一是定義目錄結構式的URI。此類URI具有層次結構,其根爲單個路徑,從根開始分支的是公開服務的主要方面的子路徑。根據此定義,URI並不只是斜槓分隔的字符串,而是具有在節點上連接在一起的下級和上級分支的樹。例如,在一個收集從Java到報紙的各種主題的討論線程服務中,您可能定義類似如下的結構化URI集合:

   http://www.myservice.org/discussion/topics/{topic}

  根/discussion之下有一個/topics節點。該節點之下有一系列主題名稱,例如閒談、技術等等,每個主題名稱指向某個討論線程。在此結構中,只需在/topics/後面輸入某個內容即可容易地收集討論線程。

  在某些情況下,指向資源的路徑尤其適合於目錄式結構。例如,以按日期進行組織的資源爲例,這種資源非常適合於使用層次結構語法。

  此示例非常直觀,因爲它基於規則:

   http://www.myservice.org/discussion/2008/12/10/{topic}

  第一個路徑片段是四個數字的年份,第二個路徑片斷是兩個數字的日期,第三個片段是兩個數字的月份。這樣解釋它可能有點愚蠢,但這就是我們追求的簡單級別。人類和計算機能夠容易地生成類似如此的結構化URI,因爲這些URI基於規則。在語法的空隙中填入路徑部分就大功告成了,因爲存在用於組合URI的明確模式:

  http://www.myservice.org/discussion/{year}/{day}/{month}/{topic}

  在考慮基於REST的Web服務的URI結構時,需要指出的一些附加指導原則包括:

  ·隱藏服務器端腳本技術文件擴展名(.jsp、.php、.asp)——如果有的話,以便您能夠移植到其他腳本技術而不用更改URI。
  ·將所有內容保持小寫。
   ·將空格替換爲連字符或下劃線(其中一種或另一種)。
  ·儘可能多地避免查詢字符串。
  ·如果請求URI用於部分路徑,與使用 404 Not Found代碼不同,應該始終提供缺省頁面或資源作爲響應。

  URI還應該是靜態的,以便在資源發生更改或服務的實現發生更改時,鏈接保持不變。這可以實現書籤功能。URI中編碼的資源之間的關係與在存儲資源的位置表示資源關係的方式無關也是非常重要的。

  傳輸XML、JSON或同時傳輸這兩者

  資源表示形式通常反映了在客戶端應用程序請求資源時的資源當前狀態及其屬性。這種意義上的資源表示形式只是時間上的快照。這可以像數據庫中的記錄表示形式一樣簡單,其中包括列名稱與XML標記之間的映射,XML中的元素值包含行值。或者,如果系統具有數據模型,那麼根據此定義,資源表示形式是系統的數據模型中的對象之一的屬性快照。這些對象就是您希望您的REST Web服務爲客戶端提供的資源。

  基於REST的Web服務設計中的最後一組約束與應用程序和服務在請求/響應有效負載或HTTP正文中交換的數據的格式有關。這是真正值得將一切保持簡單、可讀和連接在一起的方面。

  數據模型中的對象通常以某種方式相關,應該以在將資源傳輸到客戶端應用程序時表示資源的方式,反映數據模型對象(資源)之間的關係。在討論線程服務中,連接的資源表示形式的示例可能包括根討論主題及其屬性,以及指向爲該主題提供的響應的嵌入鏈接。

  清單6. 討論線程的XML表示形式

  <?xml version="1.0"?>
  <discussion date="{date}" topic="{topic}">
    <comment>{comment}</comment>
    <replies>
       <reply from="[email protected]" href="/discussion/topics/{topic}/joe"/>
      <reply from="[email protected]" href="/discussion/topics/{topic}/bob"/>
    </replies>
  </discussion>
 
  最後,爲了賦予客戶端請求最適合它們的特定內容類型的能力,您的服務的構造應該利用內置的HTTP Accept Header,其中該Header的值爲MIME類型。基於REST的服務使用的一些常見MIME類型如表1所示。

      

  表1. 基於REST的服務使用的常見MIME類型

  這使得服務可由運行在不同平臺和設備上並採用不同語言編寫的各種各樣的客戶端所使用。使用MIME類型和HTTP Accept Header是一種稱爲內容協商的機制,這種機制允許客戶端選擇適合於它們的數據格式,並最小化服務與使用服務的應用程序之間的數據耦合。

  結束語

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

  通過基於REST的API公開系統資源是一種靈活的方法,可以爲不同種類的應用程序提供以標準方式格式化的數據。它可以幫助滿足集成需求(這對於構建可在其中容易地組合(Mashup)數據的系統非常關鍵),並幫助將基於REST的基本服務集擴展或構建爲更大的集合。本文僅略微談到了基礎,但願本文的討論會誘發您繼續探索該主題。

  關於作者

  Alex Rodriguez是在IBM工作的一名軟件工程師,主要從事分佈式Java技術和REST Web服務方面的工作。他從JDK 1.1.7B發佈以來就一直在進行Java編程工作,擅長於設計和開發基於Java EE的軟件。

原文出處:http://www.ibm.com/developerworks/cn/webservices/ws-restful /index.html

發佈了31 篇原創文章 · 獲贊 11 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章