title: Restful API 設(shè)計(jì)學(xué)習(xí)
date: 2021/03/30 10:15
一、協(xié)議
API與用戶的通信協(xié)議,總是使用HTTPs協(xié)議。
如果能全站 HTTPS 當(dāng)然是最好的,不能的話也請(qǐng)盡量將登錄、注冊(cè)等涉及密碼的接口使用 HTTPS。
二、域名
應(yīng)該盡量將API部署在專用域名之下:
https://api.example.com
如果確定API很簡(jiǎn)單,不會(huì)有進(jìn)一步擴(kuò)展,可以考慮放在主域名下。
三、版本(Versioning)
應(yīng)該將API的版本號(hào)放入U(xiǎn)RL。
另一種做法是,將版本號(hào)放在HTTP頭信息中,但不如放入U(xiǎn)RL方便和直觀。Github采用這種做法。
四、路徑(Endpoint)
路徑又稱"終點(diǎn)"(endpoint),表示API的具體網(wǎng)址。
在RESTful架構(gòu)中,每個(gè)網(wǎng)址代表一種資源(resource),所以網(wǎng)址中不能有動(dòng)詞,只能有名詞,而且所用的名詞往往與數(shù)據(jù)庫(kù)的表格名對(duì)應(yīng)。一般來(lái)說(shuō),數(shù)據(jù)庫(kù)中的表都是同種記錄的"集合"(collection),所以API中的名詞也應(yīng)該使用復(fù)數(shù)。
舉例來(lái)說(shuō),有一個(gè)API提供動(dòng)物園(zoo)的信息,還包括各種動(dòng)物和雇員的信息,則它的路徑應(yīng)該設(shè)計(jì)成下面這樣。
五、HTTP動(dòng)詞
對(duì)于資源的具體操作類(lèi)型,由HTTP動(dòng)詞表示。
常用的HTTP動(dòng)詞有下面五個(gè)(括號(hào)里是對(duì)應(yīng)的SQL命令)。
- GET(SELECT):從服務(wù)器取出資源(一項(xiàng)或多項(xiàng))。
- POST(CREATE):在服務(wù)器新建一個(gè)資源。
- PUT(UPDATE):在服務(wù)器更新資源(客戶端提供改變后的完整資源)。
- PATCH(UPDATE):在服務(wù)器更新資源(客戶端提供改變的屬性)。
- DELETE(DELETE):從服務(wù)器刪除資源。
還有兩個(gè)不常用的HTTP動(dòng)詞。
- HEAD:獲取資源的元數(shù)據(jù)。
- OPTIONS:獲取信息,關(guān)于資源的哪些屬性是客戶端可以改變的。
下面是一些例子。
- GET /zoos:列出所有動(dòng)物園
- POST /zoos:新建一個(gè)動(dòng)物園
- GET /zoos/{ID}:獲取某個(gè)指定動(dòng)物園的信息
- PUT /zoos/{ID}:更新某個(gè)指定動(dòng)物園的信息(提供該動(dòng)物園的全部信息)
- PATCH /zoos/{ID}:更新某個(gè)指定動(dòng)物園的信息(提供該動(dòng)物園的部分信息)
- DELETE /zoos/{ID}:刪除某個(gè)動(dòng)物園
- GET /zoos/{ID}/animals:列出某個(gè)指定動(dòng)物園的所有動(dòng)物
- DELETE /zoos/{ID}/animals/{ID}:刪除某個(gè)指定動(dòng)物園的指定動(dòng)物
六、過(guò)濾信息(Filtering)
如果記錄數(shù)量很多,服務(wù)器不可能都將它們返回給用戶。API應(yīng)該提供參數(shù),過(guò)濾返回結(jié)果。
下面是一些常見(jiàn)的參數(shù)。
- ?limit=10:指定返回記錄的數(shù)量
- ?offset=10:指定返回記錄的開(kāi)始位置。
- ?page=2&per_page=100:指定第幾頁(yè),以及每頁(yè)的記錄數(shù)。
- ?sortby=name&order=asc:指定返回結(jié)果按照哪個(gè)屬性排序,以及排序順序。
- ?animal_type_id=1:指定篩選條件
參數(shù)的設(shè)計(jì)允許存在冗余,即允許API路徑和URL參數(shù)偶爾有重復(fù)。比如,GET /zoo/ID/animals 與 GET /animals?zoo_id=ID 的含義是相同的。
七、狀態(tài)碼(Status Codes)
服務(wù)器向用戶返回的狀態(tài)碼和提示信息,常見(jiàn)的有以下一些(方括號(hào)中是該狀態(tài)碼對(duì)應(yīng)的HTTP動(dòng)詞)。
- 200 OK - [GET]:服務(wù)器成功返回用戶請(qǐng)求的數(shù)據(jù),該操作是冪等的(Idempotent)。
- 201 CREATED - [POST]:成功請(qǐng)求并創(chuàng)建了新的資源,該請(qǐng)求已經(jīng)被實(shí)現(xiàn),而且有一個(gè)新的資源已經(jīng)依據(jù)請(qǐng)求的需要而建立,且其 URI 已經(jīng)隨Location 頭信息返回。假如需要的資源無(wú)法及時(shí)建立的話,應(yīng)當(dāng)返回 '202 Accepted'。
- 202 Accepted - [*]:表示一個(gè)請(qǐng)求已經(jīng)進(jìn)入后臺(tái)排隊(duì)(異步任務(wù))
- 204 NO CONTENT - [GET, DELETE]:服務(wù)器成功處理了請(qǐng)求,但沒(méi)返回任何內(nèi)容。
- 204 VS 沒(méi)有響應(yīng)體的HTTP/200響應(yīng)
- 如果導(dǎo)航到的URL返回了一個(gè)沒(méi)有響應(yīng)體的HTTP/200響應(yīng),則頁(yè)面將會(huì)顯示一個(gè)空白文檔(就是一片白色).頁(yè)面的URL地址也會(huì)變成新指定的URL.
- 如果服務(wù)器返回的是一個(gè)HTTP/204響應(yīng),當(dāng)前頁(yè)面不會(huì)有任何變化,就好像根本沒(méi)有進(jìn)行導(dǎo)航操作一樣.頁(yè)面的URL地址也保持不變.
- 400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發(fā)出的請(qǐng)求有錯(cuò)誤(例如,格式錯(cuò)誤的請(qǐng)求語(yǔ)法,太大的大小,無(wú)效的請(qǐng)求消息或欺騙性路由請(qǐng)求),服務(wù)器沒(méi)有進(jìn)行新建或修改數(shù)據(jù)的操作,該操作是冪等的。
- 401 Unauthorized - [*]:表示用戶沒(méi)有權(quán)限(令牌、用戶名、密碼錯(cuò)誤)。
- 403 Forbidden - [*] 表示用戶得到授權(quán)(與401錯(cuò)誤相對(duì)),但是訪問(wèn)是被禁止的。
- 404 NOT FOUND - [*]:用戶發(fā)出的請(qǐng)求針對(duì)的是不存在的記錄,服務(wù)器沒(méi)有進(jìn)行操作,該操作是冪等的。
- 406 Not Acceptable - [GET]:用戶請(qǐng)求的格式不可得(比如用戶請(qǐng)求JSON格式,但是只有XML格式)。
- 409 Conflict -[PUT]:表示請(qǐng)求與服務(wù)器端目標(biāo)資源的當(dāng)前狀態(tài)相沖突。 沖突最有可能發(fā)生在對(duì) PUT 請(qǐng)求的響應(yīng)中。例如,當(dāng)上傳文件的版本比服務(wù)器上已存在的要舊,從而導(dǎo)致版本沖突的時(shí)候,那么就有可能收到狀態(tài)碼為409 的響應(yīng)。
- 410 Gone -[GET]:用戶請(qǐng)求的資源被永久刪除,且不會(huì)再得到的。
- 422 Unprocesable entity - [POST/PUT/PATCH] 當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),發(fā)生一個(gè)驗(yàn)證錯(cuò)誤。
- 500 INTERNAL SERVER ERROR - [*]:服務(wù)器發(fā)生錯(cuò)誤,用戶將無(wú)法判斷發(fā)出的請(qǐng)求是否成功。
422(不可處理實(shí)體)狀態(tài)碼表示服務(wù)器理解請(qǐng)求實(shí)體的內(nèi)容類(lèi)型,并且請(qǐng)求實(shí)體的語(yǔ)法正確(因此400(錯(cuò)誤請(qǐng)求)狀態(tài)碼不合適),但無(wú)法處理其中的說(shuō)明。例如,如果XML請(qǐng)求主體包含格式正確(即,語(yǔ)法正確)但語(yǔ)義上錯(cuò)誤的XML指令,則可能發(fā)生此錯(cuò)誤情況。
所以我覺(jué)得驗(yàn)證異常應(yīng)該使用 422 狀態(tài)碼。
狀態(tài)碼的完全列表參見(jiàn)這里。
- 但是有些時(shí)候僅僅使用 HTTP 狀態(tài)碼沒(méi)有辦法明確的表達(dá)錯(cuò)誤信息,所以我傾向于在里面再包一層自定義的返回碼,例如:
成功時(shí):
{
"code": 100,
"msg": "成功",
"data": {}
}
失敗時(shí):
{
"code": -1000,
"msg": "用戶名或密碼錯(cuò)誤"
}
data是真正需要返回的數(shù)據(jù),并且只會(huì)在請(qǐng)求成功時(shí)才存在,msg只用在開(kāi)發(fā)環(huán)境,并且只為了開(kāi)發(fā)人員識(shí)別。客戶端邏輯只允許識(shí)別code,并且不允許直接將msg的內(nèi)容展示給用戶。如果這個(gè)錯(cuò)誤很復(fù)雜,無(wú)法使用一段話描述清楚,也可以在添加一個(gè)doc字段,包含指向該錯(cuò)誤的文檔的鏈接。
八、錯(cuò)誤處理(Error handling)
如果狀態(tài)碼是4xx,就應(yīng)該向用戶返回出錯(cuò)信息。一般來(lái)說(shuō),返回的信息中將error作為鍵名,出錯(cuò)信息作為鍵值即可。
{
error: "Invalid API key"
}
九、返回結(jié)果
針對(duì)不同操作,服務(wù)器向用戶返回的結(jié)果應(yīng)該符合以下規(guī)范。
- GET /collection:返回資源對(duì)象的列表(數(shù)組)
- GET /collection/resource:返回單個(gè)資源對(duì)象
- POST /collection:返回新生成的資源對(duì)象
- PUT /collection/resource:返回完整的資源對(duì)象
- PATCH /collection/resource:返回完整的資源對(duì)象
- DELETE /collection/resource:返回一個(gè)空文檔
創(chuàng)建和修改操作成功后,需要返回該資源的全部信息。
返回?cái)?shù)據(jù)不要和客戶端界面強(qiáng)耦合,不要在設(shè)計(jì) API 時(shí)就考慮少查詢一張關(guān)聯(lián)表或是少查詢 / 返回幾個(gè)字段能帶來(lái)多大的性能提升。
最好將返回?cái)?shù)據(jù)進(jìn)行加密和壓縮,尤其是壓縮在移動(dòng)應(yīng)用中還是比較重要的。
十、Hypermedia API
注:文檔相關(guān),我們一般用 swagger,所以不太重要
RESTful API最好做到Hypermedia,即返回結(jié)果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應(yīng)該做什么。
比如,當(dāng)用戶向api.example.com的根目錄發(fā)出請(qǐng)求,會(huì)得到這樣一個(gè)文檔。
{"link": { "rel": "collection https://www.example.com/zoos", "href": "https://api.example.com/zoos", "title": "List of zoos", "type": "application/vnd.yourformat+json" }}
上面代碼表示,文檔中有一個(gè)link屬性,用戶讀取這個(gè)屬性就知道下一步該調(diào)用什么API了。rel表示這個(gè)API與當(dāng)前網(wǎng)址的關(guān)系(collection關(guān)系,并給出該collection的網(wǎng)址),href表示API的路徑,title表示API的標(biāo)題,type表示返回類(lèi)型。
Hypermedia API的設(shè)計(jì)被稱為HATEOAS。Github的API就是這種設(shè)計(jì),訪問(wèn)api.github.com會(huì)得到一個(gè)所有可用API的網(wǎng)址列表。
{ "current_user_url": "https://api.github.com/user", "authorizations_url": "https://api.github.com/authorizations", // ... }
從上面可以看到,如果想獲取當(dāng)前用戶的信息,應(yīng)該去訪問(wèn)api.github.com/user,然后就得到了下面結(jié)果。
{ "message": "Requires authentication", "documentation_url": "https://developer.github.com/v3" }
上面代碼表示,服務(wù)器給出了提示信息,以及文檔的網(wǎng)址。
十一、其他
(1)API的身份認(rèn)證應(yīng)該使用OAuth 2.0框架。
API 需要設(shè)計(jì)成無(wú)狀態(tài),所以客戶端在每次請(qǐng)求時(shí)都需要提供有效的 Token 和 Sign,在我看來(lái)它們的用途分別是:
- Token 用于證明請(qǐng)求所屬的用戶,一般都是服務(wù)端在登錄后隨機(jī)生成一段字符串(UUID)和登錄用戶進(jìn)行綁定,再將其返回給客戶端。Token 的狀態(tài)保持一般有兩種方式實(shí)現(xiàn):一種是在用戶每次操作都會(huì)延長(zhǎng)或重置 TOKEN 的生存時(shí)間(類(lèi)似于緩存的機(jī)制),另一種是 Token 的生存時(shí)間固定不變,但是同時(shí)返回一個(gè)刷新用的 Token,當(dāng) Token 過(guò)期時(shí)可以將其刷新而不是重新登錄。
- Sign 用于證明該次請(qǐng)求合理,所以一般客戶端會(huì)把請(qǐng)求參數(shù)拼接后并加密作為 Sign 傳給服務(wù)端,這樣即使被抓包了,對(duì)方只修改參數(shù)而無(wú)法生成對(duì)應(yīng)的 Sign 也會(huì)被服務(wù)端識(shí)破。當(dāng)然也可以將時(shí)間戳、請(qǐng)求地址和 Token 也混入 Sign,這樣 Sign 也擁有了所屬人、時(shí)效性和目的地。
(2)服務(wù)器返回的數(shù)據(jù)格式,應(yīng)該盡量使用JSON,避免使用XML。
十二、接口冪等性
RESTful 中使用 GET、POST、PUT 和 DELETE 來(lái)表示資源的查詢、創(chuàng)建、更改、刪除,并且除了 POST 其他三種請(qǐng)求都具備冪等性(多次請(qǐng)求的效果相同,我覺(jué)得這個(gè)效果指的是對(duì)資源操作后的效果)。
| 操作 | 釋義 |
|---|---|
| 查詢 | 查詢對(duì)于結(jié)果是不會(huì)有改變的,查詢一次和查詢多次,在數(shù)據(jù)不變的情況下,查詢結(jié)果是一樣的。select是天然的冪等操作 |
| 刪除 | 刪除一次和多次刪除都是把數(shù)據(jù)刪除。 |
| 更新 | 修改在大多場(chǎng)景下都具有冪等性的,但是如果是增量修改就需要我們來(lái)手動(dòng)的保證冪等性,如下例子: |
| 1. 將表中的 age 字段的值設(shè)置為 1,這種操作不管執(zhí)行多少次都是冪等的 | |
| 2. 將表中的 age 字段的值增加 1,這種操作就不是冪等的,就需要我們來(lái)對(duì)這個(gè)接口進(jìn)行防重設(shè)計(jì)。 | |
| 新增 | 增加在重復(fù)提交的場(chǎng)景下會(huì)出現(xiàn)冪等性問(wèn)題 |
像這種非冪等性的接口,我們一般都需要對(duì)其進(jìn)行防重設(shè)計(jì)。
注:
防重設(shè)計(jì)和冪等設(shè)計(jì),其實(shí)是有區(qū)別的。防重設(shè)計(jì)主要為了避免產(chǎn)生重復(fù)數(shù)據(jù),對(duì)接口返回沒(méi)有太多要求。而冪等設(shè)計(jì)除了避免產(chǎn)生重復(fù)數(shù)據(jù)之外,還要求每次請(qǐng)求都返回一樣的結(jié)果。
這里只介紹兩種常見(jiàn)的防重設(shè)計(jì):
- 通過(guò)代碼邏輯實(shí)現(xiàn)接口的冪等性,但只能針對(duì)一些滿足判斷的邏輯實(shí)現(xiàn),具有一定的局限性。
- 使用 token 的機(jī)制來(lái)實(shí)現(xiàn)接口的冪等性,通用性比較強(qiáng)
代碼邏輯實(shí)現(xiàn)防重
假設(shè)一個(gè)用戶支付的場(chǎng)景,涉及到了訂單系統(tǒng)與支付系統(tǒng)
- 訂單系統(tǒng)負(fù)責(zé)記錄用戶的購(gòu)買(mǎi)記錄已經(jīng)訂單的流轉(zhuǎn)狀態(tài)(orderStatus)
- 支付系統(tǒng)用于付款,提供如下接口,訂單系統(tǒng)與支付系統(tǒng)通過(guò)分布式網(wǎng)絡(luò)交互。
支付系統(tǒng)提供給訂單系統(tǒng)的接口如下:
boolean pay(int accountid, BigDecimal amount); // 用于付款,扣除用戶的
此時(shí)訂單系統(tǒng)執(zhí)行的 sql:
update userAmount set amount = amount - 'value' where account= 'account';
如果用戶進(jìn)行了支付操作,訂單系統(tǒng)調(diào)用這個(gè)接口來(lái)進(jìn)行扣款,但是由于網(wǎng)絡(luò)原因,沒(méi)有獲取到確切的結(jié)果,因此訂單系統(tǒng)需要重試。由于支付系統(tǒng)并沒(méi)有做防重設(shè)計(jì),所以會(huì)導(dǎo)致重復(fù)扣款,不符合冪等性原則(同一個(gè)訂單,無(wú)論是調(diào)用了多少次,用戶都只會(huì)扣款一次)。如果需要支持冪等性,付款接口需要修改為以下接口:
boolean pay(int orderId,int accountId,BigDecimal amount)
此時(shí)訂單系統(tǒng)執(zhí)行的 sql:
update userAmount set amount = amount - 'value' ,paystatus = 'paid' where orderId= 'orderid' and paystatus = 'unpay';
token 機(jī)制實(shí)現(xiàn)防重
該方案需要兩次請(qǐng)求才能完成一次業(yè)務(wù)操作。
- 第一次請(qǐng)求獲取
token - 第二次請(qǐng)求帶著這個(gè)
token,完成業(yè)務(wù)操作。
具體流程圖如下:
第一步,先獲取token。
第二步,做具體業(yè)務(wù)操作。

token 的特點(diǎn):
- 需要申請(qǐng)
- 一次有效
- 可以用來(lái)限流(多次相同請(qǐng)求不執(zhí)行)
注意:redis要用刪除操作來(lái)判斷token,刪除成功代表token校驗(yàn)通過(guò),如果用 select+delete 來(lái)校驗(yàn)token,存在并發(fā)問(wèn)題,不建議使用。
其他方案實(shí)現(xiàn)防重
1、insert前先select

該方案不適用于并發(fā)場(chǎng)景,在并發(fā)場(chǎng)景中,要配合其他方案一起使用,否則同樣會(huì)產(chǎn)生重復(fù)數(shù)據(jù)。
2、加唯一索引
絕大數(shù)情況下,為了防止重復(fù)數(shù)據(jù)的產(chǎn)生,我們都會(huì)在表中加唯一索引,這是一個(gè)非常簡(jiǎn)單,并且有效的方案。
alter table `order` add UNIQUE KEY `un_code` (`code`);
加了唯一索引之后,第一次請(qǐng)求數(shù)據(jù)可以插入成功。但后面的相同請(qǐng)求,插入數(shù)據(jù)時(shí)會(huì)報(bào)Duplicate entry '002' for key 'order.un_code異常,表示唯一索引有沖突。
雖說(shuō)拋異常對(duì)數(shù)據(jù)來(lái)說(shuō)沒(méi)有影響,不會(huì)造成錯(cuò)誤數(shù)據(jù)。但是為了保證接口冪等性,我們需要對(duì)該異常進(jìn)行捕獲,然后返回成功。
如果是java程序需要捕獲:DuplicateKeyException異常,如果使用了spring框架還需要捕獲:MySQLIntegrityConstraintViolationException異常。

3、建防重表
有時(shí)候表中并非所有的場(chǎng)景都不允許產(chǎn)生重復(fù)的數(shù)據(jù),只有某些特定場(chǎng)景才不允許。這時(shí)候,直接在表中加唯一索引,顯然是不太合適的。
針對(duì)這種情況,我們可以通過(guò)建防重表來(lái)解決問(wèn)題。
該表可以只包含兩個(gè)字段:id 和 唯一索引,唯一索引可以是多個(gè)字段比如:name、code等組合起來(lái)的唯一標(biāo)識(shí),例如:susan_0001。

4、加分布式鎖
其實(shí)前面介紹過(guò)的加唯一索引或者加防重表,本質(zhì)是使用了數(shù)據(jù)庫(kù)的分布式鎖,也屬于分布式鎖的一種。但由于數(shù)據(jù)庫(kù)分布式鎖的性能不太好,我們可以改用:redis或zookeeper。
鑒于現(xiàn)在很多公司分布式配置中心改用apollo或nacos,已經(jīng)很少用zookeeper了,我們以redis為例介紹分布式鎖。
目前主要有三種方式實(shí)現(xiàn)redis的分布式鎖:
- setNx命令
- set命令
- Redission框架

十三、代碼示例
/**
* 注:這里面的實(shí)體全部用的是 User 對(duì)象,正式使用的時(shí)候需要有相應(yīng)的 pojo
*
* http://websystique.com/springmvc/spring-mvc-4-restful-web-services-crud-example-resttemplate/
*
* http://www.ruanyifeng.com/blog/2014/05/restful_api.html
*
* @author yujx
* @date 2021/03/29 10:18
*/
@RestController
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
private UserService userService;
// 一般來(lái)說(shuō),數(shù)據(jù)庫(kù)中的表都是同種記錄的"集合"(collection),所以**API中的名詞也應(yīng)該使用復(fù)數(shù)**。
@GetMapping("/v1/users")
public ResponseEntity<List<User>> getUserList() {
List<User> userList = userService.listUser();
if (userList.isEmpty()) {
// 204 No Content:服務(wù)器成功處理了請(qǐng)求,但沒(méi)返回任何內(nèi)容。
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(userList, HttpStatus.OK);
}
@GetMapping("/v1/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
if (user == null) {
log.warn("User with id 「{}」 not found", id);
// 404 Not Found:因?yàn)榭蛻舳溯斎肓艘粋€(gè)錯(cuò)誤的 id,無(wú)法定位資源
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(user, HttpStatus.OK);
}
@PostMapping("/v1/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user,
BindingResult bindingResult,
UriComponentsBuilder ucBuilder) throws MethodArgumentNotValidException {
// 其實(shí)我覺(jué)得這種錯(cuò)誤應(yīng)該使用 409
// 409 Conflict 表示請(qǐng)求與服務(wù)器端**目標(biāo)資源**的當(dāng)前狀態(tài)相沖突。 沖突最有可能發(fā)生在對(duì) PUT 請(qǐng)求的響應(yīng)中。
// 例如,當(dāng)上傳文件的版本比服務(wù)器上已存在的要舊,從而導(dǎo)致版本沖突的時(shí)候,那么就有可能收到狀態(tài)碼為409 的響應(yīng)。
// * 因?yàn)槲矣X(jué)得當(dāng)前場(chǎng)景和"目標(biāo)資源"的這個(gè)定義有沖突,所以還是用 422 吧
User theUser = userService.getUserByName(user.getName());
if (theUser != null) {
bindingResult.rejectValue("name", "already.exists", "A User with name " + user.getName() + " already exist");
}
// 如果驗(yàn)證有異常,則對(duì)外拋出 MethodArgumentNotValidException,會(huì)由 ResponseEntityExceptionHandler catch 住,
// 然后包裝成 400 狀態(tài)碼,INVALID REQUEST - [POST/PUT/PATCH]:用戶發(fā)出的請(qǐng)求有錯(cuò)誤,服務(wù)器沒(méi)有進(jìn)行新建或修改數(shù)據(jù)的操作,該操作是冪等的。
// 但其實(shí)我覺(jué)得應(yīng)該用 422 狀態(tài)碼,Unprocessable Entity 請(qǐng)求格式正確,但是由于含有語(yǔ)義錯(cuò)誤,無(wú)法響應(yīng)。-[POST/PUT/PATCH] 當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),發(fā)生一個(gè)驗(yàn)證錯(cuò)誤。
if (bindingResult.hasErrors()) {
throw new MethodArgumentNotValidException(
// https://stackoverflow.com/questions/442747/getting-the-name-of-the-currently-executing-method
new MethodParameter(new Object() {
}.getClass().getEnclosingMethod(), 0), bindingResult);
}
// 新增之后要返回資源的訪問(wèn)路徑,添加到 location 中,并響應(yīng) 201 狀態(tài)碼
// 201狀態(tài)碼英文名稱是Created,該狀態(tài)碼表示已創(chuàng)建。成功請(qǐng)求并創(chuàng)建了新的資源,該請(qǐng)求已經(jīng)被實(shí)現(xiàn),而且有一個(gè)新的資源已經(jīng)依據(jù)請(qǐng)求的需要而建立,
// 且其 URI 已經(jīng)隨Location 頭信息返回。假如需要的資源無(wú)法及時(shí)建立的話,應(yīng)當(dāng)返回 '202 Accepted' - [*]:表示一個(gè)請(qǐng)求已經(jīng)進(jìn)入后臺(tái)排隊(duì)(異步任務(wù))
user = userService.saveUser(user);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(ucBuilder.path("/v1/user/{id}").buildAndExpand(user.getId()).toUri());
// 理論上不應(yīng)該返回這個(gè) user 對(duì)象的,但還是返回吧
return new ResponseEntity<>(user, headers, HttpStatus.CREATED);
}
@PutMapping("/v1/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
// 如果兩個(gè) id 不同,則響應(yīng) 400 狀態(tài)碼
if (!id.equals(user.getId())) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
User theUser = userService.getUserById(id);
if (theUser == null) {
// 404 Not Found:因?yàn)榭蛻舳溯斎肓艘粋€(gè)錯(cuò)誤的 id,無(wú)法定位資源
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
// 因?yàn)?put 請(qǐng)求是全量更新,所以直接調(diào)用 save 方法,如果要部分更新則使用 patch 請(qǐng)求
// 正確使用 Patch 請(qǐng)求方式:https://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/
user = userService.saveUser(user);
return new ResponseEntity<>(user, HttpStatus.OK);
}
@DeleteMapping("/v1/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
User theUser = userService.getUserById(id);
if (theUser == null) {
// 404 Not Found:因?yàn)榭蛻舳溯斎肓艘粋€(gè)錯(cuò)誤的 id,無(wú)法定位資源
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
userService.deleteUserById(id);
// 204 NO CONTENT - [DELETE]:用戶刪除數(shù)據(jù)成功。
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@DeleteMapping("/v1/users")
public ResponseEntity<Void> deleteAllUsers() {
userService.deleteAllUsers();
// 204 NO CONTENT - [DELETE]:用戶刪除數(shù)據(jù)成功。
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
/*
獲取指定用戶的家庭成員 GET /v1/users/{uid}/familyMembers
獲取指定用戶的指定家庭成員 GET /v1/users/{uid}/familyMembers/{fid}
獲取某個(gè)審查任務(wù)的審查要點(diǎn)信息 /v1/reviewTasks/{rid}/reviewPoints/{pid}
*/
}
參考文章
**Spring MVC 4 RESTFul Web Services CRUD Example+RestTemplate
*我所認(rèn)為的RESTful API最佳實(shí)踐
基于游標(biāo)的分頁(yè)接口實(shí)現(xiàn)
Please. Don't Patch Like That.