摘要:但第3種設計還有一種情況就是不論業務語義是成功還是失敗,HTTP狀態碼都返回200,實際成功還是失敗需要解析Body才知道,這種情況的確有,但這種設計方式我認爲是應該極力避免的,即使統一了正常、異常情況下的結構,即使你的結構裏面已經包含了業務真實的狀態,那也不要放棄保持HTTP狀態碼與業務語義的一致性,不然這種設計不光不RESTful,而且還很糟糕。另外,前面也說了,RESTful是一種軟件架構風格,雖然定義了一些約束,但也很泛,導致在一些細節設計上沒有特別統一的標準規範,所以本文下面分享的一些東西只是最佳實踐和我本人的觀點,並不表示你一定要這麼做,或者REST明確規定你就要這麼做,我們更多的是討論如何讓自己的API設計的更加RESTful而已。

在如今的互聯網時代,Web服務隨處可見,REST這個詞也經常出現。特別是現在微服務架構和前後端分離架構日益盛行,服務間越來越多的採用RESTful API通信,Java這門語言更是在Java SE 5中制定了JAX-RS(Jakarta/Java RESTful Web Services)規範,提供了一系列註解幫助開發者方便的構建符合RESTful風格的Web服務,當然REST和語言是沒有直接關係的。但我發現很多人其實沒有特別好的理解REST,或者說號稱自己設計的是RESTful API,但實際一點都不RESTful。所以本文從偏應用的角度分享一些最佳實踐和我自己的一些觀點。

什麼是REST

首先來看什麼是REST?以下是維基百科的解釋:

中文版:

表現層狀態轉換(英語:Representational State Transfer,縮寫:REST)是Roy Thomas Fielding博士於2000年在他的博士論文中提出來的一種萬維網 軟件架構風格 ,目的是便於不同軟件/程序在網絡(例如互聯網)中互相傳遞信息。表現層狀態轉換是根基於超文本傳輸協議(HTTP)之上而確定的一組約束和屬性,是一種設計提供萬維網絡服務的軟件構建風格。符合或兼容於這種架構風格(簡稱爲 REST 或 RESTful)的網絡服務,允許客戶端發出以統一資源標識符訪問和操作網絡資源的請求,而與預先定義好的無狀態操作集一致化。

英文版:

Representational state transfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web services . Web services that conform to the REST architectural style, called RESTful Web services, provide interoperability between computer systems on the Internet. RESTful Web services allow the requesting systems to access and manipulate textual representations of Web resources by using a uniform and predefined set of stateless operation s. Other kinds of Web services, such as SOAP Web services, expose their own arbitrary sets of operations.

從這些解釋裏面我們能找出一些關鍵點:

  • REST只是一種軟件架構風格,它不是標準,更不是協議。就像我們說的OOP一樣,它只是一種編程思想和風格,我們不能因爲Java是純面嚮對象語言,就認爲所有Java寫的程序就一定是面向對象的,現實中很多Java代碼其實就是面向過程風格的(注:面向過程和麪向對象本身並沒有孰優孰劣,尺有所短寸有所長而已)。
  • REST主要是基於HTTP協議,且應用於Web服務(Web Service),遵從REST風格的Web服務可以稱之爲RESTful Web services。
  • REST並非唯一的Web風格,主流的還有SOAP( Simple Object Access Protocol ,簡單對象訪問協議)、XML-RPC。相比於後兩者,REST比較簡潔。

因爲REST是出自Roy Thomas Fielding(也是HTTP 1.1協議的設計者之一)的博士論文,所以比較偏理論,原始論文讀起來有些味同嚼蠟(個人感覺)。對絕大多數人來說肯定是沒必要去研究論文的,根據二八原則,我們只要掌握百分之二十的REST知識,就能讓自己構建的Web Services比較RESTful了。本着簡潔的原則,本文就不寫REST定義的架構屬性和約束了,有興趣的可以直接去看維基百科。

