Restful API 設(shè)計(jì)學(xué)習(xí)


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ò)展,可以考慮放在主域名下。

https://example.org/api/

三、版本(Versioning)

應(yīng)該將API的版本號(hào)放入U(xiǎn)RL。

https://api.example.com/v1/

另一種做法是,將版本號(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 VS 400

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ì):

  1. 通過(guò)代碼邏輯實(shí)現(xiàn)接口的冪等性,但只能針對(duì)一些滿足判斷的邏輯實(shí)現(xiàn),具有一定的局限性。
  2. 使用 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ù)操作。

  1. 第一次請(qǐng)求獲取token
  2. 第二次請(qǐng)求帶著這個(gè)token,完成業(yè)務(wù)操作。

具體流程圖如下:

第一步,先獲取token。

獲取token

第二步,做具體業(yè)務(wù)操作。

具體業(yè)務(wù)操作

token 的特點(diǎn):

  1. 需要申請(qǐng)
  2. 一次有效
  3. 可以用來(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

image

該方案不適用于并發(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異常。

image

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。

image

4、加分布式鎖

其實(shí)前面介紹過(guò)的加唯一索引或者加防重表,本質(zhì)是使用了數(shù)據(jù)庫(kù)分布式鎖,也屬于分布式鎖的一種。但由于數(shù)據(jù)庫(kù)分布式鎖的性能不太好,我們可以改用:rediszookeeper。

鑒于現(xiàn)在很多公司分布式配置中心改用apollonacos,已經(jīng)很少用zookeeper了,我們以redis為例介紹分布式鎖。

目前主要有三種方式實(shí)現(xiàn)redis的分布式鎖:

  1. setNx命令
  2. set命令
  3. Redission框架
image

本部分參考

十三、代碼示例

/**
 * 注:這里面的實(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

**RESTful API 設(shè)計(jì)指南

HTTP狀態(tài)碼:204 No Content

*我所認(rèn)為的RESTful API最佳實(shí)踐

基于游標(biāo)的分頁(yè)接口實(shí)現(xiàn)

*分頁(yè)加載實(shí)現(xiàn)方案

Please. Don't Patch Like That.

Redis SCAN 命令

*HTTP Status Codes For Invalid Data: 400 vs. 422

*高并發(fā)下如何保證接口的冪等性?

**接口的冪等性原則

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 網(wǎng)絡(luò)應(yīng)用程序,分為前端和后端兩個(gè)部分。當(dāng)前的發(fā)展趨勢(shì),就是前端設(shè)備層出不窮(手機(jī)、平板、桌面電腦、其他專用設(shè)備……...
    涼薄少年yi冷閱讀 204評(píng)論 0 1
  • 一、協(xié)議 API與用戶的通信協(xié)議,總是使用 HTTPs協(xié)議. 二、域名 應(yīng)該盡量將 API部署在專用域名之下. h...
    Silence_王凱閱讀 575評(píng)論 0 1
  • 文章導(dǎo)讀: 網(wǎng)絡(luò)應(yīng)用程序,分為前端和后端兩個(gè)部分。當(dāng)前的發(fā)展趨勢(shì),就是前端設(shè)備層出不窮(手機(jī)、平板、桌面電腦、其他...
    創(chuàng)造new_world閱讀 332評(píng)論 0 0
  • 目錄 定義(Definitions) 數(shù)據(jù)的設(shè)計(jì)與抽象化(Data Design and Abstraction)...
    55lover閱讀 2,246評(píng)論 0 4
  • 轉(zhuǎn)載自 http://www.cnblogs.com/moonz-wu/p/4211626.html 做出一個(gè)好的...
    啟艦科技閱讀 3,267評(píng)論 0 5

友情鏈接更多精彩內(nèi)容