我所理解的RESTful Web API [設計篇]

我所理解的RESTful Web API [Web標準篇]》 Web服務已經成爲了異質系統之間的互聯與集成的主要手段,在過去一段不短的時間裏,Web服務幾乎清一水地採用SOAP來構建。構建REST風格的 Web服務是最近兩三年風行的潮流,所以很多人以爲REST是一個事物。而事實卻是:REST自其誕生之日起到現在(2014年)已經有14年了,它爲什 麼叫這麼一個“奇怪”的名字呢?

目錄      
一、爲什麼叫這個“奇怪”的名字?二、採用URI標識資源      
二、採用URI標識資源      
三、使用“鏈接”關聯相關的資源      
四、使用統一的接口      
五、使用標準的HTTP方法      
六、支持多種資源表示方式      
七、無狀態性

一、爲什麼叫這個“奇怪”的名字?

06074821-458cc97986cc44a9980d80d23699217

2000 年,Roy Thomas Fielding博士在他那篇著名的博士論文《Architectural Styles and the Design of Network-based Software Architectures》中提出了幾種軟件應用的架構風格,REST作爲其中的一種架構風格在這篇論文的第5章中進行了概括性的介紹。我個人建議本書 的讀者都能讀讀這篇論文,原文和中文譯文都可以從網絡上找到。

REST是“REpresentational State Transfer”的縮寫,可以翻譯成“表現狀態轉換”,但是在絕大多數場合中我們只說REST或者RESTful。爲什麼會起這麼一個奇怪的名字呢?我 們可以從上述這篇論文中找到答案。Fielding在論文中將REST定位爲“分佈式超媒體應用(Distributed Hypermedia System)”的架構風格,它在文中提到一個名爲“HATEOAS(Hypermedia as the engine of application state)”的概念。

我 們利用一個面向最終用戶的Web應用來對這個概念進行簡單闡述:這裏所謂的應用狀態(Application State)表示Web應用的客戶端的狀態,簡單起見可以理解爲會話狀態。資源在瀏覽器中以超媒體的形式呈現,通過點擊超媒體中的鏈接可以獲取其它相關的 資源或者對當前資源進行相應的處理,獲取的資源或者針對資源處理的響應同樣以超媒體的形式再次呈現在瀏覽器上。由此可見,超媒體成爲了驅動客戶端會話狀態 的轉換的引擎。

藉助於超媒體這種特殊的資源呈現方式,應用狀態的轉換體現爲瀏覽器中呈現資源的轉換。如果將超媒體進一步抽象成一般意義上的資源呈現(Representation )方式,那麼應用狀態變成了可被呈現的狀態(REpresentational State)。應用狀態之間的轉換就成了可被呈現的狀態裝換(REpresentational State Transfer),這就是REST。

REST 在我看來是一種很籠統的概念,它代表一種架構風格。對於多個Web應用採用的架構,我們只能說其中某一個比其它的更具有REST風格,而不能簡單粗暴地 說:“它採用了REST架構而其它的沒有”。爲了將REST真正地落地,Lenoard Rechardson & Sam Ruby在《RESTful Web Services》一書中提出了一種名爲“面向資源的架構(ROA: Resource Oriented Architecture)”。該書中介紹了一些採用ROA架構的Web服務應該具備的基本特徵,它們可以指導我們如果構架具體的RESTful Web API。

二、採用URI標識資源

SOAP Web API採用RPC風格,它採用面向功能的架構,所以我們在設計SOAP Web API的時候首相考慮的是應高提供怎樣的功能(或者操作)。RESTful Web API採用面向資源的架構,所以在設計之初首先需要考慮的是有哪些資源可供操作。

資 源是一個很寬泛的概念,任何寄宿於Web可供操作的“事物”均可視爲資源。資源可以體現爲經過持久化處理保存到磁盤上的某個文件或者數據庫中某個表的某條 記錄,也可以是Web應用接受到請求後採用某種算法計算得出的結果。資源可以體現爲一個具體的物理對象,它也可以是一個抽象的流程。

