簡介
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 是一種很籠統的概念,它代表一種架構風格。
版本號
在 RESTful API 中,API 接口應該儘量兼容之前的版本。但是,在實際業務開發場景中,可能隨着業務需求的不斷迭代,現有的 API 接口無法支持舊版本的適配,此時如果強制升級服務端的 API 接口將導致客戶端舊有功能出現故障。實際上,Web 端是部署在服務器,因此它可以很容易爲了適配服務端的新的 API 接口進行版本升級,然而像 Android 端、IOS 端、PC 端等其他客戶端是運行在用戶的機器上,因此當前產品很難做到適配新的服務端的 API 接口,從而出現功能故障,這種情況下,用戶必須升級產品到最新的版本才能正常使用。
爲了解決這個版本不兼容問題,在設計 RESTful API 的一種實用的做法是使用版本號。一般情況下,我們會在 url 中保留版本號,並同時兼容多個版本。
【GET】 /v1/users/{user_id} // 版本 v1 的查詢用戶列表的 API 接口
【GET】 /v2/users/{user_id} // 版本 v2 的查詢用戶列表的 API 接口
現在,我們可以不改變版本 v1 的查詢用戶列表的 API 接口的情況下,新增版本 v2 的查詢用戶列表的 API 接口以滿足新的業務需求,此時,客戶端的產品的新功能將請求新的服務端的 API 接口地址。雖然服務端會同時兼容多個版本,但是同時維護太多版本對於服務端而言是個不小的負擔,因爲服務端要維護多套代碼。這種情況下,常見的做法不是維護所有的兼容版本,而是隻維護最新的幾個兼容版本,例如維護最新的三個兼容版本。在一段時間後,當絕大多數用戶升級到較新的版本後,廢棄一些使用量較少的服務端的老版本API 接口版本,並要求使用產品的非常舊的版本的用戶強制升級。
注意的是,“不改變版本 v1 的查詢用戶列表的 API 接口”主要指的是對於客戶端的調用者而言它看起來是沒有改變。而實際上,如果業務變化太大,服務端的開發人員需要對舊版本的 API 接口使用適配器模式將請求適配到新的API 接口上。
資源路徑
RESTful API 的設計以資源爲核心,每一個 URI 代表一種資源。因此,URI 不能包含動詞,只能是名詞。注意的是,形容詞也是可以使用的,但是儘量少用。一般來說,不論資源是單個還是多個,API 的名詞要以複數進行命名。此外,命名名詞的時候,要使用小寫、數字及下劃線來區分多個單詞。這樣的設計是爲了與 json 對象及屬性的命名方案保持一致。例如,一個查詢系統標籤的接口可以進行如下設計。
【GET】 /v1/tags/{tag_id}
同時,資源的路徑應該從根到子依次如下
/{resources}/{resource_id}/{sub_resources}/{sub_resource_id}/{sub_resource_property}
我們來看一個“添加用戶的角色”的設計,其中“用戶”是主資源,“角色”是子資源。
【POST】 /v1/users/{user_id}/roles/{role_id} // 添加用戶的角色
有的時候,當一個資源變化難以使用標準的 RESTful API 來命名,可以考慮使用一些特殊的 actions 命名。
/{resources}/{resource_id}/actions/{action}
舉個例子,“密碼修改”這個接口的命名很難完全使用名詞來構建路徑,此時可以引入 action 命名。
【PUT】 /v1/users/{user_id}/password/actions/modify // 密碼修改
請求方式
可以通過 GET、 POST、 PUT、 PATCH、 DELETE 等方式對服務端的資源進行操作。其中:
- GET : 用於查詢資源
- POST : 用於創建資源
- PUT :用於更新服務端的資源的全部信息
- PATCH :用於更新服務端的資源的部分信息
- DELETE : 用於刪除服務端的資源
這裏使用 “用戶” 的案例進行回顧通過GET、POST、PUT、PATCH、DELETE等方式對服務端的資源進行操作。
【GET】 /users # 查詢用戶信息列表
【GET】 /users/1001 # 查看某個用戶信息
【POST】 /users # 新建用戶信息
【PUT】 /users/1001 # 更新用戶信息(全部字段)
【PATCH】 /users/1001 # 更新用戶信息(部分字段)
【DELETE】 /users/1001 # 刪除用戶信息
查詢參數
RESTful API 接口應該提供參數,過濾返回結果。其中,offset 指定返回記錄的開始位置。一般情況下,它會結合 limit 來做分頁的查詢,這裏 limit 指定返回記錄的數量。
【GET】 /{version}/{resources}/{resource_id}?offset=0&limit=20
同時,orderby 可以用來排序,但僅支持單個字符的排序,如果存在多個字段排序,需要業務中擴展其他參數進行支持。
【GET】 /{version}/{resources}/{resource_id}?orderby={field} [asc|desc]
爲了更好地選擇是否支持查詢總數,我們可以使用 count 字段,count 表示返回數據是否包含總條數,它的默認值爲 false。
【GET】 /{version}/{resources}/{resource_id}?count=[true|false]
上面介紹的 offset、 limit、 orderby 是一些公共參數。此外,業務場景中還存在許多個性化的參數。我們來看一個例子。
【GET】 /v1/categorys/{category_id}/apps/{app_id}?enable=[1|0]&os_type={field}&device_ids={field,field,…}
注意的是,不要過度設計,只返回用戶需要的查詢參數。此外,需要考慮是否對查詢參數創建數據庫索引以提高查詢性能。
狀態碼
使用適合的狀態碼很重要,而不應該全部都返回狀態碼 200,或者隨便亂使用。這裏,列舉在實際開發過程中常用的一些狀態碼,以供參考。
狀態碼 | 描述 |
---|---|
200 | 請求成功 |
201 | 創建成功 |
400 | 錯誤的請求 |
401 | 未驗證 |
403 | 被拒絕 |
404 | 資源無法找到 |
409 | 資源衝突 |
500 | 服務器內部錯誤 |
異常響應
當 RESTful API 接口出現非 2xx 的 HTTP 錯誤碼響應時,採用全局的異常結構響應信息。
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}
請求參數
在設計服務端的 RESTful API 的時候,我們還需要對請求參數進行限制說明。例如一個支持批量查詢的接口,我們要考慮最大支持查詢的數量。
【GET】 /v1/users/batch?user_ids=1001,1002 // 批量查詢用戶信息
參數說明
- user_ids: 用戶ID串,最多允許 20 個。
此外,在設計新增或修改接口時,我們還需要在文檔中明確告訴調用者哪些參數是必填項,哪些是選填項,以及它們的邊界值的限制。
【POST】 /v1/users // 創建用戶信息
請求內容
{
"username": "James", // 必填, 用戶名稱, max 10
"realname": "詹姆斯", // 必填, 用戶名稱, max 10
"password": "123456", // 必填, 用戶密碼, max 32
"email": "[email protected]", // 選填, 電子郵箱, max 32
"weixin": "xxx", // 選填,微信賬號, max 32
"sex": 1 // 必填, 用戶性別[1-男 2-女 99-未知]
}
響應參數
針對不同操作,服務端向用戶返回的結果應該符合以下規範。
【GET】 /{version}/{resources}/{resource_id} // 返回單個資源對象
【GET】 /{version}/{resources} // 返回資源對象的列表
【POST】 /{version}/{resources} // 返回新生成的資源對象
【PUT】 /{version}/{resources}/{resource_id} // 返回完整的資源對象
【PATCH】 /{version}/{resources}/{resource_id} // 返回完整的資源對象
【DELETE】 /{version}/{resources}/{resource_id} // 狀態碼 200,返回完整的資源對象。
// 狀態碼 204,返回一個空文檔
如果是單條數據,則返回一個對象的 JSON 字符串。
HTTP/1.1 200 OK
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
}
如果是列表數據,則返回一個封裝的結構體。
HTTP/1.1 200 OK
{
"count":100,
"items":[
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
},
...
]
}
一個完整的案例
最後,我們使用一個完整的案例將前面介紹的知識整合起來。這裏,使用“獲取用戶列表”的案例。
【GET】 /v1/users?[&keyword=xxx][&enable=1][&offset=0][&limit=20] 獲取用戶列表
功能說明:獲取用戶列表
請求方式:GET
參數說明
- keyword: 模糊查找的關鍵字。[選填]
- enable: 啓用狀態[1-啓用 2-禁用]。[選填]
- offset: 獲取位置偏移,從 0 開始。[選填]
- limit: 每次獲取返回的條數,缺省爲 20 條,最大不超過 100。 [選填]
響應內容
HTTP/1.1 200 OK
{
"count":100,
"items":[
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
},
...
]
}
失敗響應
HTTP/1.1 403 UC/AUTH_DENIED
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}
錯誤代碼
- 403 UC/AUTH_DENIED 授權受限