1. 程式人生 > >我所理解的RESTful Web API [設計篇]

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

一個 nta 協議 cti 提交 李維 目前 web api 介紹

本文轉載自文章:我所理解的RESTful Web API [設計篇]。

其他參考文章有:RESTful 介紹 ppt,Restful的理解,Restful 優缺點,理解RESTful架構,RESTful API 設計指南,HTTP報文

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

目錄
一、為什麽叫這個“奇怪”的名字?

二、采用URI標識資源

三、采用URI標識資源

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

五、使用統一的接口

六、使用標準的HTTP方法。

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

八、無狀態性

九、個人總結(自己加的)

一、為什麽叫這個“奇怪”的名字?

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/images/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來說,目前具有兩種主流的數據結構,XML和JSON,它們也是資源的兩種主要的呈現方式。在多語言環境下,還應該考慮描述資源采用的語言。

我們在設計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)”使我們可以有效地實施負載均衡,因為只有這樣集群中的每一臺服務器對於每個客戶端才是等效的。

九、個人總結

如何理解RESTful API
  是什麽
    是一種基於HTTP協議的一套編寫客戶端和服務器端交互接口的規範。
  特點
    - 采用的是面向資源的架構方式,所以在設計之初首先要考慮的是有哪些資源可供操作。建議在URL中盡量使用名詞
    - 給用戶一個URL,根據不同的METHOD在後端進行不同的處理。
    - POST 新建一個資源
    - GET 獲取一個資源
    - PUT 修改資源
    - DELETE 刪除數據
    - 利用HTTP狀態碼來反應服務器的處理結果
    - 使用JSON或XML等格式來返回結果
    - 使用查詢參數過濾信息,因為有時我們並不需要全部的數據
      ?limit=10
    - 版本話RESTful API,有些數據或程序可能有多個版本共存
      https://api.example.com/version1/

      https://api.example.com/version2/


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

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

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