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