1 概述
1.1 撰寫目的
本文用于定義一種統(tǒng)一的RESTful接口設(shè)計(jì)方案,希望具有參考價(jià)值。本文所描述的方案比較學(xué)院派,在上一家公司提出沒有被采納,在所了解到的有限的若干家聲稱采用了RESTful風(fēng)格的公司里,發(fā)現(xiàn)他們也偏離甚遠(yuǎn)。當(dāng)然,他們這么做是有理由的,我也理解,這只是取舍問題。這篇文章其實(shí)是舊文了,2016年年底就已經(jīng)寫好,但是一直躺在電腦的硬盤里,不想白費(fèi)了當(dāng)時(shí)的功夫,因此在此公開。
1.2 為什么采用REST
目的是為了服務(wù)端與客戶端的解耦。SOA僅僅是從結(jié)構(gòu)上將前后端分離,但是實(shí)際上數(shù)據(jù)邏輯還是沒有實(shí)現(xiàn)解耦,服務(wù)端接口升級(jí)往往會(huì)影響客戶端,兩者的行為需要嚴(yán)格約定。而REST采用HTTP協(xié)議進(jìn)行約定,客戶端僅僅需要按照HTTP協(xié)議來(lái)理解服務(wù)端返回的數(shù)據(jù),雖然與業(yè)務(wù)相關(guān)的數(shù)據(jù)結(jié)構(gòu)還是需要約定,但是這確實(shí)進(jìn)一步解耦了服務(wù)端與客戶端。
另外,由于嚴(yán)格遵照HTTP協(xié)議進(jìn)行數(shù)據(jù)返回,對(duì)于安全的接口,可以在返回的Header里設(shè)置緩存策略(接口安全性的概念在下文會(huì)解釋)。
1.3 文檔結(jié)構(gòu)
第二部分將闡述關(guān)于RESTful的若干個(gè)關(guān)鍵的概念,明確第二部分闡述的幾個(gè)概念有利于設(shè)計(jì)、實(shí)現(xiàn)優(yōu)雅規(guī)范的接口。
第三部分就URL命名的問題進(jìn)行約定。
第四部分對(duì)消息實(shí)體進(jìn)行約定。
第五部分對(duì)『向RESTful接口發(fā)起請(qǐng)求』進(jìn)行闡述,約定要實(shí)現(xiàn)的方法,約定請(qǐng)求的頭部和body的格式。
第六部分對(duì)接口的響應(yīng)格式進(jìn)行約定,包括響應(yīng)消息的頭部、狀態(tài)碼、JSON實(shí)體。
第七部分對(duì)版本控制的問題進(jìn)行約定。
第八部分對(duì)RESTful接口的實(shí)現(xiàn)提出了實(shí)現(xiàn)工具的建議。
2 關(guān)鍵概念
明確一些關(guān)鍵的概念是很重要的,雖然RESTful風(fēng)格的API設(shè)計(jì)方案并沒有統(tǒng)一的標(biāo)準(zhǔn),但是還是需要符合一定的原則進(jìn)行設(shè)計(jì),否則就不能稱為RESTful風(fēng)格的API。因?yàn)樵S多人并沒有對(duì)REST進(jìn)行充分的了解就宣稱自己的API是RESTful風(fēng)格的API,以至于RESTful的提出者Fielding博士本人無(wú)法忍受,在2008年為此專門寫了一篇博客『REST APIs must be hypertext-driven』,hypertext-driven與HATEOAS是同一個(gè)概念的不同表述,在下文會(huì)進(jìn)行闡述。
2.1 RESTful
REST不是一種協(xié)議,也不是一種文件格式,更不是一種開發(fā)框架。它是一系列的設(shè)計(jì)約束的集合:無(wú)狀態(tài)性、將超媒體作為應(yīng)用狀態(tài)的引擎等。REST是Representation State Transfer的縮寫,中文是『表述性狀態(tài)轉(zhuǎn)移』,這里就涉及到資源的表述與狀態(tài)兩個(gè)概念。
簡(jiǎn)單地說(shuō),資源可以看作是服務(wù)器上存儲(chǔ)的所有數(shù)據(jù),資源的表述則是服務(wù)器對(duì)外提供的指向這些資源的方式,使用JSON、XML等均可,一個(gè)資源可以有多種表述;資源的狀態(tài)則是服務(wù)器的數(shù)據(jù)存儲(chǔ)狀態(tài),例如在t時(shí)刻,服務(wù)器中存儲(chǔ)了m條數(shù)據(jù),這時(shí)候客戶端向服務(wù)端提交了一個(gè)創(chuàng)建數(shù)據(jù)的請(qǐng)求,服務(wù)器處理了此請(qǐng)求并創(chuàng)建了一條數(shù)據(jù),那么在t+1時(shí)刻,服務(wù)器中就存儲(chǔ)了m+1條數(shù)據(jù),這兩個(gè)時(shí)刻的資源狀態(tài)就是不一樣的,t時(shí)刻發(fā)生的請(qǐng)求導(dǎo)致了資源狀態(tài)的改變。
2.2 HATEOAS
Hypermedia As The Engine Of Application State,超媒體作為應(yīng)用程序狀態(tài)的引擎。這是REST區(qū)別于其他SOA風(fēng)格的主要特點(diǎn)??蛻舳伺c服務(wù)端進(jìn)行互動(dòng)的時(shí)候,完全是通過(guò)服務(wù)端動(dòng)態(tài)提供的超媒體進(jìn)行的。除了對(duì)超媒體的一般理解,客戶端不需要知道其他額外的知識(shí)。相反,在一些SOA接口的設(shè)計(jì)中,客戶端與服務(wù)端的通信是要事先進(jìn)行約定的,例如通過(guò)文檔或者接口描述語(yǔ)言(Interface Description Language, IDL)。而基于HTTP協(xié)議的REST設(shè)計(jì)里,一般采用的就是請(qǐng)求與響應(yīng)的Header來(lái)體現(xiàn)HATEOAS原則(具體請(qǐng)參考:https://en.wikipedia.org/wiki/HATEOAS)。這里也隱含這樣一層含義:REST應(yīng)盡可能地利用HTTP標(biāo)準(zhǔn)中現(xiàn)有的東西,例如Header、標(biāo)準(zhǔn)方法與狀態(tài)碼。
從標(biāo)準(zhǔn)的角度看,HTTP標(biāo)準(zhǔn)是一項(xiàng)RFC標(biāo)準(zhǔn),世界認(rèn)可;而其他自定義的SOA標(biāo)準(zhǔn)則可能是一項(xiàng)個(gè)人標(biāo)準(zhǔn)或者公司標(biāo)準(zhǔn),最多是一項(xiàng)互聯(lián)網(wǎng)草案(這對(duì)大部分公司來(lái)說(shuō)都不可能),而一項(xiàng)標(biāo)準(zhǔn)越是被廣為認(rèn)可接受,其實(shí)現(xiàn)的通用性就越強(qiáng)。個(gè)人標(biāo)準(zhǔn)和公司標(biāo)準(zhǔn)都五花八門,這樣對(duì)每一個(gè)標(biāo)準(zhǔn)都要參照其相關(guān)文檔實(shí)現(xiàn)相應(yīng)的行為邏輯是很麻煩的。
2.3 安全性
一個(gè)方法被調(diào)用1次與被調(diào)用0次是一樣的,此方法就是安全的,否則就是不安全的。例如,一個(gè)方法A僅僅是讀取數(shù)據(jù),并不創(chuàng)建或者修改數(shù)據(jù),不論A方法被調(diào)用多少次,都不對(duì)數(shù)據(jù)記錄產(chǎn)生任何影響,A方法是安全的。而假如有另一個(gè)方法B對(duì)數(shù)據(jù)進(jìn)行刪除,B方法被調(diào)用1次后,數(shù)據(jù)會(huì)被刪除(或者標(biāo)識(shí)位被修改),系統(tǒng)里的數(shù)據(jù)發(fā)生了變化,那么B方法是不安全的。
2.4 冪等性
一個(gè)方法被同樣地調(diào)用1次與被調(diào)用多次是一樣的,即同樣的輸入會(huì)得到同樣的輸出,此方法就是冪等的,否則就不是冪等的。
2.3節(jié)中A方法與B方法都是冪等的,一個(gè)安全的方法一定是冪等的,一個(gè)冪等的方法不一定是安全的。
假設(shè)一個(gè)方法C對(duì)某個(gè)全局計(jì)數(shù)器執(zhí)行自增操作并寫入數(shù)據(jù)庫(kù),每次調(diào)用C方法都會(huì)對(duì)系統(tǒng)數(shù)據(jù)產(chǎn)生影響,那么C方法就不是冪等的。
3 URL命名
URL用于標(biāo)識(shí)資源,因此URL應(yīng)該以名詞進(jìn)行命名,例如/users, /users/children等。
一般URL會(huì)內(nèi)嵌參數(shù),例如要獲取id為313的user的信息,那么URL應(yīng)該為/users/313,前面的user采用復(fù)數(shù),如果要列出其所有后代,則URL應(yīng)為/users/313/children,children為復(fù)數(shù)形式,如果要獲取其id為499的后代,則URL應(yīng)為/users/313/children/499
4 消息實(shí)體
消息實(shí)體,就是請(qǐng)求和響應(yīng)消息中的entity-body(也稱為body),消息實(shí)體采用JSON字符串格式。
5 請(qǐng)求
5.1 方法
使用HTTP標(biāo)準(zhǔn)定義的請(qǐng)求方法。
5.1.1 get
獲取資源,單個(gè)參數(shù)一般寫在URL上,多個(gè)參數(shù)則作為query parameter附在URL后面,例如:
單個(gè)參數(shù):/user/123, 表示id為123的user
多個(gè)參數(shù):/user?name=tom&phone=13787890987&gender=male
get方法應(yīng)為冪等的,并且不對(duì)數(shù)據(jù)記錄產(chǎn)生影響。對(duì)于漢字與特殊字符,應(yīng)該進(jìn)行urlencode。
5.1.2 post
創(chuàng)建資源,請(qǐng)求的headers里設(shè)置Content-type為application/json,參數(shù)為json類型。
根據(jù)約定,在創(chuàng)建成功之后,返回的狀態(tài)碼應(yīng)該是201(Created),并且在response的Header里設(shè)置Location為新創(chuàng)建的資源的URL,例如,創(chuàng)建了一個(gè)新的user,該user創(chuàng)建后id為888,那么Header里應(yīng)該設(shè)置Location為/users/888,當(dāng)然,這應(yīng)該是一個(gè)完整的URL,這里只是給出了一個(gè)相對(duì)路徑的URI以作為說(shuō)明。返回了這些數(shù)據(jù)后,客戶端可以自定義后續(xù)行為,或者查看創(chuàng)建后的user,或者刷新當(dāng)前的user列表,這些行為服務(wù)端并不關(guān)心。
如果重復(fù)提交了相同的數(shù)據(jù),第一次應(yīng)該返回201,以后則應(yīng)返回409(Conflict),并且在response的Header里設(shè)置Location指向已經(jīng)存在的資源,說(shuō)明沖突的來(lái)源。
5.1.3 put
更新資源,對(duì)現(xiàn)有資源進(jìn)行修改,請(qǐng)求的headers與post一樣,參數(shù)也是。此方法應(yīng)該是冪等的。
5.1.4 delete
刪除資源。此方法應(yīng)是冪等的。
5.2 Header
Content-type應(yīng)設(shè)為application/json。
另外應(yīng)設(shè)置一個(gè)version,指明所使用的接口版本。這不屬于HTTP協(xié)議中的一部分,是自定義的,出于版本控制的考量,具體見第七章。
5.3 body
采用JSON字符串,具體的結(jié)構(gòu)有待商定,這不屬于HTTP協(xié)議的一部分,是自定義的。
這里主要放置業(yè)務(wù)相關(guān)的數(shù)據(jù)。
6 響應(yīng)
6.1 Header
根據(jù)響應(yīng)的狀態(tài)碼不同,相應(yīng)地設(shè)置頭部,具體見下一節(jié)。
但是在我所了解的公司里,做法都是統(tǒng)一返回200,然后在返回的JSON字符串里設(shè)置消息碼。我是不能理解的。據(jù)一位前端同學(xué)說(shuō),前端代碼接收到了請(qǐng)求以后,不方便獲取Http狀態(tài)碼。其實(shí)我也寫過(guò)前端,不深入,但是一些基本的知識(shí)還是有的,我覺得這并不難做到,估計(jì)是他的代碼封裝的時(shí)候沒有考慮到這一點(diǎn),現(xiàn)在要改比較麻煩,所以不想大動(dòng)干戈、傷筋動(dòng)骨。
6.2 狀態(tài)碼
| 狀態(tài)碼 | 語(yǔ)義 | 使用場(chǎng)景 |
|---|---|---|
| 200 | OK | 正常返回消息,什么問題也沒有 |
| 201 | Created | 創(chuàng)建資源成功,Header里應(yīng)設(shè)置Location指向新創(chuàng)建的資源 |
| 202 | Accepted | 請(qǐng)求已被接收,但是處理過(guò)程較長(zhǎng),不能馬上返回結(jié)果 |
| 304 | Not Modified | 沒有任何修改發(fā)生 |
| 401 | Unauthorized | 缺乏權(quán)限,指已經(jīng)登錄但是缺乏請(qǐng)求這個(gè)資源的權(quán)限 |
| 403 | Forbidden | 拒絕訪問,可用于未登錄時(shí)攔截返回的狀態(tài)碼,此時(shí)Header里應(yīng)設(shè)置Location為登錄頁(yè)面的URL |
| 404 | Not Found | 不存在所請(qǐng)求的資源 |
| 406 | Not Acceptable | 請(qǐng)求沒有被接收,參數(shù)約束校驗(yàn)不通過(guò),或者其他業(yè)務(wù)類型的錯(cuò)誤都可以返回這個(gè)狀態(tài)碼,response的body里應(yīng)有表示錯(cuò)誤信息的JSON實(shí)體。 |
| 409 | Conflict | 請(qǐng)求的資源有沖突,例如多次提交一樣的創(chuàng)建請(qǐng)求,response的Header里應(yīng)設(shè)置Location為產(chǎn)生沖突的資源的URL |
| 500 | Internal Server Error | 服務(wù)器的非業(yè)務(wù)類錯(cuò)誤,response的body里應(yīng)有表示錯(cuò)誤信息的JSON實(shí)體 |
6.3 body采用JSON字符串。
JSON的結(jié)構(gòu)分為兩種:成功、失敗。
一般而言,只有返回200的時(shí)候才需要讀取成功的JSON,只有返回406和500的時(shí)候才需要讀取失敗的JSON,對(duì)于其他的狀態(tài)碼,客戶端不需要服務(wù)器提供額外的消息。
對(duì)于成功的JSON,里面應(yīng)該只包含一個(gè)result對(duì)象,而失敗的JSON應(yīng)該使用這樣的結(jié)構(gòu):
{
error: {
code: xxx,
message: "xxx",
data: {...}
}
}
失敗的JSON只有一個(gè)error對(duì)象,包含錯(cuò)誤碼、消息及相關(guān)數(shù)據(jù),message應(yīng)該是直接可讀的消息,客戶端毋需理解發(fā)生了什么錯(cuò)誤,客戶端只需將消息展示出來(lái)即可。在收到406的時(shí)候,客戶端只需知道發(fā)生的錯(cuò)誤是由客戶端造成的即可,具體是什么類型并不需要知道,將消息直接展示出來(lái),讓使用的人知道是什么即可,所以message應(yīng)該是人類可以理解的文本。同理,收到500的時(shí)候,只需知道這個(gè)錯(cuò)誤是服務(wù)端的問題即可,客戶端也毋需知道具體的錯(cuò)誤類型,最多就將錯(cuò)誤碼和消息展示出來(lái),讓使用者有反饋的依據(jù)即可。
7 版本控制
考慮到接口有可能升級(jí),升級(jí)的類型有幾種:
新增功能接口
原有接口返回?cái)?shù)據(jù)增加字段
現(xiàn)有接口返回?cái)?shù)據(jù)變更現(xiàn)有字段格式或刪除現(xiàn)有字段
現(xiàn)有接口變更業(yè)務(wù)邏輯
刪除接口
其中,前兩種升級(jí)并不會(huì)影響客戶端,因此毋需處理。而后面三種會(huì)導(dǎo)致使用舊接口的客戶端不能正常工作。
一般服務(wù)端升級(jí)與客戶端升級(jí)都不是同步的,客戶端升級(jí)往往會(huì)滯后,因此在服務(wù)端升級(jí)后應(yīng)該保留舊版本的接口繼續(xù)運(yùn)行一段時(shí)間,讓未升級(jí)的客戶端可以繼續(xù)工作一段時(shí)間,同時(shí)可以上線新版本的客戶端。過(guò)一段時(shí)間后再將舊版本的接口下線。
而版本控制應(yīng)該是向下兼容的,即假設(shè)當(dāng)前版本是1.2,如果客戶端請(qǐng)求1.3版本的服務(wù),應(yīng)當(dāng)用當(dāng)前版本提供服務(wù)。如果沒有注明請(qǐng)求的版本號(hào),應(yīng)當(dāng)提供當(dāng)前版本的服務(wù)。
一般情況下,客戶端請(qǐng)求需要帶版本號(hào),但是服務(wù)端并不需要對(duì)此進(jìn)行處理,除非是同時(shí)運(yùn)行新舊版本的同一個(gè)接口,才需要做差異處理。
8 實(shí)現(xiàn)
8.1 Spring HATEOAS
Spring HATEOAS可以很方便地與Spring MVC結(jié)合來(lái)開發(fā)RESTful接口。具體參照其文檔:
http://docs.spring.io/spring-hateoas/docs/0.20.0.RELEASE/reference/html/#fundamentals.jaxb-json
原文鏈接:
https://bungder.github.io/2017/07/24/REST/
我的技術(shù)博客:
https://bungder.github.io
為什么簡(jiǎn)書的MarkDown不支持表格語(yǔ)法......