雖然RESTful API並沒有強制使用HTTP+JSON的方式,但絕大部分的RESTful API都是基於HTTP+JSON這種形式的,所以本文就以這種討論。另外,前面也說了,RESTful是一種軟件架構風格,雖然定義了一些約束,但也很泛,導致在一些細節設計上沒有特別統一的標準規範,所以本文下面分享的一些東西只是最佳實踐和我本人的觀點,並不表示你一定要這麼做,或者REST明確規定你就要這麼做,我們更多的是討論如何讓自己的API設計的更加RESTful而已。

如何設計RESTful API

設計比較RESTful的API有非常多的細節要注意,我認爲有3個點尤其重要:URI的設計、HTTP方法的選擇、Response的設計。做好這3個,我個人認爲已經基本比較RESTful了。

URI設計

URI全稱 U niform R esource I dentifier,一般翻譯爲統一資源標識符(URL也可以視爲一種URI)。REST規定所有的Web資源都必須通過一個URI進行訪問,所以URI的設計至關重要。REST API中的URI的組成一般爲:

http(s)://[主機名]:[端口]/[路徑]?[查詢參數]

主機名、端口都是配置的,需要設計的主要是路徑以及參數(包括query、form-data等)。路徑以及參數設計一般要考慮兩方面:名稱、層次。

和變量起名類似,URI中的名稱一般有3種命名:

  • 蛇形命名法 :全小寫加下劃線,比如 user_id .
  • 脊柱命名法 :全小寫加中劃線,比如 user-id .
  • 駝峯命名法 :大小寫混合,比如 userId .

這三種裏面不推薦使用駝峯命名法,最主要的原因是URI中 協議和主機名(即域名)是不區分大小寫 的,比如下面幾種效果是一樣的:

但除協議和主機名之外,其它地方都是區分大小寫的,比如 https://niyanchun.com/pages/about.html和https ://niyanchun.com/pages/ABOUT.html理論上是不一樣的。這裏說理論上不一樣是因爲RFC 3986中是這樣定義的,但實際中有些服務器端的實現可能是不區分的(大部分對外的網站的網址爲了方便用戶,都是不區分大小寫的),但我們不能依賴這個,我們必須假定實際的實現是區分大小寫的。

另外一個原因是像一些FTP或者靜態資源的Web,他們的資源來自於操作系統(準確的說是操作系統的文件系統),比如Windows就是不區分大小寫的,“a.pdf”和“A.pdf”是一樣的,但Linux等unix-like的系統是區分大小寫的,“a.pdf”和“A.pdf”是不一樣的。

所以基於這些原因,在設計路徑以及參數的名稱的時候建議使用蛇形命名法或者 脊柱命名法(推薦) ,但不要混用。個人建議脊柱命名法,因爲它的輸入比較方便,而下劃線輸入的時候還要按shift鍵,尤其在移動設備上很不方便。還有一個就是下劃線在某些顯示場景下容易和下邊沿重合,看着不清楚。這裏需要額外說明的是蛇形命名法或者脊柱命名法並沒有規定一定要使用小寫字母,它們僅規定使用中劃線或者下劃線來連接單詞,但因爲我們不建議在URI中使用大寫字母,所以就直接規定爲全小寫字母了。

另外,在REST中,URI中要 使用名詞,而非動詞 (SOAP風格里面推薦使用動詞,比如 getBookById )。具體的行爲應該由HTTP方法去體現。有一個例外是當行爲太多,HTTP方法不夠用時,有時會在URI最後面加一個動詞(一般稱爲controller),這個地方是唯一可以使用動詞的地方。比如下面這個:

POST /alerts/245743/resend

但是,設計良好的URI應該極少有這種場景,如果你發現你的設計中有很多這種,很可能你的設計本身存在問題。

說到層次,先說一個比較簡單的規範,就用被玩爛了的客戶(customer)舉例吧:

http://example.com/customer/{id}
http://example.com/customer

