一篇學(xué)會(huì) REST 深度進(jìn)階
本文轉(zhuǎn)載自微信公眾號(hào)「老王Plus」,作者老王Plus的老王。轉(zhuǎn)載本文請(qǐng)聯(lián)系老王Plus公眾號(hào)。
說(shuō)起來(lái),REST 出現(xiàn)已經(jīng)很久了。
從早期的三層架構(gòu),到現(xiàn)在的多層、微服務(wù),核心內(nèi)容之一就是 API --- 從非常簡(jiǎn)單的 API,到多設(shè)備多用途的 API,包括一些外接的三方,像 BAT 的公共服務(wù),簡(jiǎn)單的、麻煩的,都是 API。而這些 API,又基本上都是基于 REST 的。
今天我們不去詳細(xì)解釋 REST,只說(shuō)說(shuō) REST 應(yīng)用中間的一些要點(diǎn)。
REST 應(yīng)用之多,是有他的原因的。他很容易理解,很靈活,并且可以適用于任何規(guī)模的應(yīng)用。
當(dāng)然,REST 并不是唯一的規(guī)范,還有 SOAP、GraphQL。但是,這只是字面上的并列的規(guī)范。所有的規(guī)范用過(guò)了,你就會(huì)知道:SOAP 很笨重,有時(shí)候還很古怪:你需要花大量的心思去想接口的表示,而不是邏輯本身。至于 GraphQL,又延伸的太多了,居然需要調(diào)用 API 的客戶端去考慮和設(shè)計(jì),這絕不是個(gè)好主意。
好吧,這個(gè)問(wèn)題見(jiàn)仁見(jiàn)智,我們不展開(kāi)討論。
不管怎么說(shuō),在我看來(lái),REST 仍然是 API 接口規(guī)范的王者,并且不會(huì)在短時(shí)間內(nèi)被取代。
在我的習(xí)慣中,使用 REST 會(huì)有以下幾個(gè)約束。
1. 使用 JSON 數(shù)據(jù)
別誤解,這是我的習(xí)慣,不是 REST 的。
REST 并沒(méi)有規(guī)定使用什么樣的格式來(lái)傳遞數(shù)據(jù),XML 也行,JSON 也行。但是在我的團(tuán)隊(duì)中,JSON 傳遞數(shù)據(jù)是一個(gè)硬性要求。
相比較而言,JSON 比 XML 有太多的優(yōu)勢(shì)了:
- 更易于使用、書(shū)寫(xiě)和閱讀
 - 更快,占用的內(nèi)存空間更少
 - 不需要特殊的依賴項(xiàng)或包來(lái)解析
 - 主流的編程語(yǔ)言對(duì) JSON 都有支持
 
