原文:http://www.ibm.com/developerworks/cn/web/1103_chenyan_restapi/html
因爲 REST 能夠下降開發的複雜度,提升系統的可伸縮性,加強系統的可擴展性,簡化應用系統之間的集成,於是獲得了廣大開發人員的喜好,同時獲得了業界普遍的支持。好比 IBM,Google 等公司的不少產品都提供了 REST API 給開發人員;與此同時,大量的開源項目和雲計算服務都提供了 REST API 接口。web
而在最近,一些新產品的開發甚至已經幾乎徹底拋棄了傳統的相似 JSP 的技術, 轉而大量使用 REST 風格的構架設計, 即在服務器端全部商業邏輯都以 REST API 的方式暴露給客戶端, 全部瀏覽器用戶界面使用 widget,Ajax,HTML5 等技術,用 HTTP 的方式與後臺直接交互。正則表達式
那麼, 在 REST API 爆炸式增加的今天, 咱們應該如何更好的設計咱們的接口, 來提升咱們的 API 的可用性,易用性,可維護性與可擴展性呢?本文將從如下方面與您探討 REST API 設計方面的最佳實踐:編程
如何規劃資源標識結構與 URI 模式json
如何根據應用場景提供內容協商api
如何正確的使用 HTTP 響應代碼數組
如何處理緩存和併發請求瀏覽器
如何利用數據冗餘和連接元素緩存
若是您具備以下知識與經驗,將有助於您閱讀和理解本文章的內容 。
REST 相關的基本知識;
HTTP 協議的基本知識;
必定的 Web 開發經驗。
REST 是英文 Representational State Transfer 的縮寫,是近年來迅速興起的,一種基於 HTTP,URI,以及 XML 這些現有協議與標準的,針對網絡應用的設計和開發方式。它能夠下降開發的複雜度,提升系統的可伸縮性。
REST 的核心是可編輯的資源及其集合,用符合 Atom 文檔標準的 Feed 和 Entry 表示。每一個資源或者集合有一個唯一的 URI。系統以資源爲中心,構建並提供一系列的 Web 服務。REST 的基本概念和原則包括:系統上的全部事物都被抽象爲資源;每一個資源對應惟一的資源標識;對資源的操做不會改變資源標識自己;全部的操做都是無狀態的;等等。
在 REST 中,開發人員顯式地使用 HTTP 方法,對系統資源進行建立、讀取、更新和刪除的操做:
使用 POST 方法在服務器上建立資源
使用 GET 方法從服務器檢索某個資源或者資源集合
使用 PUT 方法對服務器的現有資源進行更新
使用 DELETE 方法刪除服務器的某個資源
REST 中,最基本的莫過於資源標識結構和 URI 模式了。更好的規劃他們,是咱們的 API 設計取得成功的最關鍵的一步。
上文中提到,在 REST 構架的設計中,系統中的全部事物都被抽象爲資源。
在一個文檔系統中,文檔,目錄,註釋,草稿,等等,是組成系統的資源。
在一個銀行系統中,客戶信息,理財產品,利率信息,網點信息,等等,是組成系統的資源。
在一個航空客票系統中,旅客信息,機票訂單,航班信息,機場信息,等等,是組成系統的資源。
這些資源,一般在系統中以「Entry」的形式出現。下面的代碼樣例向您展現了一個常見的「Entry」資源。
REST API 請求: GET http://example.com/航班信息/CA827/entry <entry> <id> CA827 </id> <link href="航班信息 /CA827/entry" rel="self"/> <title type="text">CA827 </title> <published>2010-08-24T02:35:40.937Z </published> <updated>2010-08-24T02:35:40.937Z </updated> <ca:from>Beijing </ca:from> <ca:to>Changsha </ca:to> <ca:time>09:30:00 </ca:time> <ca:aircraft>AB330 </ca:aircraft> <summary type="text"/> </entry> |
咱們會注意到,這些資源,在描述了某種事物的同時,還有可能存在必定的層次結構關係。好比,文檔從屬於某個目錄,註釋從屬於文檔;旅客信息能夠從屬於機票訂單,也能夠從屬於某個航班。
當咱們的資源有這種層次關係的時候,咱們不妨在 URI 模式的設計中,用複合的 URI 來幫助開發者更好的理解和設計資源。
好比, 針對一個文檔的評論, 他的 URI 模式能夠設計成以下:/ 文件夾 / [ 文件夾名 ] / 文件 / [ 文件名 ] / 評論 / [ 評論惟一標示 ]。 這樣,在構造和解析 URI 的過程當中, 能夠幫助開發者更好的理解系統,設計程序。
資源,除了做爲獨立個體能夠被訪問,還能夠由多個個體組合成一個集合,在系統中,一般以「feed」的形式存在。資源的集合, 能夠是處於相同層次上,有相同從屬關係的一組資源,好比一個文件夾下的全部文件; 也能夠是根據某種條件查詢出來的查詢結果的資源集合,好比全部 30 歲以上 40 歲如下,擁有 100 萬資產以上客戶的名單。
下面,咱們來討論一下設計集合類型資源的 REST API 時須要考慮的問題。
使用過濾條件來幫助用戶更準確地獲取數據
咱們要返回的資源集合,不管是否有相同從屬關係,大部分時候都須要進行必要的過濾,提供足夠的過濾參數,查詢參數, 可以幫助開發者高效的,準確地獲取所須要的數據。 在服務器端過濾數據一般比客戶端高效,而且減小了沒必要要的數據傳輸,能夠大大減小網絡開銷,提升執行效率。
最多見的過濾條件,是經過 URL 參數實現,好比 / 環境工程系 / 學生 ? 籍貫 = 北京 & 性別 = 女。
不少時候,咱們須要制定更加複雜的過濾條件,那麼咱們能夠有兩種選擇:
首先,咱們可使用正則表達式或者服務器能夠理解的語法, 好比 / 環境工程系 / 學生 ?filter=age between (15, 18)
其次,咱們還可使用 POST 方法,攜帶一個文件來描述複雜的查詢條件,文件的格式與語法一般須要在服務器端有相應的設計與定義。不過一般 POST 方法沒有緩存機制,所以不是查詢數據的首選。
使用排序來幫助客戶端更好的展示數據
雖然進行客戶端排序對於開發者來講是件垂手可得的事情,可是直接獲得已經排序的返回結果,仍然是大部分開發者所指望的。尤爲是不少時候,咱們在瀏覽器,使用 Widget 展現結果,不適宜在客戶端存儲大量數據進行內存排序。
排序, 一般有 2 個參數, 一個是用來排序的字段, 一個是排序的升序降續方式。好比咱們能夠用支持這樣的參數組合的手段,提供基本的排序能力:?sortOrder=asc&sortField=age
使用分頁來幫助客戶端處理大量數據
因爲返回的結果可能有幾百幾千條記錄,將這些記錄一次性的返回給客戶端是不現實的,巨大的網絡流量開銷和客戶端數據區的內存開銷,都是咱們在應用開發的時候不但願看到的,所以,若是你的集合資源有可能有大量的數據返回,請務必提供分頁的功能支持。
咱們一般用一個以上參數來制定一個返回結果的區域,比較常見的有下面兩種:
一種常見於用固定行數的表格來展現數據,用當前處於第幾頁和每頁返回多少行數據來肯定須要的數據, 好比 / 全部學生 ?page=5&pagesize=50
另一種常見於用更加靈活的界面展現數據,用從第幾行開始,一共返回多少行數據來肯定須要的數據, 好比 / 全部學生 ?startIndex=27&count=22
下面是一個來自 IBM developerWorks 的 API 樣例,嘗試請求該 API,你能夠看到該集合很好的支持告終果的分頁與排序。同時咱們從返回的信息中能夠看到,每一個文檔 Entry 的 URI 都按照 / 社區庫 /[ 社區庫 ID]/ 文檔 /[ 文檔 ID] 的複合 URI 的模式設計的。
清單 2. IBM developerWorks 的某個社區文件庫的集合資源的 API
REST API 請求: GET https://www.ibm.com/developerworks/mydeveloperworks /files/form/anonymous/api/communitylibrary /0a7c97bb-6cf9-4ddb-a918-80994e7b444d/feed? pageSize=5&page=1&sK=modified&sO=dsc |
理想的 REST 世界,一切事物都抽象爲資源,一切操做都抽象爲增刪改查。然而,全部事物與操做均可以很容易的按照這個規則做抽象嗎?讓咱們看看這個例子:
檢入和檢出一個文檔 ---- 這個時候,咱們要處理的資源是一個文檔,然而增刪改查彷佛都沒法與「檢入檢出」這個動做進行對應。固然,咱們能夠在文檔資源中,設計一個檢入檢出狀態的元素,經過編輯文檔資源來實現。可是,這種設計從天然語義上看,並非很貼切;而且增長了資源編輯操做的複雜度。
若是咱們來定義一個新的集合 ----「我檢出的文檔」,用建立一個集合資源來對應檢出(建立一個文檔鎖),用刪除一個集合資源來對應檢入(刪除一個文檔鎖), 是否是邏輯上能夠變得更加清楚?
在 REST 這個以名詞爲核心的構架結構中,當你遇到一些動詞特性比較強的操做,而又很難用原始資源的增刪改查來匹配的時候,不妨換個思路, 經過引入新的邏輯資源集合的方式, 來進行 API 的設計與規劃。
咱們的開發者在發送一個 REST API 請求的同時,根據應用場景,針對相同的資源,可能會期待不一樣的返回形式。
好比,我但願根據用戶客戶端語言,同一個資源的內容能夠返回不一樣的語言。又好比,當我使用 Java 編程的時候,我但願獲得 ATOM 格式的返回結果,而當我使用 JavaScript 編程的時候,我但願獲得 Json 格式的返回結果。
所以,咱們在設計 REST API 的時候,應該提供完備的內容協商能力。
最容易想到的天然是經過 URL 參數進行控制,咱們常常看到形如 / 航班號 /entry? format=JSON 這樣的 URL。這種方式的優點就是簡單靈活, 你能夠經過任何 URL 參數來組合你的輸出格式。
下面是一個來自 IBM developerWorks 的 API 樣例,嘗試請求該 API,你能夠看到該集合是如何支持不一樣的輸出格式請求的。
清單 3. IBM developerWorks 的文件服務標籤雲的 API
REST API 請求,要求返回 XML 格式數據: GET https://www.ibm.com/developerworks/mydeveloperworks /files/form/anonymous/api/tags/feed?format=xml &scope=document&pageSize=30&sK=cloud&sO=dsc REST API 請求,要求返回 JSON 格式數據: GET https://www.ibm.com/developerworks/mydeveloperworks /files/form/anonymous/api/tags/feed?format=json &scope=document&pageSize=30&sK=cloud&sO=dsc |
使用 URL 參數,簡單靈活,可是也由此帶來了設計上的隨意和不標準。而且,過多的參數會致使 URL 的可讀性變差,更有甚者,可能會致使 URL 過長,超出規範,API 請求沒法執行。
更爲標準的內容協商方式是使用 HTTP 頭。咱們一般使用 Accept 來設置咱們接受的返回結果的內容格式,用 Accept-Charset 來設置字符集,用 Accept-Encoding 來設置數據傳輸格式,用 Accept-Language 來設置語言。
還有一種模式,就是將協商設置直接做爲 URI 的一部分,將不一樣的返回視爲不一樣的資源,好比 / 航班號 /json 來返回 JSON 格式的結果,用 / 航班號 /atom 來返回 ATOM 格式的結果。
做爲 API 的設計者,正確的將 API 執行結果和失敗緣由用清晰簡潔的方式傳達給客戶程序是十分關鍵的一步。 咱們確實能夠在 HTTP 的相應內容中描述是否成功,若是出錯是由於什麼, 然而, 這就意味着用戶須要進行內容解析,才知道執行結果和錯誤緣由。所以,HTTP 響應代碼能夠保證客戶端在第一時間用最高效的方式獲知 API 運行結果,並採起相應動做。 下表列出了比較經常使用的響應代碼。
HTTP 響應代碼 | 代碼含義 |
200 | 已建立,請求成功且服務器已建立了新的資源。 |
201 | 是否只顯示處於警告狀態的應用實例 |
301 | 重定向 , 請求的網頁已被永久移動到新位置。服務器返回此響應時,會自動將請求者轉到新位置。 |
302 | 重定向 , 請求的網頁臨時移動到新位置,但求者應繼續使用原有位置來進行之後的請求。302 會自動將請求者轉到不一樣的臨時位置。 |
304 | 未修改,自從上次請求後,請求的網頁未被修改過。服務器返回此響應時,不會返回網頁內容。 |
400 | 錯誤請求 , 服務器不理解請求的語法。 |
401 | 未受權 , 請求要求進行身份驗證。 |
403 | 已禁止 , 服務器拒絕請求。 |
404 | 未找到 , 服務器找不到請求的網頁。 |
405 | 方法禁用 , 禁用請求中所指定的方法。 |
406 | 不接受 , 沒法使用請求的內容特性來響應請求的網頁。 |
408 | 請求超時 , 服務器等候請求時超時。 |
410 | 已刪除 , 若是請求的資源已被永久刪除,那麼,服務器會返回此響應。 |
412 | 未知足前提條件 , 服務器未知足請求者在請求中設置的其中一個前提條件。 |
415 | 不支持的媒體類型 , 請求的格式不受請求頁面的支持。 |
500 | 內部服務器錯誤。 |
緩存和併發處理,歷來是大型軟件系統設計中的重要組成部分。
在 REST 的構架中,咱們除了在與後臺的數據交換中,須要有一個良好的緩存機制外,針對 REST API 請求都是在遠端用 HTTP 發起這一特色,還須要爲網絡緩存進行更多考慮。經過減小 HTTP 響應內容,避免沒必要要的 HTTP 鏈接等方式,達到提升 REST API 使用效率的目的。
HTTP 頭中,有多個字段能夠用於緩存處理。比較經常使用的有緩存控制和條件請求。
緩存控制:
緩存控制一般是須要客戶端,緩存服務器 / 代理服務器與業務服務器一塊兒發生做用。
HTTP 頭中有「Cache-control」字段來控制如何使用緩存,常見的取值有 private、no-cache、max-age、must-revalidate 等。好比當你給返回的數據內容設置 max-age=600,那麼當用戶隔了 30 秒再次請求的時候,就不會致使從新請求後臺數據。
另外,也能夠經過「Expires」字段來指定內容過時時間,在此時間前的請求都不會致使後臺程序從新請求數據。
下圖展現了 max-age 是如何工做的。
條件請求與電子標籤:
不少時候,數據內容可能會幾個小時甚至幾天都不會發生變更,這個時候根據請求時間間隔來控制緩存,就不能知足系統的需求了。經過支持條件請求與電子標籤,能夠幫助咱們來解決這個問題。
當用戶請求數據內容時,系統在返回數據的同時,在 HTTP 頭中,將返回根據服務器內容的最後修改時間 Last-Modified,或者根據服務器內容生成電子標籤 ETag。 當用戶再次請求數據時,就能夠在 HTTP 請求中使用 If-Modified-Since 或者 If-None-Match 頭信息,把上次請求獲得的時間戳或者電子標籤傳給服務器。當收到一個有條件請求的 HTTP 頭的 REST 請求的時候,咱們的程序須要將收到的時間戳或者電子標籤與當前內容做比較,就能夠很容易的知道用戶請求的數據內容在這段時間是否發生過修改,並根據比較結果返回給用戶最新內容,或者用 HTTP 響應碼 304 告知用戶,內容沒有變化。
下面是一個來自 IBM developerWorks 的 API 樣例,嘗試請求該 API,你能夠看到該 API 會在 HTTP 頭中返回電子標籤和緩存處理信息。
清單 4. IBM developerWorks 的帶有電子標籤的文件服務 API
REST API 請求: GET https://www.ibm.com/developerworks/mydeveloperworks /form/anonymous/api/communitylibrary /7e2e8015-bf72-43b6-bacd-36565b67febc/document /ddc0ef4e-224e-449c-bb2c-f919fafb17d2 /entry?acls=true&includeRecommendation=true &includeTags=false&includeLibraryInfo=true&format=xml |
上文咱們提到了使用條件請求控制緩存,其實咱們還可使用條件請求進行併發處理。
好比當用戶 Alice 和 Bob 經過 REST 獲取了一篇文檔。Bob 閱讀文檔以後,經過 PUT 來修改文檔;而此前幾分鐘,Alice 剛剛修改了這篇文檔,因而 Bob 就在絕不知情的狀況下不慎覆蓋了 Alice 的修改。
經過在寫操做中支持條件請求,咱們能夠更好的處理併發修改。用戶在發出修改請求的同時,在 HTTP 請求中使用 If-Not-Modified-Since 或者 If-Match 頭信息,把獲取數據時獲得的時間戳或者電子標籤傳給服務器;咱們的程序經過與服務器當前內容的比較,就能夠知道,這個修改請求是不是針對當前內容提出的。當服務器發現內容已經被其餘用戶修改過了,就不會執行修改請求,並返回 HTTP 響應碼 412(未知足前提條件)給用戶。
下圖展現了使用條件請求和電子標籤進行併發處理是如何工做的
在 ATOM 文檔中,咱們用各類數據元素來傳遞信息。其中有一類元素叫作連接,能夠用於開發者的進一步訪問。一般,咱們會提供編輯當前資源的連接,訪問當前資源的連接,等等。經過更加靈活的使用這類連接元素,以及提供必要的數據冗餘,咱們能夠大大簡化開發者的編程邏輯,提升 REST API 的使用效率 。
咱們在一個航班信息的文檔中,一般會包括飛機的型號;而咱們可能常常須要在顯示航班信息的時候,同時顯示更多的飛機信息(如單通道仍是雙通道,載客人數等)。這個時候,咱們就須要對飛機型號的資源再發起一次請求,才能得到咱們須要的信息。若是咱們能夠在請求航班信息的時候,返回飛機型號的同時得到更多的該型號的信息,就能夠減小一次網絡鏈接。爲了保證 API 的靈活與效率,咱們能夠提供一個開關參數,如 includeAircraftDetail=true。
咱們要展現一個文件夾下面全部的文件,並容許用戶察看每一個文件都容許哪些人編輯,哪些人下載以及將某文件放入收藏夾。這時候,咱們能夠考慮將這些能夠執行的操做的 API 都用連接元素的方式返回給客戶端,這樣,開發者無需本身拼接 API 調用的 URL,就可使用,從而下降代碼複雜度。
REST API 請求: GET http://example.com/doc/docID12345/entry <entry> <id> docID12345</id> <link href="doc/docID12345/entry" rel="self"/> <title type="text">Doc Title </title> <published>2010-08-28T02:35:40.937Z </published> <updated>2010-08-28T02:35:40.937Z </updated> <atom:link doc:rel="reader" href="/doc/docID12345/reader/feed" rel="related" type="application/atom+xml"/> <atom:link doc:rel="editor" href="/doc/docID12345/editor/feed" rel="related" type="application/atom+xml"/> <atom:link ca:rel="collect" href="/doc/collect?Add=docID12345" rel="related" type="application/atom+xml"/></entry> |
除了以上提到的方面,還有大量的細節與技巧,能夠幫助咱們更好的設計 REST API:
批量更新:
當用戶須要更新多個資源的時候,你打算讓開發者一次次的發送 HTTP 請求逐個更新嗎?你能夠考慮在設計 API 的時候容許客戶同時建立或者更新多個資源。
REST 安全:
除了使用固有的 HTTP 基本驗證,你還能夠考慮經過支持表單驗證,LTPA 驗證,Open ID 驗證等方式,來知足更多的企業安全要求。
文檔服務:
是否因爲 API 持續更新,使得客戶端鏈接不一樣版本服務的時候疲於奔命?嘗試着把你的 API 定義規範成 XML 文檔,這樣客戶端很容易理解當前服務能夠提供哪些功能,以及如何使用這些功能。
你還能夠經過閱讀其餘文檔獲得更多這方面的指導,本文沒法將全部的細節與技巧一一窮盡。
經過以上的經驗介紹和技巧舉例,咱們學習到了如何應用最佳實踐來更好的設計 REST API。咱們注意到,因爲 REST API 主要針對網絡應用, 而且大量調用來自於瀏覽器腳本,所以在細節上有不少本身獨特的須要注意的技巧。此外,因爲 REST 愈來愈成爲一種系統設計的原則與構架,也要求咱們的程序開發人員在設計 API 的時候須要用構架師的視角與高度來思考。但願本文可以幫助您打開 REST API 設計的思路,摸索和總結出更多的技巧,與廣大開發人員分享。