實際中對於API,一般還會加上一些統一的前綴,比如版本、模塊等,這裏就不討論了。別看這兩個簡單的URI,現實中很多RESTful API都沒有做到,訪問具體某一個資源的時候,要把id等資源標識符放在path上面,而不是放在querystring或者form-data等數據裏面;訪問一組資源的時候,路徑到資源就可以了,最後面不要加 /

然後就是分層次了,先看個簡單的例子,比如客戶下面還會有訂單(order),那訂單這個資源的URI該如何設計呢?看下面的一些例子(爲了方便,這裏只舉單個訂單資源的URI的例子,批量的去掉最後面的id即可):

http://example.com/orders/{order_id}
http://example.com/{customer_id}/{order_id}
http://example.com/customers/orders/{order_id}
http://example.com/{customer_id}/orders/{order_id}
http://example.com/customers/{customer_id}/orders/{order_id}

上面5種,哪種設計更好呢?It depends...上面5種URI越來越長,包含的層級信息越來越多。選擇多了有時候未必是好事,特別是選項特別多的時候就一定不是一件好事了。這裏我也沒法給出孰優孰劣,只能給一些方法,然後每個人根據自己的情況去做取捨了。我列兩種比較極端的處理方式:

  1. 最短原則:即上面的1. 其實按照REST的設計,分客戶端(Client)和服務端(Server),資源的層次結構是由服務端去自由控制的,不需要體現給客戶端。如果單從這個角度考慮的話,其實1這種方式是完全OK的,是更加RESTful的。
  2. 最長原則:實際中我們往往在URI的設計上一般還是會體現一下層級關係,上面例子中只有Customer和Order兩級關係,但業務往往是複雜的,可能會有三級、四級,甚至更多,還可能是嵌套的。總的原則就是不建議在URI上面嵌套超過2級的層次關係,那樣會讓URI很長。

現實中,往往是在最長和最短中間取一個折中的選擇(成功的又把問題拋出去了~~)。

這個問題是一個容易引起無休止討論的問題,且沒有標準答案,StackOverflow就有一個討論這個的問題: REST URI convention - Singular or plural name of resource while creating it ,有興趣的可以查看。對於URI中 資源 使用單數還是複數,我的個人觀點是:不論是單數還是複數,統一就好:所有資源保持一致,操作單個資源和批量資源保持一致。如果一定要在單數和複數裏面選一個,我個人傾向於選擇全部採用複數形式。

這裏請務必注意,我們討論的僅僅是URI中的資源部分,而不是其它部分,比如假設有個服務是用戶管理,然後你的API是這樣的:

http://example.com/user-management/users/{id}

那其中的 user-management 指代的是服務,不算資源,後面的 users 纔是資源。非資源部分一般推薦使用單數形式。

HTTP方法選擇

這部分非常簡單,前面說了,URI中不包含動詞,具體行爲是有HTTP方法描述的,而用來對資源進行CRUD的方法無外乎以下四種:

  • GET:查詢操作。特點是安全且冪等。安全是指不會對服務端資源進行任何修改,冪等是指多次執行產生的結果是一樣的。
  • POST:創建新資源。不安全、不冪等。
  • PUT:更新或創建新資源。不安全、冪等。
  • DELETE:刪除資源。不安全、冪等。

當然HTTP還有其它方法,但在RESTful API中使用很少,這裏不討論了。這裏有兩個問題需要注意。

一個是有一個特殊情況就是如果查詢的參數非常多,或者有敏感數據,可以使用POST實現查詢,比如很多DSL查詢都走的是POST,但也有走GET的,比如ElasticSearch的DSL查詢同時支持GET和POST。

另外一個是上面有個細節不知道大家注意到沒有:POST和PUT都能用於創建新資源,那有什麼區別呢?

當客戶端能夠決定資源最終的URI時(其實就是決定資源的唯一標識符時),可以使用PUT;如果不能,就使用POST。比如我們要創建一個新的訂單,如果客戶端可以決定(需要服務端支持)訂單ID,那可以使用PUT操作:

PUT http://example.com/orders/1

如果客戶端決定不了,需要服務端自己生成,那就使用POST:

// 格式1
POST http://example.com/orders
// 格式2
POST http://example.com/orders/1

注意一下上面的格式2,也是有這種形式的,此時那個1僅是客戶端給服務端的建議,服務端未必會採納。

所以,爲了讓你的API更加RESTful,請只在客戶端能夠決定資源ID的時候,才使用PUT創建新資源,或者表達“不存在就創建,存在就更新”的語義的時候才使用PUT操作,否則請使用POST創建新資源。

下面給一些例子:

// 查詢所有訂單
GET http://example.com/orders
// 查詢某個訂單
GET http://example.com/orders/{id}
// 創建一個或多個訂單(如果有批量創建的需求,可以分兩個URI。個人建議使用一個URI,然後支持1~n個資源對象)
POST http://example.com/orders
// 修改一個訂單
PUT http://example.com/orders/{id}
// 修改多個訂單
PUT http://example.com/orders/{id1},{id2},{id3}
// 刪除一個訂單
DELETE http://example.com/orders/{id}
// 刪除多個訂單
DELETE http://example.com/orders/{id1},{id2},{id3}
// 刪除所有訂單
DELETE http://example.com/orders

Response設計

這個其實也很靈活,這裏僅討論兩個問題:

  • 返回的HTTP狀態碼
  • 返回的Body格式是否要統一

返回的HTTP狀態碼

REST對於Response返回的HTTP狀態碼其實就一個要求: 狀態碼和行爲是一致的。 要判斷狀態碼和行爲是否一致首先肯定要知道HTTP狀態碼的含義。HTTP規定的狀態碼說多不多,但說少也不少,我相信極少有人能完整說出HTTP規範定義的每個狀態碼及其含義。好在常用的狀態碼並不多,RESTful API中常用的就更少了(而且也不建議用太多)。常用的有以下這些狀態碼:

  • 表示成功的 :即2xx類的,RESTful API中最常用的也就200和201了。200表示通用的成功,201表示新建資源成功,POST接口創建新資源一般返回201。
  • 表示重定向類的 :即3xx類的,這種在RESTful API中很少用,最多也就用301。一般3xx的返回值算成功,客戶端需要根據3xx返回的重定向地址重新發起強求,RESTful API中一般很少用重定向技術,這裏就不說了。
  • 表示客戶端錯誤的 :即4xx類的,常用的有400、401、403、404. 400表示通用客戶端錯誤,比如參數非法等;401表示未認證,比如沒有傳用戶名、密碼或者傳的不對等;403表示未授權,這個注意要和401區分一下:401是未認證,403是已經認證過了,但是沒有權限訪問這個資源;404表示資源未找到。
  • 表示服務端錯誤的 :即5xx類的,絕大多數RESTful API裏面只使用通用的500即可,即表示服務端內部錯誤。

注意一下,現在我們開發一個RESTful API肯定會基於某個HTTP框架去開發,比如Java的Spring MVC、Jersey,Python的Flask,Golang的Beego等,那這個時候Response大的有兩種:一種是我們應用層代碼返回的,一種是框架層返回的。上面說的這些常用的HTTP狀態碼僅指應用層返回的情況,也就是我們能決定的情況。而框架一般會實現的比較完善,可能會根據不同的情況返回一些比較偏的HTTP狀態,但服務端無需關注。

瞭解了HTTP常用的狀態碼含義,再來看什麼叫狀態碼和行爲一致。所謂一致就是請求成功了,肯定返回的狀態碼是2xx的,你返回個4xx、5xx那肯定就是不符合REST規範了;同理,明明業務失敗了,你還返回2xx,那肯定也是不一致的,而且明明是未授權,你卻返回一個500,那也不一致。現在問題來了:

  1. 爲什麼需要一致?
  2. HTTP狀態碼那麼少,不足以描述複雜的業務場景怎麼辦?