如果不理解這些優(yōu)勢(shì),沒(méi)關(guān)系,去網(wǎng)上隨便找一個(gè) XML,試著自己解析一下看看。熟悉大廠的各種開(kāi)放平臺(tái)的同學(xué)們也會(huì)有直覺(jué)的感覺(jué):早期的 SOAP 和 XML,已經(jīng)被逐步替換為了 REST 和 JSON。
此外,這里說(shuō)的使用 JSON 數(shù)據(jù),不僅僅是響應(yīng)數(shù)據(jù),還包括請(qǐng)求數(shù)據(jù)。不要使用 form-data 或者 x-www-form-urlencoded 發(fā)送數(shù)據(jù),轉(zhuǎn)成 JSON 發(fā)送,會(huì)更容易閱讀、編寫(xiě)、測(cè)試和管理。
真心的,如果你這么做了,我會(huì)替所有開(kāi)發(fā)的同學(xué)們感謝你。
2. 認(rèn)真對(duì)待方法
想一下,你有沒(méi)有見(jiàn)到過(guò)只用 GET 方法來(lái)處理一切事情的 API?
這并不是不可以,只不過(guò),這樣的寫(xiě)法說(shuō)明沒(méi)有深入理解這個(gè)工具,以及 HTTP 的準(zhǔn)確的工作方式。要知道,HTTP 中每個(gè)方法都被設(shè)計(jì)為處理特定的工作和內(nèi)容。
這兒我逐個(gè)說(shuō)說(shuō):
GET - 在僅僅用于讀數(shù)據(jù)時(shí),應(yīng)該用 GET。不寫(xiě)入、不更新,只讀取數(shù)據(jù)。這個(gè)概念很簡(jiǎn)單。而且,在這個(gè)前提下,相同的請(qǐng)求一定會(huì)返回相同的結(jié)果。
POST - 看字面的意思就明白,就是存儲(chǔ)一些東西,像是在數(shù)據(jù)庫(kù)中創(chuàng)建一條記錄、在某處寫(xiě)入一些內(nèi)容。通常來(lái)說(shuō),可以選擇很多種方式 POST 數(shù)據(jù):multipart/form-data、x-www-form-urlencoded、application/json 或者 text/plain,等等,很多。不過(guò),我們要求只使用 application/json 方式,這樣做可以保持開(kāi)發(fā)和調(diào)用的一致性。
PUT - 字意就是更新內(nèi)容。所以當(dāng)我們需要更新數(shù)據(jù)時(shí),就需要定義為 PUT 方法。當(dāng)然,也可以用來(lái)創(chuàng)建新數(shù)據(jù)。
DELETE - 刪除,很好理解。
PATCH - 打補(bǔ)丁,對(duì)于已經(jīng)存在的數(shù)據(jù)進(jìn)行更新操作。這個(gè)跟 PUT 有一點(diǎn)點(diǎn)區(qū)別,通常 PATCH 是有范圍的,更新需要更新的內(nèi)容,而 PUT 更多時(shí)候是更新整個(gè)數(shù)據(jù)。
當(dāng)然,在某些文章里,還會(huì)有 OPTIONS、HEAD、TRACE等等,這些用得少,就不說(shuō)了。想了解的,可以去查 HTTP 相關(guān)的文檔。
說(shuō)這么多,重要的是 --- 既然 HTTP 提供了這樣的方法定義,我們完全可以把任何 CRUD 的操作對(duì)映到這些方法,而不是只用 GET,這決不是一個(gè)好習(xí)慣。
3. 注意語(yǔ)義
在團(tuán)隊(duì)開(kāi)發(fā) API 時(shí),有一個(gè)嚴(yán)格的要求,就是 API 名稱需要有語(yǔ)義感。語(yǔ)義感這個(gè)詞是我自己生造的,不是什么高大上的東西,就是要求寫(xiě)的 API 名稱能使用正確的英文和次序,能夠讓人看得懂。都 9021 年了,居然還有人用拼音首字符,說(shuō)出來(lái)你敢信嗎?
在我看來(lái),所有的 API 都應(yīng)該可以在不看注釋和說(shuō)明的情況下被調(diào)用方理解,從調(diào)用端點(diǎn),到參數(shù),和 JSON 的鍵。
這兒,我參考了國(guó)外的一些規(guī)則。規(guī)則也很簡(jiǎn)單:
- 用名詞,別用動(dòng)詞。想一下,上面列出的方法,本身就是動(dòng)詞,比方說(shuō):GET /clients,就很好理解,如果換成 GET /getClients,總覺(jué)得怪怪的。
 - 一定要準(zhǔn)確使用單數(shù)和復(fù)數(shù),針對(duì)一條數(shù)據(jù),就用單數(shù);針對(duì)多條數(shù)據(jù),就一定用復(fù)數(shù)。感覺(jué)一下 GET /client 和 GET /clients 的區(qū)別。當(dāng)然,對(duì)于單個(gè)數(shù)據(jù)來(lái)說(shuō),通常還需要某種 ID 的存在:GET /client/id。
 
下面用一些例子來(lái)理解一下這個(gè)規(guī)則。
- // 好的方式
 - GET /clients
 - POST /clients
 - GET /client/23
 - PUT /client/23
 - DELETE /client/23
 - GET /client/23/comments
 - // 不好的方式
 - GET /clients/23
 - GET /listAllClients
 - POST /client/create
 - PUT /updateClient/23
 - GET /clientComments/23
 