一 個資源必須具有一個或者多個標識,既然我們設計的Web API,那麼很自然地應該採用URI來作爲資源的標識。作爲資源標識的URI最好具有“可讀性”,因爲具有可讀性的URI更容易被使用,使用者一看就知道 被標識的是何種資源,比如如下一些URI就具有很好的可讀性。

  • http://www.artech.com/employees/c001(編號C001的員工)

  • http://www.artech.com/sales/2013/12/31(2013年12月31日的銷售額)

  • http://www.artech.com/orders/2013/q4(2013年第4季度簽訂的訂單)

除 了必要的標誌性和可選的可讀性之外,標識資源的URI應該具有“可尋址性(Addressability)”。也就是說,URI不僅僅指明瞭被標識資源所 在的位置,而且通過這個URI可以直接獲取目標資源。通過前面的介紹 我們知道URI具有URL和URN兩種主要的表現形式,只要前者具有可尋址性,所以我們最好採用一個URL作爲資源的標識。

URI除了 可以標識某個獨立的資源外(比如“http://www.artech.com/employees/c001”),還可以標識一組資源的集合或者資源的 容器(比如“http://www.artech.com/orders/2013/q4”)。當然,一組同類資源的集合或者存放一組同類資源的容器本身 也可以視爲另一種類型的複合型(Composite)資源,所以“URI總是標識某個資源”這種說法是沒有問題的。

三、使用“鏈接”關聯相關的資源

在 絕大多數情況下,資源並不會孤立地存在,必然與其它資源具有某種關聯。既然我們推薦資源採用具有可尋址性的URL來標識,那麼我們就可以利用它來將相關的 資源關聯起來。比如我們採用XML來表示一部電影的信息,那麼我們採用如下的形式利用URL將相關的資源(導演、領銜主演、主演、編劇以及海報)關聯在一 起。實際上這可以視爲一份超文本/超媒體文檔。當用戶得到這樣一份文檔的時候,可以利用自身的內容獲得某部影片基本的信息,還可以利用相關的“鏈接”得到其它相關內容的詳細信息。

   1: <movie>

   

   2:   <name>魔鬼代言人</name>

   

   3:   <genre>劇情|懸疑|驚悚</genre>

   

   4:   <directors>

   

   5:     <add ref="http://www.artech.com/directors/taylor-hackford">泰勒.海克福德</add>

   

   6:   </directors>

   

   7:   <starring>

   

   8:     <add ref = "http://www.artech.com/actors/al-pacino">阿爾.帕西諾</add>

   

   9:     <add ref = "http://www.artech.com/actors/keanu-reeves ">基諾.李維斯</add>

   

  10:   </starring>

   

  11:   <supportingActors>

   

  12:     <add ref = "http://www.artech.com/actors/charlize-theron ">查理茲.塞隆</add>

   

  13:     <add ref = "http://www.artech.com/actors/jeffrey-jones ">傑弗瑞.瓊斯</add>

   

  14:     <add ref = "http://www.artech.com/actors/connie-nielsen">康尼.尼爾森</add>

   

  15:   </supportingActors>

   

  16:   <scriptWriters>

   

  17:     <add ref = "http://www.artech.com/scriptwriters/jonathan-lemkin">喬納森萊姆金</add>

   

  19:     <add ref = "http://www.artech.com/scriptwriters/tony-gilroy">託尼吉爾羅伊 </add>

   

  20:   </scriptWriters>

   

  21:   <language>英語</language>

   

  22:   <poster ref = "http://www.artech.com/p_w_picpaths/the-devil-s-advocate"/>

   

  23:   <story>...</story>

   

  24: </movie>

Fielding在他的論文中將REST定位爲“分佈式超媒體應用”的架構風格,而超媒體的核心就是利用“鏈接”相關的信息結成一個非線性的網,所以從一點也可以看出REST和“使用鏈接關聯相關的資源”這個特性使吻合的。

四、使用統一的接口

由於REST是面向資源的,所以一個Web API旨在實現針對單一資源的操作。我們在前面已經說個,針對資源的基本操作唯CRUD而已,這是使我們可以爲Web API定義標準接口成可能。所謂的標準接口就是針對不同資源的Web API定義一致性的操作來操作它們,其接口可以採用類似於下面的模式。

   1: public class ResourceService

   

   2: {

   

   3:     public IEnumerable<Resource>[] Get();

   

   4:     public void Create(Resource resource);

   

   5:     public void Update(Resource resource);

   

   6:     public void Delete(string id);

   

   7: }

能否採用統一接口是RESTful Web API和採用RPC風格的SOAP Web服務又一區別。如果採用RPC風格的話,我們在設計Web API的時候首先考慮的是具體哪些功能需要被提供,所以這樣的Web API是一組相關功能的集合而已。

以一個具體的場景爲例。現在我們需要設計一個Web API來管理用於授權的角色,它只需要提供針對角色本身的CRUD的功能以及建立/解除與用戶名之間的映射關係。如果我們將其定義成針對SOAP的Web服務,其服務接口具有類似於如下的結構。

   1: public class RoleService

   

   2: {

   

   3:     public IEnumerable<string> GetAllRoles();

   

   4:     public void CreateRole(string roleName);

   

   5:     public void DeleteRole(string roleName);

   

   6:

   

   7:     public void AddRolesInUser(string userName, string[] roleNames);

   

   8:     public void RemoveRolesFromUser(string userName, string[] roleNames);

   

   9: }

如下我們需要將其定義成一個純粹的RESTful的Web API,只有前面三個方法在針對角色的CRUD操作範疇之內,但是後面兩個方法卻可以視爲針對“角色委派(Role Assignment)”對象的添加和刪除操作。所以這裏實際上涉及到了兩種資源,即角色和角色委派。爲了使Web API具有統一的接口,我們需要定義如下兩個Web API。

   1: public class RolesService

   

   2: {

   

   3:     public IEnumerable<string> Get();

   

   4:     public void Create(string roleName);

   

   5:     public void Delete(string roleName);

   

   6: }

   

   7:

   

   8: public class RoleAssignmentsService

   

   9: {

   

  10:     public void Create(RoleAssignment roleName);

   

  11:     public void Delete(RoleAssignment roleName);

   

  12: }

 

五、使用標準的HTTP方法

由於RESTful Web API採用了同一的接口,所以其成員體現爲針對同一資源的操作。對於Web來說,針對資源的操作通過HTTP方法來體現。我們應該將兩者統一起來,是Web API分別針對CRUD的操作只能接受具有對應HTTP方法的請求。

我們甚至可以直接使用HTTP方法名作爲Web API接口的方法名稱,那麼這樣的Web API接口就具有類似於如下的定義。對於ASP.NET Web API來說,由於它提供了Action方法名稱和HTTP方法的自動映射,所以如果我們採用這樣的命名規則,就無需再爲具體的Action方法設定針對 HTTP方法的約束了。

   1: public class ResourceService

   

   2: {

   

   3:     public IEnumerable<Resource>[] Get();

   

   4:     public void Post(Resource resource);

   

   5:     public void Put(Resource resource);

   

   6:     public void Patch (Resource resource);

   

   7:     public void Delete(string id);

   

   8:

   

   9:     public void Head(string id);

   

  10:     public void Options();

   

  11: }

上面代碼片斷提供的7個方法涉及到了7個常用的HTTP方法,接下來我們針對資源操作的語義對它們作一個簡單的介紹。首先GET、HEAD和 OPTIONS這三個HTTP方法旨在發送請求以或者所需的信息。對於GET,相應所有人對它已經非常熟悉了,它用於獲取所需的資源,服務器一般講對應的 資源置於響應的主體部分返回給客戶端。

HEAD和OPTIONS相對少見。從資源操作的語義來講,一個針對某個目標資源發送的HEAD請求一般不是爲了獲取目標資源本身的內容,而是得到描述目標資源的元數據信息。 服務器一般講對應資源的元數據置於響應的報頭集合返回給客戶端,這樣的響應一般不具有主體部分。OPTIONS請求旨在發送一種“探測”請求以確定針對某 個目標地址的請求必須具有怎樣的約束(比如應該採用怎樣的HTTP方法以及自定義的請求報頭),然後根據其約束髮送真正的請求。比如針對“跨域資源”的預 檢(Preflight)請求採用的HTTP方法就是OPTIONS。

至於其它4中HTTP方法(POST、PUT、PATCH和DELETE),它們旨在針對目標資源作添加、修改和刪除操作。對於DELETE,它的語義很明確,就是刪除一個已經存在的資源。我們着重推薦其它三個旨在完成資源的添加和修改的HTTP方法作一個簡單的介紹。

通過發送POST和PUT請求均可以添加一個新的資源,但是兩者的不同之處在於:對於前者,請求着一般不能確定標識添加資源最終採用的URI,即服務端最終爲成功添加的資源指定URI;對於後者,最終標識添加資源的URI是可以由請求者控制的。也正是因爲這個原因,如果發送PUT請求,我們一般直接將標識添加資源的URI作爲請求的URI;對於POST請求來說,其URI一般是標識添加資源存放容器的URI。

比如我們分別發送PUT和POST請求以添加一個員工,標識員工的URI由其員工ID來決定。如果員工ID由客戶端來指定,我們可以發送PUT請 求;如果員工ID由服務端生成,我們一般發送POST請求。具體的請求與下面提供的代碼片斷類似,可以看出它們的URI也是不一樣的。

   1: PUT http://www.artech.com/employees/300357 HTTP/1.1

   

   2: ...

   

   3:

   

   4: <employee>

   

   5:   <id>300357</id>

   

   6:   <name>張三</name>

   

   7:   <gender>男<gender>

   

   8:   <birthdate>1981-08-24</birthdate>

   

   9:   <department>3041</department>

   

  10: </employee>

 

   1: POST http://www.artech.com/employees HTTP/1.1

   

   2: ...

   

   3:

   

   4: <employee>

   

   5:   <name>張三</name>

   

   6:   <gender>男<gender>

   

   7:   <birthdate>1981-08-24</birthdate>

   

   8:   <department>3041</department>

   

   9: </employee>

POST和PUT請求一般將所加資源的內容置於請求的主體。但是對於PUT請求來說,如果添加資源的內容完全可以由其URI來提供,這樣的請求可以 不需要主體。比如我們通過請求添加一個用於控制權限的角色,標識添加角色的URI由其角色名稱來決定,並且不需要指定除角色名稱的其它信息,那麼我們只要 發送如下一個不含主體的PUT請求即可。

   1: PUT http://www.artech.com/roles/admin HTTP/1.1

   

   2:

   

   3: ...

除了進行資源的添加,PUT請求還能用於資源的修改。由於請求包含提交資源的標識(可以放在URI中,也可以置於保存在主體部分的資源內容中),所以服務端能夠定位到對應的資源予以修改。對於POST和PUT,也存在一種一刀切的說法:POST用於添加,PUT用於修改。我個人比較認可的是:如果PUT提供的資源不存在,則做添加操作,否則做修改

對於發送PUT請求以修改某個存在的資源,服務器一般會將提供資源將原有資源整體“覆蓋”掉。如果需要進行“局部”修改,我們推薦請求採用PATCH方法,因爲從語義上講“Patch”就是打補丁的意思。

安全性與冪等性

關於HTTP請求採用的這些個方法,具有兩個基本的特性,即“安全性”和“冪等性”。對於上述7種HTTP方法,GET、HEAD和OPTIONS均被認爲是安全的方法,因爲它們旨在實現對數據的獲取,並不具有“邊界效應(Side Effect[1])”。至於其它4個HTTP方法,由於它們會導致服務端資源的變化,所以被認爲是不安全的方法。

冪等性(Idempotent)是一個數學上的概念,在這裏表示發送一次和多次請求引起的邊界效應是一致的。在網速不夠快的情況下,客戶端發送一個請求後不能立即得到響應,由於不能確定是否請求是否被成功提交,所以它有可能會再次發送另一個相同的請求,冪等性決定了第二個請求是否有效。

上述3種安全的HTTP方法(GET、HEAD和OPTIONS)均是冪等方法。由於DELETE和PATCH請求操作的是現有的某個資源,所以它們是冪等方法。對於PUT請求,只有在對應資源不存在的情況下服務器纔會進行添加操作,否則只作修改操作,所以它也是冪等方法。至於最後一種POST,由於它總是進行添加操作,如果服務器接收到兩次相同的POST操作,將導致兩個相同的資源被創建,所以這是一個非冪等的方法。

當我們在設計Web API的時候,應該儘量根據請求HTTP方法的冪等型來決定處理的邏輯。由於PUT是一個冪等方法,所以攜帶相同資源的PUT請求不應該引起資源的狀態變化,如果我們在資源上附加一個自增長的計數器表示被修改的次數,這實際上就破壞了冪等型。

不過就我個人的觀點來說,在有的場合下針對冪等型要求可以不需要那麼嚴格。舉個例子,我對於我們開發的發部分應用來說,數據表基本上都有一個名爲LastUpdatedTime的 字段表示記錄最後一次被修改的時間,因爲這是爲了數據安全審覈(Auditing)的需要。在這種情況下,如果接收到一個基於數據修改的PUT請求,我們 總是會用提交數據去覆蓋現有的數據,並將當前服務端時間(客戶端時間不可靠)作爲字段LastUpdatedTime的值,這實際上也破壞了冪等性。

可能有人說我們可以在真正修改數據之前檢查提交的數據是否與現有數據一致,但是在涉及多個錶鏈接的時候這個“預檢”操作會帶來性能損失,而且針對每個字段的逐一比較也是一個很繁瑣的事情,所以我們一般不作這樣的預檢操作。

六、支持多種資源表示方式

資源和資源的表示(Representaion)是兩個不同的概念,資源本身是一個抽象的概念,是看不見摸不着的,而看得見摸得着的是資源的表現。 比如一個表示一個財年銷售情況的資源,它既可以表示爲一個列表、一個表格或者是一個圖表。如果採用圖表,又可以使用柱狀圖、K線圖和餅圖等,這一切都是針 對同一個資源的不同表示。

我們說“調用Web API獲取資源”,這句話其實是不正確的,因爲我們獲取的不是資源本身,僅僅是資源的某一種表示而已。對於Web來說,目前具有兩種主流的數據結構,XMLJSON,它們也是資源的兩種主要的呈現方式。在多語言環境下,還應該考慮描述資源採用的語言。

我們在設計Web API的時候,應該支持不同的資源表示,我們不能假定請求提供的資源一定表示成XML,也不能總是以JSON格式返回獲取的資源,正確的做法是:根據請求攜帶的信息識別提交和希望返回的資源表示。對於請求提交的資源,我們一般利用請求的Content-Type報頭攜帶的媒體類型來判斷其採用的表示類型。對於響應資源表示類型的識別,可以採用如下兩種方式。

  • 讓請求URI包含資源表示類型,這種方式使用的最多的是針對多語言的資源,我們一般講表示語言(也可以包含地區)的代碼作爲URI的一部分,比如“http://www.artech.com/en/orders/2013”表示將2013年的訂單以英文的形式返回。

  • 採用“內容協商(Content Negotiation)”根據請求相關報頭來判斷它所希望的資源表示類型,比如“Accept”和“Accept-language”報頭可以體現請求可以接受的響應媒體類型和語言。

對於上述兩種資源表示識別機制,我們很多人會喜歡後者,因爲第一種不夠“智能”。實際上前者具有一個後者不具有的特性:“瀏覽器兼容型”[2]。 對於Web API開發來說,瀏覽器應該成爲一種最爲常用的測試工具。在不借助任何插件的情況下,我們利用瀏覽器訪問我們在地址欄中輸入的URI時對生成的請求內容不 能作任何干預的,如果與資源表示相關的信息(比如語言、媒體類型)被直接包含到請求的URI中,那麼所有的情況都可以利用瀏覽器直接測試。

有人從另一方面對“URI攜帶資源表示類型”作了這樣的質疑:由於URI是資源的標識,那麼這導致了相同的資源具有多個標識。其實這是沒有問題的,URI是資源的唯一標識,但不是其“唯一的唯一標識“,相同的資源可以具有多個標識。

七、無狀態性

RESTful只要維護資源的狀態,而不需要維護客戶端的狀態。對於它來說,每次請求都是全新的,它只需要針對本次請求作相應的操作,不需要將本次請求的相關信息記錄下來以便用於後續來自相同客戶端請求的處理。

對於上面我們介紹的RESTful的這些個特性,它們都是要求我們爲了滿足這些特徵做點什麼,唯有這個無狀態卻是要求我們不要做什麼,因爲HTTP 本身就是無狀態的。舉個例子,一個網頁通過調用Web API分頁獲取符合查詢條件的記錄。一般情況下,頁面導航均具有“上一頁”和“下一頁”鏈接用於呈現當前頁的前一頁和後一頁的記錄。那麼現在有兩種實現方 式返回上下頁的記錄。

  • Web API不僅僅會定義根據具體頁碼的數據查詢定義相關的操作,還會針對“上一頁”和“下一頁”這樣的請求定義單獨的操作。它自身會根據客戶端的Session ID對每次數據返回的頁面在本地進行保存,以便能夠知道上一頁和下一頁具體是哪一頁。

  • Web API只會定義根據具體頁碼的數據查詢定義相關的操作,當前返回數據的頁碼由客戶端來維護。

第一種貌似很“智能”,其實就是一種畫蛇添足的作法,因爲它破壞了Web API的無狀態性。設計無狀態的Web API不僅僅使Web API自身顯得簡單而精煉,還因減除了針對客戶端的“親和度(Affinty)”使我們可以有效地實施負載均衡,因爲只有這樣集羣中的每一臺服務器對於每 個客戶端纔是等效的。


[1] 大部分計算機書籍都將Side Effect翻譯成“副作用”,而我們一般將“副(負)作用”理解爲負面的作用,其實計算機領域Side Effect表示的作用無所謂正負,所以我們覺得還是還原其字面的含義“邊界效用”。除此之外,對於GET、HEAD和OPTIONS請求來說,如果服務 端需要對它們作日誌、緩存甚至計數操作,嚴格來說這也算是一種Side Effect,但是請求的發送者不對此負責。

[2] 這裏的“兼容”不是指支持由瀏覽器發送的請求,因爲通過執行JavaScript腳本可以讓作爲宿主的瀏覽器發送任何我們希望的請求,這裏的兼容體現在儘可能地支持瀏覽器訪問我們在地址欄中輸入的URI默認發送的HTTP-GET請求。

參考資料:  
[1] 《HTTP: The Definitive Guide》, By By David Gourley, Brian Totty, Marjorie Sayer, Anshu Aggarwal, Sailu Reddy  
[2] 《RESTful Web Services》, RESTful Web Services  
[3] 《A Brief Introduction to REST》,http://www.infoq.com/articles/rest-introduction  
[4] 《TCP/IP Illustrated (Volumn 1: The Protocol)》, by W. Richard Stevens


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章