第1個問題:爲什麼要一致?兩個原因,其一,定標準、定規範的目的就是爲了統一,爲了減少溝通的成本。在你的業務裏面,你把2xx定義爲失敗,5xx定義爲成功,技術上可以做到,但這樣你就得給團隊裏面每個人都講一下你的這個規則;換一個人,你都得再講一遍。更重要的,還會被懂行的人鄙視,內心一萬個xxx在蹦騰。其二,REST和HTTP是非常密切的(都是一個作者),REST裏面的一些思想已經被定義在了HTTP規範裏面,比如緩存。一般GET請求如果返回200,查詢結果是會被緩存的(當然實際得看服務端的實現以及查詢的參數、條件等),如果數據沒有變化,會直接讀上次查詢的緩存,這對於查詢操作比較重的情況還是能提升不少性能的,也能減少服務器的壓力,類似的道理還有很多。如果你一個查詢操作非要使用POST,或者明明成功了,卻要返回5xx,那這些優勢就沒了。而且還可能會產生一些錯誤,後文會提到。

第2個問題:HTTP狀態碼那麼少,不足以描述複雜的業務場景怎麼辦?這個問題也簡單,很多人都知道答案,那就是自定義業務狀態碼,的確是這樣的。但是一定要注意的是自定義的業務狀態碼只是HTTP狀態碼的一個補充,二者一定要是一致的。從技術上來說HTTP狀態碼是HTTP協議的一部分,而自定義的業務狀態碼只是自定義格式的Body的一部分而已,誰也不是誰的替代品。不能因爲自定義了業務狀態碼,就忽略HTTP狀態碼與業務行爲的一致性了,比如明明失敗了,還返回個200,哪怕你的業務狀態碼已經表明產生了錯誤。弊端下面會再次提到。

返回的Body格式是否要統一

這個問題其實已經不是REST的範疇了,只是在設計RESTful API的時候,常常會面臨這個問題,所以這裏也拿出來討論一下。這個問題呢是這樣的:就是說API返回的Body的數據格式要不要在任何情況下都統一。目前常見的做法有三種:

  1. 正常、異常返回都不統一。
  2. 正常返回不統一,異常返回統一。
  3. 正常、異常返回都統一。

現在都是基於框架開發RESTful API,第1種情況已經比較少見了,至少框架層返回的錯誤格式一般都是統一的,如果你自己在應用層返回的不統一,那就是你自己的事情了,或者說你沒用按照框架提供的方式去返回(即沒有用正確的姿勢使用框架)。

比較多的主要是第2種和第3種情況。這種問題沒有標準答案,而且有時還是要結合業務以及具體使用的框架去看,以下爲我個人觀點。我一般傾向於使用第2種情況,主要兩個原因:一,我使用過Python的Flask、Golang的Beego、Java的Spring MVC、Jersey等Web/JAX-RS框架,它們默認都沒有對正常返回時的數據格式做統一封裝,甚至有的接口返回列表,有的返回JSON,但對於異常情況都是統一了返回格式,至少都提供了統一封裝異常返回的方式。二,很多選用3的人的觀點是所有情況統一了數據結構,前端好處理,但這個真的是理由嗎?我認爲不是的,即使你統一了大格式,裏面存儲具體格式的字段還是統一不了。舉一個下面的例子:

{
    "status": "xxx",
    "error_code": "xxxx",
    "message": "xxxx",
    "data": "xxx"
}

上面這個是一個比較簡單的統一格式,data字段用於存返回的業務數據,現在即使所有情況都統一成這種格式,不同的接口,data字段對應的value也是很難統一的,有的可能僅返回一個字符串/布爾值/整數值,有的可能返回一個list,有的返回一個map,代碼層面你可以定義成Object(以Java爲例),但前端人員還是要去看接口文檔,然後根據情況做必要的反序列化或者類型轉換,和直接返回data的value沒有太多區別。