這兒要多說(shuō)兩句:規(guī)則只是規(guī)則,不用那么死板的去記。要把這種規(guī)則理解了,并習(xí)慣性地應(yīng)用在編程的過(guò)程中,變成一種類(lèi)似肌肉記憶的東西。
4. 隨時(shí)留心 API 的安全
就算你做得不是公開(kāi)的 API,也一定要記著,使用某些手段,讓你的 API 安全起來(lái)。這是 API 編程一個(gè)基本的要求。
這又有幾個(gè)方面的要求:
1). 使用 HTTPs
HTTPs 已經(jīng)出來(lái)非常久了,而且,如果你對(duì)接過(guò)大廠的 API,你會(huì)發(fā)現(xiàn)使用 HTTPs 是一個(gè)基本的要求。
HTTPs 提供了一種比 HTTP 更安全的方式,可以在基本網(wǎng)絡(luò)層面除去中間人攻擊,并加密調(diào)用端和 API 的通訊。在編程時(shí),使用 HTTPs 是個(gè)成本最低但又確實(shí)有效的安全方式。
把使用 HTTPs 當(dāng)成一個(gè)標(biāo)準(zhǔn)和習(xí)慣,有一天你會(huì)感謝自己的。
2). 從構(gòu)建 API 開(kāi)始,就要做到控制訪問(wèn)
你看得沒(méi)錯(cuò),是從構(gòu)建 API 開(kāi)始。
不需要做得很麻煩,但要有控制,要能控制誰(shuí)能訪問(wèn)這個(gè) API。通常可以先加入一個(gè)簡(jiǎn)單的 JWT Auth,等 API 成形后,再轉(zhuǎn)為 OAuth。目的很簡(jiǎn)單,就是控制訪問(wèn)。如果真出現(xiàn)了 API 被攻擊什么的,簡(jiǎn)單地關(guān)閉暴露的密鑰就可以了。當(dāng)然,我們還可以用密鑰來(lái)跟蹤 API 的調(diào)用,包括調(diào)用量、調(diào)用異常等。
3). 小心對(duì)待敏感數(shù)據(jù)
API 代表了網(wǎng)絡(luò),代表了通訊。在網(wǎng)絡(luò)和通訊上,傳遞敏感數(shù)據(jù)一定要小心再小心。我們前邊提到了一定使用 HTTPs,也是因?yàn)檫@個(gè)。如果不想面向監(jiān)獄編程,一定要確保這些敏感數(shù)據(jù)通過(guò)正確的方式,給到正確的調(diào)用方。
看了一眼數(shù)據(jù),就被追了刑責(zé),這是我身邊的真事。
4). 確保運(yùn)行環(huán)境的安全
網(wǎng)關(guān)、防火墻,有就用上,別因?yàn)槁闊┚完P(guān)掉。更深的內(nèi)容,可以扔給運(yùn)維,但基礎(chǔ)的部分,自己要懂要會(huì)。
5. 版本控制
API 疊代升級(jí),是每個(gè)開(kāi)發(fā)的會(huì)面對(duì)的事。有時(shí)候,升級(jí)僅僅是邏輯的改變,而更多時(shí)候,是會(huì)改變輸入輸出結(jié)構(gòu)的。這種情況下,保持和維護(hù) API 的版本很重要。作為后端開(kāi)發(fā)人員,我們無(wú)法保證調(diào)用端會(huì)隨時(shí)同步進(jìn)行相應(yīng)的改動(dòng)。極端情況下,改變內(nèi)部邏輯,也有可能影響到調(diào)用端。
API 版本控制,不用猶豫,馬上開(kāi)始使用。不要覺(jué)得某個(gè) API 比較小,或者調(diào)用端少,就不去做。記著,任何的代碼改動(dòng),對(duì)于不更新應(yīng)用或其它內(nèi)容的調(diào)用者來(lái)說(shuō)都是有風(fēng)險(xiǎn)的。你不僅需要確保你的代碼不會(huì)破壞任何東西或任何人,還需要知道某個(gè)應(yīng)用版本的表現(xiàn)。這件事一點(diǎn)都不好玩。
關(guān)于 API 版本控制的詳細(xì)實(shí)現(xiàn),我前邊一篇推文,可以去看看。傳送門(mén)
至于版本的方式,倒是不那么重要,可以看個(gè)人的習(xí)慣,v1、v2、v3也可以,v1.0、v1.1、v1.2也可以。按照微軟的建議,是采用 Major.Minor.Patch 的方式。不過(guò)我自己覺(jué)得帶上 Patch 部分有點(diǎn)太長(zhǎng)了。
所以,在我的習(xí)慣中,應(yīng)用版本控制后,API 的 URL會(huì)是這樣的:
- GET /v1.7/clients
 - POST /v1.7/clients
 - GET /v1.7/client/23
 - PUT /v1.7/client/23
 - DELETE /v1.7/client/23
 - GET /v1.7/client/23/comments
 
