摘要
本文介紹了 REST 的由來,對 REST 的風格架構設計指導原則做了詳細的說明。同時舉例了過往開發中若干細節的考慮和實現方案。
文字略長,預計需要10 ~ 20 分鐘讀完。也可以收藏起來,在需要的時候查閱。
RESTful 架構是目前流行的一種網際網路應用架構。如果把網站,移動應用從伺服器到前端,從整體上看作是一個軟體,它就是一個層次清楚,功能強,擴展方便,適宜通信的架構規範。
· 01 ·
什麼是 REST
REST 是 「Representational State Transfer」的縮寫,直譯過來就是「表述性狀態轉移」。這是一個很奇怪的名詞,剛看到的時候,不知其所以然。這個名詞來源於 Roy Thomas Fielding 博士著名的論文《Architectural Styles and the Design of Network-based Software Architectures》(架構風格與基於網絡的軟體架構設計)。
論文發表於2000年,作者在基於 REST 的約束上設計了 HTTP 協議。設計 REST 的目的,就是為了指導現代 Web 架構的設計與開發。他是 HTTP 協議(1.0 版和 1.1 版)的主要設計者、Apache 伺服器軟體的作者之一、Apache 基金會的第一任主席。
論文的第六章,作者解釋了這個名詞的由來:
REST 最初被 稱作「HTTP 對象模型」,但是那個名稱常常引起誤解,使人們誤以為它是一個 HTTP 服務 器的實現模型。這個名稱「表述性狀態轉移」是有意喚起人們對於一個良好設計的 Web 應 用如何運轉的印象:一個由網頁組成的網絡(一個虛擬狀態機),用戶通過選擇連結(狀態轉移)在應用中前進,導致下一個頁面(代表應用的下一個狀態)被轉移給用戶,並且呈現給他們,以便他們來使用。
第四章描述了設計 REST 的動機:「為 Web 應該如何運轉創建一種架構模型, 使之成為 Web 協議標準的指導框架」。第五章從一個沒有約束的空架構開始,不斷的添加約束,從而使此架構進化為 Web 所需要的架構。
所以,REST 是一組架構約束。
REST 約束包括:客戶-伺服器,無狀態,緩存,統一接口,分層系統,按需代碼。如果一個架構符合 REST 原則,就稱它為 RESTful 架構。
為實現統一的接口,REST由四個接口約束來定義:
資源的識別(identification of resources)、通過表述對資源執行的操作(manipulation of resources through representations)、自描述的消息(self-descriptive messages)、應用狀態引擎的超媒體(hypermedia as the engine of application state,HEOAS)。
當我們用這個原則來設計伺服器端的接口,為前端或者外部提供數據時,就稱它為 RESTful API 。目前觀察到,業內在實踐中有下面這些指導原則:
用統一資源標識符來標識資源應用狀態引擎的超媒體(HEOAS)使用標準的 HTTP 方法安全性和冪等性無狀態性
在實際開發過程中,我們還會涉及下面幾個方面:
版本鑑權常見場景狀態碼和錯誤處理返回結果文檔
在 REST 出現之前,程序間的網絡通信架構採用的是遠程過程調用(Remote Procedure Call,RPC),而後又在 RPC 基礎上發展出來簡單對象訪問協議( Simple Object Access Protocol,SOAP),此後,出現了 REST。
REST 是一種面向資源的架構,它能更好的適應分布式下的系統架構設計。對於開發者來說,越來越簡單,越來越靈活。
· 02 ·
什麼是資源
資源是一種概念上的映射。
任何能夠被命名的信息都能夠作為一個資源,它是對信息的核心抽象:一份文檔、一張圖片、一個與時間相關的服務(例如:「我現在城市的天氣」)、一個包含其他資源的集合、一個非虛擬的對象(例如:用戶)等等。它是到一組實體的概念上的映射,而不是實體本身。
更精確地說,資源 R 是一個隨時間變化的成員函數 MR(t),該函數將時間 t 映射到等價的一個實體或值的集合,集合中的值可能是資源的表述和,或資源的標識符。
資源不是存儲對象,也不是單個文件,更不是某個文本、音頻、視頻等具體事物。
在設計具體 API 的時候,資源是業務系統裡,抽象出來的一個業務對象。例如:用戶(User),訂單(Order),令牌(Token)等等。它允許隨時間變化,輸出不同值。
一個資源具有一個或者多個標識。這裡說的標識就是統一資源標識符(Uniform Resource Identifier,URI)。統一資源定位符(Uniform Resource Locator,URL)則是 URI 的一種具體實現,是 URI 的子集。在通常的 API 設計中,直接使用 URL 來標示應用系統中的資源。
例如:
https://www.sample.com/api/shops/10083 - 編號10083店鋪的基本信息
https://www.sample.com/api/users/2372/orders - 編號2372用戶的所有訂單信息
在標準的 REST 規範觀點中,通常會要求將資源 shop 加上複數形式 s 表示多個資源。但現實中,很多時候忘記給獲取多個資源的接口加上複數形式,在不影響理解的前提下,這種做法也不是不可以接受。
實踐中常見的情況有:
移動應用的首頁,通常有多種資源:橫幅廣告、編輯精選的內容、針對當前用戶推薦的商品,相應的對應著三個資源:廣告(banners),內容(contents),商品(products)。
通常後端的開發會讓前端調用這三個資源的接口,獲得相應的數據。但是前端開發會要求,後端能不能針對首頁單獨給一個接口?
為適應這種情況,我們會創建一個首頁的資源:index,這個資源包括需要展示的其他子資源。如:
https://www.sample.com/api/indexs。
幾乎所有的應用,都會有一個搜索功能。我們很容易習慣性的設計這樣的接口:
https://www.sample.com/users/search?key=searchkey。
這樣的接口設計就沿襲了 RPC 的設計風格,表示 users 服務提供的 search 方法,不符合 REST 規範,也是規範裡建議的,URI中不要有動詞。因為"資源"表示一種實體,所以應該是名詞,URI不應該有動詞,動詞應該放在HTTP協議中。
推薦的做法是設計一個資源 search,也就是說把動詞變成資源。例如:
https://www.sample.com/search/?key=searchkey&source=user
https://www.sample.com/search/user?key=searchkey
· 03 ·
應用狀態引擎的超媒體(HEOAS)
這個名詞也非常的拗口。它實際的意思是,在資源的表述中,如果有其他資源的,則會提供相應的連結 URL,使得用戶不查文檔,也知道如何獲得相關的資源。
Fielding 明確表示,系統必須滿足 HATEOAS 約束才能稱為是符合 REST 風格的。
Github 的 API 就是這種設計,訪問 api.github.com 會得到一個所有可用的 API 的信息列表,類似這樣:
{
"current_user_url": "https://api.github.com/user",
"current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}",
"authorizations_url": "https://api.github.com/authorizations",
...
}
所以 HATEOAS 會被稱為狀態引擎,因為它會引導狀態的轉移。
在設計的理想狀態中,使用 HATEOAS 的 REST 服務中,客戶端可以通過伺服器提供的資源的表達來智能地發現可以執行的操作。當伺服器發生了變化時,客戶端並不需要做出修改,因為資源的 URI 和其他信息都是動態發現的。
這樣的設計,保證了客戶端和伺服器的實現之間是鬆耦合的。客戶端需要根據伺服器提供的返回信息來了解所暴露的資源和對應的操作。當伺服器端發生了變化時,如修改了資源的 URI,客戶端不需要進行相應的修改。
在實踐中,我沒發現哪家國內的公司公布的 API 的接口遵守了這條原則,我們自己開發時,也不實現這條原則。為什麼實際中大部分開發不遵守這條原則呢?
因為客戶端無法決策!
HTTP 能實現 RESTful,是因為瀏覽器只是將表述以及對資源的操作選項展示了出來,至於具體該如何操作,是由使用瀏覽器的人來決定的。也就是說,雖然服務端告訴了客戶端操作的可選項,但是客戶端沒辦法知道該選擇什麼。網頁瀏覽是有人參與的,但是 RESTful API 沒有人參與,導致 RESTful API 的客戶端難以做出決定,該做什麼。
鑑於現實這個尷尬情況,Richardson 提出了「REST成熟度模型」。該模型把 REST 服務按照成熟度劃分成 4 個層次:
Level 0:Web 服務只是使用 HTTP 作為傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。SOAP 和 XML-RPC 都屬於此類。 Level 1:Web 服務引入了資源的概念。每個資源有對應的標識符和表述。 Level 2:Web 服務使用不同的 HTTP 方法來進行不同的操作,並且使用 HTTP 狀態碼來表示不同的結果。如:HTTP GET 方法來獲取資源,HTTP DELETE 方法來刪除資源。 Level 3:Web 服務使用 HATEOAS。在資源的表述中包含了連結信息。客戶端可以根據連結來發現可以執行的動作。從成熟度模型中可以看到,使用 HATEOAS 的 REST 服務是成熟度最高的,也是推薦的做法。但實際的落地實現時,多數都是次一級的做法。
· 04 ·
使用標準的 HTTP 方法
RESTful API 使用標準的 HTTP 協議實現前後端的接口調用。對涉及到的資源,常用的操作就是增、刪、改、查,類似對資料庫記錄的CRUD(Create,Read,Update,Delete)。使用的 HTTP 方法規則如下:
查詢 GET :GET /users/{userId}增加 POST:POST /users全量修改 PUT:PUT /users/{userId} 即提供該用戶的所有信息來修改部分修改 PATCH:PATCH /users/{userId} 只提供需要的修改的信息刪除 DELETE:DELETE /users/{userId}
在修改的時,PUT 和 PATCH 區別在於 PUT 是全量修改,user 資源有多少信息,需要全部提供,而 PATCH 可以只修改手機或者郵箱,暱稱,密碼等信息。
HTTP 的方法中還有兩個涉及到 REST:HEAD 和 OPTIONS。
HEAD 方法用於得到描述目標資源的元數據信息。
例如,騰訊雲的對象存儲的API:HEAD Bucket 請求可以確認該存儲桶是否存在,是否有權限訪問。
請求:
HEAD / HTTP/1.1
Host: <BucketName-APPID>.cos.<Region>.myqcloud.com
Date: GMT
Date Authorization: Auth String
伺服器響應:
HTTP/1.1 200 OK
Content-Type: application/xml
Content-Length: 0
Connection: close
Date: Tue, 28 May 2019 03:16:12 GMT
Server: tencent-cos x-cos-bucket-region: ap-beijing
x-cos-request-id: NWNlY2E3ZmNfZj****
OPTIONS 請求用來確定對某個資源必須具有怎樣的約束。
使用場景:客戶端先使用 OPTIONS 詢問服務端,應該採用怎樣的 HTTP 方法以及自定義的請求報頭,然後根據其約束髮送真正的請求。
例如,騰訊雲的對象存儲 OPTIONS Object 接口實現 Object 跨域訪問配置的預請求。即在發送跨域請求之前會發送一個 OPTIONS 請求並帶上特定的來源域,HTTP 方法和 Header 信息等給 COS,以決定是否可以發送真正的跨域請求。
請求:
OPTIONS /exampleobject HTTP/1.1
Host: examplebucket-1250000000.cos.ap-beijing.myqcloud.com
Date: Thu, 12 Jan 2017 17:26:53 GMT
Origin: http://www.qq.com
Access-Control-Request-Method: PUT
伺服器響應:
HTTP/1.1 200 OK
Content-Type: application/xml
Content-Length: 16087
Connection: keep-alive
x-cos-request-id: NTg3NzRiZGRfYmRjMzVfM2Y2OF81N2YzNA==
Date: Thu, 12 Jan 2017 17:26:53 GMT
ETag: \"9a4802d5c99dafe1c04da0a8e7e166bf\"
Access-Control-Allow-Origin: http://www.qq.com
Access-Control-Allow-Methods: PUT
Access-Control-Expose-Headers: x-cos-request-id
完全按 REST 指導原則採用標準 HTTP 方法很美好。但是實踐中,各大平臺推出了內置小程序,前端在調用小程序的 request 請求時,發現只支持 GET、POST 方法。
這種情況下,導致後端的 API 接口不得不做調整,所有方法在設計之初就只用 GET/POST 方法。
或者在已成型的系統中使用擴展屬性:X-HTTP-Method-Override,一個非標準的HTTP協議頭,來繞過這個問題。前端統一使用POST,在請求頭帶上這個非標屬性,服務端根據Header:X-HTTP-Method-Override,轉換成真正的 METHOD。
· 05 ·
安全性和冪等性
在討論 RESTful API 接口設計時,會提到兩個基本的特性:「安全性」和「冪等性」。
安全性是指調用接口不對資源產生修改。
冪等性是指調用方法1次或N次對資源產生的影響結果都是相同的。
需要特別注意的是:這裡冪等性指的是對資源產生的影響結果,而不是調用HTTP方法的返回結果。
常用 HTTP 方法的冪等性和安全性總結:
從上述表格中可以看出,HTTP 方法的冪等性和安全性並不是同一個概念:
OPTONS、HEAD、GET 既是冪等也是安全的,不修改資源,多次調用對資源的影響是相同的。POST、PATCH 既不冪等也不安全,修改了資源,同時多次調用時,對資源影響是不同的,PATCH 的影響不同在於,每次的局部更新可能會導致資源不一樣。PUT 是對資源的全量更新,多次更新總是對資源影響是一致的,所以它是冪等,但不安全。DELETE 用於刪除資源,多次調用的情況下,都是刪除了資源,所以它是冪等,但不安全。
冪等性原本是數學中的含義,表達式的N次變換與1次變換的結果相同。為什麼要在接口設計時,考慮冪等性?
在實際的業務流程場景下,我們可能會碰到下面一些問題:
訂單創建接口,前端調用超時了,但服務端已經完成了訂單的創建,然後前端顯示失敗,用戶又點了一次。用戶完成了支付,服務端完成了扣錢操作,但前端超時了,用戶不知道,又去支付了一次。用戶發起一筆轉帳業務,服務端已經完成了扣款,接口響應超時,調用方重試了一次。
以上類似的場景,需要在設計接口時考慮冪等性。我們可以借鑑微信支付的接口方案來實現這類場景需要的冪等性。在支付之前,需要調用一個接口生產預支付交易單,獲得一個交易單號,隨後再針對這個交易單號完成支付。服務端確保一個交易單號只會被支付一次,這樣就保證了支付過程的冪等性。
在對冪等性的理解上,有時候我們會有疑惑:服務端一般會有日誌、緩存或者數據表上的計數器、最後更新時間等。這樣上面說的 GET、PUT、DELETE 符合冪等性的方法就會導致這些數據內容的變化,是不是就不是冪等性的方法呢?
我認為在這個冪等判斷問題上,還是要回到什麼是資源的定義問題上來。服務端的日誌、緩存、計數標誌、更新時間等,不屬於抽象出來的核心概念,也就是對資源沒有本質上的影響,這些方法仍然是冪等的。
· 06 ·
無狀態性
客戶端與服務端的交互必須是無狀態的,並在每一次請求中包含處理該請求所需的一切信息。服務端不需要在請求間保留應用狀態,只有在接受到實際請求的時候,服務端才會關注應用狀態。
這種無狀態通信原則,使得服務端和中介能夠理解獨立的請求和響應。在多次請求中,同一客戶端也不再需要依賴於同一伺服器,方便實現高可擴展和高可用性的服務端。
REST 只維護資源的狀態,並不維護客戶端狀態,而且 HTTP 本身是無狀態的。那如何在用戶登錄後,和伺服器之間傳遞用戶信息呢?
以前這類場景下的做法是使用 cookie 和 session,兩者的區別在於,是存在客戶端還是服務端。這種設計,就違反了無狀態通信的設計原則。實踐中,API 設計成在用戶登錄之後,服務端會返回客戶端一個用戶令牌(Token),並帶有失效時間,客戶端在後續的接口請求都會帶上這個令牌。
· 07 ·
版本
API 上線運行後,就會有版本升級的情況。在 API 設計中,如何體現不同的版本?有兩種通常的做法:
將版本號放入 URL將版本號放在 HTTP 信息頭
兩種方法各有優缺點:放在 URL 中,直觀,方便。Github 就是這樣的方案。
網上也有另一種說法,就是不同版本的資源,仍然還是資源的不同表現形式,所以 URL 應該是同一個,版本號需要放在 HTTP 信息頭的 Accept 欄位中。
本人比較推薦第一種方案,將版本號放在 URL 中,對前端的兼容性、普適性更友好。
· 08 ·
鑑權
REST 設計原則並未提到有些需要權限的業務場景下應該怎麼做。在實踐中,推薦使用 OAuth 2.0 標準來實現 API 的鑑權需要。具體如何實現,篇幅可能需要比較長,不在此贅述。
· 09 ·
常見場景
業務系統總是複雜的,前面舉了一些資源的操作的例子,但是距離一個實用的系統還遠遠不夠。
例如需要對列表信息進行約束。應用在初始時只顯示開始的若干條記錄,等待用戶翻頁或者下拉操作後,需要服務端從第幾頁開始,返回一頁多少條記錄。類似下面這樣:
?pageIndex=2&pageSize=20 // 指定返回第2頁的20條數據
?start=200&limit=20 // 指定返回從第200條記錄開始的20條數據
如果不使用動詞,原來用戶登錄接口 POST /user/login 這樣的應該如何設計?
還是使用資源的思維,用戶登錄就是為了獲得身份認證信息,所以這個資源就是「認證」,例如:
GET /authorize?username=xx&password=xx
GET /token
· 10 ·
錯誤處理和返回碼
REST 設計原則推薦使用 HTTP 狀態碼來返回服務端信息。如下:
200 OK - [GET]:伺服器成功返回用戶請求的數據。
201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數據成功。
202 Accepted - [*]:表示一個請求已經進入後臺排隊(異步任務)
204 NO CONTENT - [DELETE]:用戶刪除數據成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發出的請求有錯誤,伺服器沒有進行新建或修改數據的操作。
401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)。
403 Forbidden - [*] 表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的。
404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,伺服器沒有進行操作,該操作是冪等的。
406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 當創建一個對象時,發生一個驗證錯誤。
500 INTERNAL SERVER ERROR - [*]:伺服器發生錯誤,用戶將無法判斷發出的請求是否成功。
實踐過程中,不推薦 API 設計採用上面的錯誤返回碼。這樣做的結果會把業務系統裡面本身的錯誤狀態與 HTTP 協議本身的錯誤混淆起來,有悖於架構設計中分層的原則。例如:
前端收到一個 404 錯誤時,不清楚是服務端沒有這個接口,還是請求的業務系統中的對應資源不存在。
出現 403 錯誤時,是因為服務端部署出現了權限錯誤,還是用戶的對請求的資源權限不夠。
出現 500 錯誤時,是不是服務端的伺服器問題,還是創建資源時,不滿足業務系統的一些限制條件。
所以,在最終的實際運行的系統中,只保留了一個200狀態碼,表示服務端收到了請求,並做了處理,業務系統本身的錯誤返回信息在結果中體現。
· 11 ·
返回結果
上一節說了服務端返回時 HTTP 狀態碼時統一使用 200,表示業務系統正常運行。如果有系統的錯誤提示信息需要向調用者返回,則在返回結果中統一定義。如下面是一個常見的返回對象:
{
code: 0, //表示接口執行是否成功,0:成功,非0:某一類預先定義的失敗錯誤碼。
data: object, // 返回的數據對象
error: 'error descrtion message', // 返回碼有錯誤時,返回對錯誤描述性的文字,方便調用端處理
}
調用端在處理錯誤返回信息時,也有兩種方案:
直接顯示服務端返回的錯誤信息
根據錯誤碼,前端自行定義和顯示更人性化的錯誤提示信息
第一種方案便於系統修改,可以在不修改調用端的情況下,修改返回的顯示信息。
第二種方案對用戶更友好,常見用於調用外部接口時,屏蔽掉讓用戶不明白的錯誤提示信息,顯示遇到錯誤時,應該如何處理的友好提示。
· 12 ·
接口文檔
實際系統開發過程中,接口文檔也是非常重要的一環。如果單獨專門編輯接口文檔,時間一長,就會造成代碼和文檔的不一致情況。開發團隊還需要專門費事費力的在代碼版本升級後,同時更新接口文檔。所以最好的辦法,就是寫代碼的時候,同時更新文檔。
如果使用 Spring boot 開發的朋友,可以使用 swagger 自動生成在線的接口文檔,並且支持自動生成調用參數,在線測試開發好的接口。
其他語言的文檔生成工具,可以試試 apidoc。也是通過讀代碼中的約定格式的注釋,自動生成 API 文檔,也支持在線測試調用。
以上,大致將 RESTful API 架構設計原則做了一個框架性的介紹,附帶介紹了具體項目實踐中的幾種落地的做法。
技術發展到現在,在具體的代碼實現上,Java 的 Spring boot 框架已經很成熟,能滿足所有 RESTful API 設計原則。