REST 定義了一組體系架構原則,您能夠根據這些原則設計以系統資源爲中心的 Web 服務,包括使用不一樣語言編寫的客戶端如何經過 HTTP 處理和傳輸資源狀態。 若是考慮使用它的 Web 服務的數量,REST 近年來已經成爲最主要的 Web 服務設計模型。 事實上,REST 對 Web 的影響很是大,因爲其使用至關方便,已經廣泛地取代了基於 SOAP 和 WSDL 的接口設計。php
REST 這個概念於 2000 年由 Roy Fielding 在就讀加州大學歐文分校期間在學術論文「Architectural Styles and the Design of Network-based Software Architectures」(請參見參考資料以獲取此論文的連接)首次提出,他的論文中對使用 Web 服務做爲分佈式計算平臺的一系列軟件體系結構原則進行了分析,而其中提出的 REST 概念並無得到如今這麼多關注。 多年之後的今天,REST 的主要框架已經開始出現,但仍然在開發中,由於它已經被普遍接納到各個平臺中,例如經過 JSR-311 成爲了 Java™ 6 不可或缺的部分。html
本文認爲,對於今天正在吸引如此多注意力的最純粹形式的 REST Web 服務,其具體實現應該遵循四個基本設計原則:java
下面幾個部分將詳述這四個原則,並提供技術原理解釋,說明爲何這些原則對 REST Web 服務設計人員很是重要。web
回頁首數據庫
基於 REST 的 Web 服務的主要特徵之一是以遵循 RFC 2616 定義的協議的方式顯式使用 HTTP 方法。例如,HTTP GET 被定義爲數據產生方法,旨在由客戶端應用程序用於檢索資源以從 Web 服務器獲取數據,或者執行某個查詢並預期 Web 服務器將查找某一組匹配資源而後使用該資源進行響應。json
REST 要求開發人員顯式地使用 HTTP 方法,而且使用方式與協議定義一致。 這個基本 REST 設計原則創建了建立、讀取、更新和刪除(create, read, update, and delete,CRUD)操做與 HTTP 方法之間的一對一映射。 根據此映射:緩存
許多 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):restful
GET /adduser?name=Robert HTTP/1.1
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 所示。
GET /users/Robert HTTP/1.1 Host: myserver Accept: application/xml
以這種方式使用 GET 是顯式的,由於 GET 僅用於數據檢索。 GET 是應該沒有反作用的操做,即所謂的等冪性 屬性。
當支持經過 HTTP GET 執行更新操做時,也須要應用相似的 Web 方法重構,如清單 4 所示。
GET /updateuser?name=Robert&newname=Bob HTTP/1.1
這更改了資源的 name
特性(或屬性)。 雖然能夠將查詢字符串用於此類操做,清單 4 就是一個簡單的例子,可是在用於較複雜的操做時,這種將查詢字符串做爲方法簽名的模式每每會崩潰。 因爲您的目標是顯式使用 HTTP 方法,鑑於上述的相同緣由(請參見清單 5),更符合 REST 的方法是發送 HTTP PUT 請求以更新資源,而不是發送 HTTP GET。
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
變量,以便可以響應針對下一個頁面的請求。
相似如此的有狀態的服務變得複雜化了。 在 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)。
無狀態的 Web 服務生成的響應連接到結果集中的下一個頁編號,並容許客戶端完成所需的相關工做以便保留此值。 能夠做爲大體的分離將基於 REST 的 Web 服務設計的這個方面劃分爲兩組職責,以闡明如何維護無狀態的服務:
服務器
客戶端應用程序
客戶端應用程序與服務之間的這種協做對於基於 REST 的 Web 服務中的無狀態性極爲重要。 它經過節省帶寬和最小化服務器端應用程序狀態改進了性能。
從對資源尋址的客戶端應用程序的角度看,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 結構時,須要指出的一些附加指導原則包括:
URI 還應該是靜態的,以便在資源發生更改或服務的實現發生更改時,連接保持不變。 這能夠實現書籤功能。 URI 中編碼的資源之間的關係與在存儲資源的位置表示資源關係的方式無關也是很是重要的。
資源表示形式一般反映了在客戶端應用程序請求資源時的資源當前狀態及其屬性。 這種意義上的資源表示形式只是時間上的快照。 這能夠像數據庫中的記錄表示形式同樣簡單,其中包括列名稱與 XML 標記之間的映射,XML 中的元素值包含行值。 或者,若是系統具備數據模型,那麼根據此定義,資源表示形式是系統的數據模型中的對象之一的屬性快照。 這些對象就是您但願您的 REST Web 服務爲客戶端提供的資源。
基於 REST 的 Web 服務設計中的最後一組約束與應用程序和服務在請求/響應有效負載或 HTTP 正文中交換的數據的格式有關。 這是真正值得將一切保持簡單、可讀和鏈接在一塊兒的方面。
數據模型中的對象一般以某種方式相關,應該以在將資源傳輸到客戶端應用程序時表示資源的方式,反映數據模型對象(資源)之間的關係。 在討論線程服務中,鏈接的資源表示形式的示例可能包括根討論主題及其屬性,以及指向爲該主題提供的響應的嵌入連接。
<?xml version="1.0"?> <discussion date="{date}" topic="{topic}"> <comment>{comment}</comment> <replies> <reply from="joe@mail.com" href="/discussion/topics/{topic}/joe"/> <reply from="bob@mail.com" href="/discussion/topics/{topic}/bob"/> </replies> </discussion>
最後,爲了賦予客戶端請求最適合它們的特定內容類型的能力,您的服務的構造應該利用內置的 HTTP Accept Header,其中該 Header 的值爲 MIME 類型。 基於 REST 的服務使用的一些常見 MIME 類型如表 1 所示。
MIME-Type | Content-Type |
---|---|
JSON | application/json |
XML |
application/xml |
XHTML | application/xhtml+xml |
這使得服務可由運行在不一樣平臺和設備上並採用不一樣語言編寫的各類各樣的客戶端所使用。 使用 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 的基本服務集擴展或構建爲更大的集合。 本文僅略微談到了基礎,希望本文的討論會誘發您繼續探索該主題。
http://www.ibm.com/developerworks/cn/webservices/ws-restful/