聽(tīng)我的,馬上開(kāi)始 API 的版本控制。
6. 保持響應(yīng)的一致
一致性是好的 API 的優(yōu)秀品質(zhì)。開(kāi)發(fā)中,我們應(yīng)該在各種方面做到一致,包括命名、URI、請(qǐng)求、響應(yīng)等。而在這里面,響應(yīng)的一致性是我對(duì)團(tuán)人的一個(gè)硬性要求。
API 是要讓別人去調(diào)用的。保持資源響應(yīng)的一致,是對(duì)調(diào)用者最大的善意。在某個(gè)壇子上,我看到過(guò)建議每個(gè)端點(diǎn)返回不同資源結(jié)構(gòu)的說(shuō)法。如果你也看到過(guò)類(lèi)似的內(nèi)容,忘了它,那是錯(cuò)的。
記著這句話:保持資源響應(yīng)的一致,是對(duì)調(diào)用者最大的善意。
API 開(kāi)發(fā)時(shí),盡可能發(fā)送相同的響應(yīng)結(jié)構(gòu)。如果沒(méi)有數(shù)據(jù),就將其作為空值、空對(duì)象或空數(shù)據(jù)發(fā)送。
我們拿論壇的文章結(jié)構(gòu)舉個(gè)例子。
文章數(shù)據(jù)的結(jié)構(gòu)通常是這樣(有簡(jiǎn)化,不要糾結(jié)):
- {
 - "title": "文章標(biāo)題",
 - "description": "文章內(nèi)容",
 - "comments":
 - [
 - {
 - "text": "回復(fù)1",
 - "user": "張三"
 - },
 - {
 - "text": "回復(fù)1",
 - "user": "張三"
 - }
 - ]
 - }
 
如果需要返回一條數(shù)據(jù),并且要列出評(píng)論時(shí),結(jié)果會(huì)是這樣:
- {
 - "message": "fetch data successed",
 - "status": true,
 - "article":
 - {
 - "title": "文章標(biāo)題",
 - "description": "文章內(nèi)容",
 - "comments":
 - [
 - {
 - "text": "回復(fù)1",
 - "user": "張三"
 - },
 - {
 - "text": "回復(fù)1",
 - "user": "張三"
 - }
 - ]
 - }
 - }
 
如果需要返回一個(gè)文章列表,并且沒(méi)有評(píng)論時(shí),會(huì)是這樣:
- {
 - "message": "fetch data successed",
 - "status": true,
 - "articles":
 - [
 - {
 - "title": "文章標(biāo)題1",
 - "description": "文章內(nèi)容1",
 - "comments": []
 - },
 - {
 - "title": "文章標(biāo)題2",
 - "description": "文章內(nèi)容2",
 - "comments": []
 - }
 - ]
 - }
 