但異常情況我認爲是非常有必要統一的,上面說的正常情況不統一是因爲正常情況下數據格式是確定的,但異常情況卻是不確定的,甚至有些異常根本就到不了應用層代碼,比如url寫錯這種一般框架層就會返回404,根本到不了應用層代碼。也就是異常很不可控,我們沒法像正常情況那樣事先列舉好的每一種異常情況,但前端需要一個這樣的東西。所以可以統一異常時的返回形式,如果返回的狀態碼不是2xx,就按約定的統一的異常結構進行解析。然後這個統一的異常結構裏面一般都會存放狀態、簡單的錯誤原因、詳細的錯誤信息(比如調用棧等)、自定義錯誤碼等,這個就是結合自己的業務來了。所以至於2和3如何選,我一般建議是看你用的框架,如果他默認統一,那就統一;如果不統一,那就不統一,但對於異常情況一定要統一。

但第3種設計還有一種情況就是不論業務語義是成功還是失敗,HTTP狀態碼都返回200,實際成功還是失敗需要解析Body才知道,這種情況的確有,但這種設計方式我認爲是應該極力避免的,即使統一了正常、異常情況下的結構,即使你的結構裏面已經包含了業務真實的狀態,那也不要放棄保持HTTP狀態碼與業務語義的一致性,不然這種設計不光不RESTful,而且還很糟糕。一方面是語義不一致,和HTTP規範違背,另一方面前面也提到了,很多HTTP服務器會根據HTTP的狀態去做一些優化,比如緩存技術,如果你不按規範來,輕則使用不到一些協議帶來的優勢,重則還會產生錯誤。比如明明業務失敗了,但GET卻返回了200,這個時候HTTP服務還把這個結果緩存了,那可能你後來修復了錯誤,但客戶端依舊拿到的是緩存的錯誤。如果你說你的接口只是發了一個請求,實際的執行是異步的,得執行完之後才知道結果。這種情況很常見,但HTTP是個同步操作協議,它的狀態碼僅標識這次這個同步請求自身是否成功,至於業務層的成功並不強制通過這個狀態碼體現。創建資源就是一個最好的例子,複雜系統創建一個資源往往是一個重操作,比如在YARN上提交一個任務,那YARN提交的REST接口返回成功並不表示這個任務就一定會成功創建,它的成功僅表示你請求創建任務這個請求服務器已經成功收到了而已。這就是201這個狀態碼存在的意義,我們發現有些創建資源返回的是200,有些是201,一部分原因是每個人對HTTP掌握程度不一,一部分原因是對於輕量級的資源創建,可能同步就創建完成了,所以返回201,表示資源已創建;有些創建是重操作,那可能返回僅表示服務端成功收到了你的請求,這個時候返回200,而非201也是可以接受的。

本來想讓文章簡短一些,稍不注意就又寫多了。所以這裏做一下總結:如何才能讓自己的API比較RESTful:

/

推薦閱讀資料

前面也說了,按照二八原則本文涉及的內容僅是REST的20%(實際可能還不到),要真正構建一個RESTful WebService,或者只是一個RESTful API,其實還是有很多細節需要關注。但如前文所說,也許REST的最大問題在於它僅是一個架構風格,沒有明確的規範,特別是細節方面的規範,導致主觀性因素很多。不過,總體還是有一些業界統一比較認可方法論可以作爲參考,這裏我補充一些不錯的資源,有興趣的可以查閱:

  • RESTful WebServices,Subbu Allamaraju著
  • RESTful WebServices Cookbook,InfoQ翻譯整理的一個精簡版本
  • REST in Practice, Jim Webber, Savas Parastatidis, Ian Robinson著
  • RESTful API guidelines ,是一個起草中的RESTful API RFC規範

特別是最後一個,有一些明確規定的規範,值的好好讀一下。

相關文章