看到了吧?這樣的方式下,我們對(duì)于里面元素 article 里結(jié)構(gòu)是完全一樣的,而對(duì)于整個(gè)返回結(jié)構(gòu),也是相似的。
堅(jiān)持這樣做,可以為自己和他人節(jié)省大量的時(shí)間。
7. 重視出錯(cuò)后的返回信息
API 開(kāi)發(fā),應(yīng)該既能處理正確的請(qǐng)求,也能處理錯(cuò)誤的請(qǐng)求。錯(cuò)誤的請(qǐng)求并不可怕,可怕的是你沒(méi)有考慮到,或者考慮到了,但沒(méi)有給到調(diào)用端足夠的細(xì)節(jié)。
在 API 返回中,很多人在這里會(huì)忽略 HTTP 的狀態(tài)代碼,也就是 HttpStatus。
HTTP 協(xié)議,為我們定義了超過(guò) 50 種不同的狀態(tài)代碼,涵蓋了幾乎所有的場(chǎng)景。每個(gè)代碼都有獨(dú)特的含義,應(yīng)該在獨(dú)特的場(chǎng)景中使用。這個(gè)內(nèi)容網(wǎng)上有很多,我就簡(jiǎn)單列一下:
1xx - 信息性響應(yīng)代碼,簡(jiǎn)單說(shuō)就是一個(gè)狀態(tài)通知。
2xx - 成功響應(yīng)代碼。所有的成功都會(huì)在這個(gè)范圍。通常我們見(jiàn)到的是 200,但也有別的成功情況。
3xx - 重定向響應(yīng)代碼。請(qǐng)求被服務(wù)器重定向到另一個(gè) URL,就會(huì)有這個(gè)返回。
4xx - 客戶端錯(cuò)誤響應(yīng)代碼。最常見(jiàn)的是 400,請(qǐng)求協(xié)議格式或內(nèi)容錯(cuò)誤。
5xx - 服務(wù)器錯(cuò)誤響應(yīng)。最常見(jiàn)的是 500,服務(wù)端程序,也就是 API 的內(nèi)部,有內(nèi)存溢出或異常拋出。
開(kāi)發(fā)中,我們可以充分并準(zhǔn)確使用這些狀態(tài)碼。這樣,所有的開(kāi)發(fā)人員,會(huì)在相同的認(rèn)識(shí)層次上理解問(wèn)題的狀態(tài)和原因,從而使得 API 變得普遍易懂、一致和標(biāo)準(zhǔn)。
這不是 REST 的標(biāo)準(zhǔn),但應(yīng)該作為我們開(kāi)發(fā) REST 的標(biāo)準(zhǔn)。
有了狀態(tài)碼,這只是第一步。當(dāng)運(yùn)行出錯(cuò)時(shí),我們需要向調(diào)用端提供盡可能多的細(xì)節(jié)。當(dāng)然,這并不容易,我們需要能夠考慮并預(yù)測(cè) API 會(huì)如何出錯(cuò),調(diào)用者會(huì)做什么,不會(huì)做什么。所以,通常一個(gè) API 第一步是進(jìn)行嚴(yán)格的請(qǐng)求數(shù)據(jù)驗(yàn)證:數(shù)據(jù)是否存在、值是否在我們期望的范圍內(nèi)、是否可以將他們存入數(shù)據(jù)庫(kù)。
拿上面的例子來(lái)說(shuō),GET /client/23,取 clientId = 23 的數(shù)據(jù),我們需要做以下的工作:
檢查請(qǐng)求是否有 clientId 參數(shù),如果沒(méi)有,應(yīng)該是一個(gè) 400 的狀態(tài)
檢查傳入的 clientId = 23 的記錄是否存在,如果不存在,返回響應(yīng) 404
如果找到記錄,則返回響應(yīng) 200
這只是一個(gè)簡(jiǎn)單的例子,真實(shí)的編程時(shí),需要考慮的會(huì)更多。
而且,除了狀態(tài)碼外,還要返回相應(yīng)的錯(cuò)誤消息,例如:輸入?yún)?shù) clientId 沒(méi)有輸入、ID 為 23 的數(shù)據(jù)記錄不存在,等等。
重要的是,提供詳細(xì)的錯(cuò)誤信息,可以幫助開(kāi)發(fā)者和調(diào)用方了解到底什么地方發(fā)生了問(wèn)題。
放心,調(diào)用者不會(huì)將這些信息顯示給最終用戶,但可以通過(guò)這些信息來(lái)快速的定位和解決問(wèn)題。
8. 盡可能優(yōu)化
在現(xiàn)代編程中,API 在體系中的角色,絕對(duì)是整個(gè)操作的大腦。所以,對(duì)于 API 的開(kāi)發(fā),最基本的要求是快速和優(yōu)化,決不能讓 API 成為整個(gè)系統(tǒng)和生態(tài)的痛點(diǎn)。
要求就這么簡(jiǎn)單。
我們可以做很多事情來(lái)確保交付一個(gè)具備良好性能和可伸縮性的 API。來(lái)看看我們能做什么?
首先是數(shù)據(jù)庫(kù)級(jí)別的優(yōu)化。通常說(shuō) API 慢的時(shí)候,十有八九與數(shù)據(jù)庫(kù)有關(guān)。糟糕的數(shù)據(jù)庫(kù)設(shè)計(jì)、復(fù)雜的查詢、緩慢的硬件環(huán)境,甚至缺乏緩存,都是慢的理由。所以,開(kāi)發(fā)過(guò)程中,應(yīng)該隨時(shí)關(guān)注并始終優(yōu)化數(shù)據(jù)庫(kù)結(jié)構(gòu)、查詢、索引以及與數(shù)據(jù)庫(kù)交互的所有內(nèi)容。
接下來(lái)是緩存。很多人不愿意用緩存,因?yàn)闀?huì)將代碼變復(fù)雜。但是從實(shí)際效果上,越大、越復(fù)雜的系統(tǒng),越應(yīng)該通過(guò)緩存?zhèn)鬟f數(shù)據(jù)。有時(shí)候,緩存數(shù)據(jù)庫(kù)查詢能減少 100% 的加載時(shí)間。而絕大多數(shù)數(shù)據(jù),不會(huì)進(jìn)行頻繁的改變。把緩存用起來(lái),調(diào)用端的兄弟們,會(huì)把你當(dāng)親兄弟的。
另一個(gè)影響性能的因素是 API 發(fā)送到調(diào)用端的數(shù)據(jù)量。要做到確保 API 只返回調(diào)用端需要的數(shù)據(jù),而不是全部。如果可能,不要每次都返回完整的模型細(xì)節(jié)和關(guān)系。試一下,但要與響應(yīng)中的返回模型保持一致。
最后,別忘了壓縮。如果可以,使用 Brotli,或者至少也使用 Gzip 來(lái)壓縮數(shù)據(jù)。簡(jiǎn)單的配置,可以獲得減少 50-75% 的傳輸數(shù)據(jù),多好!
9. 做個(gè)體貼的開(kāi)發(fā)者
這個(gè)要求無(wú)關(guān)技術(shù),但我還是想寫(xiě)出來(lái)。
作為一個(gè)開(kāi)發(fā)人員,我們要明白,項(xiàng)目不是一個(gè)人的事。當(dāng)我們寫(xiě)完最后一行代碼,提交并合并后,你可能會(huì)認(rèn)為工作已經(jīng)完成。但不是,對(duì)其他很多人來(lái)說(shuō),這才是個(gè)剛剛開(kāi)始。
很多人在我們完成了工作后,才能開(kāi)始他們的工作。所以,我們需要以多種方式準(zhǔn)備 API。我們要確保 API 能正常工作,要有很好的文檔,更重要的事,我們需要準(zhǔn)備好集成支持。不過(guò)文檔寫(xiě)得有多好,在集成過(guò)程中,及以后的過(guò)程中,總會(huì)有問(wèn)題,各種問(wèn)題。
所以,設(shè)身處地的為他人著想,盡量讓他們的工作變得容易些。構(gòu)建一個(gè)良好的API,遵循我們?cè)谶@里定義的規(guī)則,編寫(xiě)優(yōu)秀的文檔,并為所有人服務(wù)。
10. 寫(xiě)完了
寫(xiě)完了。
上面九條,是我團(tuán)隊(duì)中執(zhí)行的標(biāo)準(zhǔn)和要求。
這里我也必須說(shuō), REST 本身并不是一個(gè)標(biāo)準(zhǔn),所以也不會(huì)有人告訴你什么是對(duì)的,什么是錯(cuò)的。開(kāi)發(fā)的時(shí)候多想一下:作為開(kāi)發(fā)人員,我們每天都在尋找使代碼更好、更漂亮、更高效的模式,那么為什么不在 API 中也做同樣的事呢?















 
 
 










 
 
 
 