一篇帶你理解Restful風(fēng)格
REST是什么
REST從2000年被Roy Fielding提出距今已有20多年,其對Web技術(shù)產(chǎn)生了深遠(yuǎn)的影響。REST本身并沒有產(chǎn)生新的技術(shù)或者中間件,REST傳遞的是一種設(shè)計思想,其提供了一種約束原則和條件。
REST全稱為Representational State Transfer,中文為表征性狀態(tài)轉(zhuǎn)移,感覺前面其實(shí)還少了一個主語“資源”,個人理解應(yīng)該是“資源表征性狀態(tài)轉(zhuǎn)移”。而其核心就是通過創(chuàng)造一種資源的定義與描述原則,形成一種標(biāo)準(zhǔn)化規(guī)范,從而減少技術(shù)人員在開發(fā)與溝通時候的成本。
實(shí)現(xiàn)REST風(fēng)格的框架叫Restful架構(gòu)時,而我們主要是使用的HTTP作為這種規(guī)范的載體,本文也是針對HTTP的形式來進(jìn)行討論。但我認(rèn)為,只要滿足REST設(shè)計思想的功能描述方式,都可以算作REST的實(shí)現(xiàn),其并不局限于HTTP協(xié)議。
理解REST
Representational State Transfer,其實(shí)已經(jīng)將REST的整體概念羅列出來了,加上我們補(bǔ)充的主語“資源”,可以很明確的體現(xiàn)出REST中主要的兩個概念:
- 資源,資源表征
- 動作,狀態(tài)轉(zhuǎn)移
簡單理解的話,REST是就是將一個接口動作的描述進(jìn)行拆分,拆分成資源與動作兩個部分。其中,資源就是對描述資源位置,資源表征則是這些資源應(yīng)該如何展示出來(具體是JSON還是XML),而狀態(tài)轉(zhuǎn)移則可以簡單的理解成正對這個資源所進(jìn)行的動作。
REST的正是通過將這兩種核心定義的邏輯進(jìn)行分離、標(biāo)準(zhǔn)化,從而讓對于“接口”、“操作”的定義更加便于理解,和可閱讀(更完成權(quán)威的介紹可以參考:
https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)。
資源
REST中的核心概念之一是對資源的的描述,而這個資源是一個抽象概念,并不一定是一個靜態(tài)的資源,也可以是完整資源的一部分。實(shí)際上,只要是可以被引用的部分,我們都可以稱之為一個資源。
而在web中我們標(biāo)識一個資源使用的是URI(Uniform Resource Identifier)。從定義上來講,URI是唯一標(biāo)識符,即可以說是資源的名稱也可以說是資源的地址。從這個角度上來說如果無法用URI來表示一個內(nèi)容,那么就不能說它是一個資源。
一些正面的例子:
https://blog.csdn.net/losorick/article/details/123311537。
https://xie.infoq.cn/article/3ddce663b21acd89f41582aa3。
一些反面的例子:
/export/create。
資源定義中多個名詞之間用"-"(最好不用用"_",會在特定情況下顯示不完全)來進(jìn)行分割,并用"/"來表示資源增肌的概念。也可以用","或者";"來作為多個資源的分割。當(dāng)然這些都是建議建議,對于具體的實(shí)施只要項(xiàng)目中統(tǒng)一就行,比如github中就是使用"..."來作為多個資源的分割,例如“/git/git/compare/master…next”。
資源表征
URI只定位了具體的資源,但對于客戶端來說是這是一個統(tǒng)一的抽象。如果客戶端要使用的話并不能直接使用,需要指明所需要使用的資源形式。當(dāng)前主流的文本交互方式表征是JSON格式,當(dāng)然對于要求響應(yīng)格式嚴(yán)謹(jǐn)?shù)膱F(tuán)隊來說仍然有使用XML格式的,而對于媒體資源來說也有PNG、MP4等形式。
原則上,資源定位符只負(fù)責(zé)資源的標(biāo)識,但是并不關(guān)注具體怎么展示資源,而需要用何種形式展示資源則是由客戶端根據(jù)自己的需要申請的。在HTTP中我們服務(wù)端通常使用“content-type”來對資源的表征來進(jìn)行描述,而客戶端則是用“Accept”請求頭中來指明所需要的格式類型。
同時,資源的表征并不局限在資源的輸出類型上,資源間的關(guān)聯(lián)關(guān)系也是資源表征的一部分。舉例而言,當(dāng)我們查詢一個詳情信息,查詢后的響應(yīng)體本身是一個資源,用于描述這個詳情信息。如果需求要我們描述這個詳情信息后的下一個詳情信息的訪問鏈接,用于提示用戶進(jìn)一步瀏覽。這個信息“下一個鏈接”的信息,如果我們放到響應(yīng)體中似乎并不是很合適,因?yàn)檫@對于一個GET請求來說,這個“下一個鏈接”信息并不是資源本身的一部分。并且隨著系統(tǒng)中推薦算法的運(yùn)行,這部分信息甚至變成不可緩存的內(nèi)容(因?yàn)榭赡茈S時發(fā)生改變)?;谝陨嫌懻摚蛟S我們增加一個LINK響應(yīng)頭用來描述“下一個鏈接”的信息,就達(dá)到了目的并保證了響應(yīng)體對于資源本身描述的正確性。
所以,資源表征并非是對數(shù)據(jù)庫的CURD,它體現(xiàn)了資源作為一個超媒體中一部分的這個概念(資源與資源的連接關(guān)系)。同時我們可以使用HTTP中的各個部分來獨(dú)立的描述各種概念,而非一股腦的都丟到響應(yīng)體中。
狀態(tài)轉(zhuǎn)移
通過上文中的描述,我們可以將資源理解成了一種靜態(tài)的內(nèi)容。那么該如何理解我們對資源的操作呢?REST是用狀態(tài)轉(zhuǎn)移來表述這件事的。當(dāng)我們新建一個資源時,就將資源從“無”狀態(tài)轉(zhuǎn)移為了存在狀態(tài);當(dāng)我們更新一個資源的時,則是讓其內(nèi)部的狀態(tài)發(fā)生了更改。
REST中的是使用HTTP方法來描述這些操作類型的,這樣的好處是,我們可以為操作類型指定統(tǒng)一的規(guī)范。舉個例子來說,在REST中所有的GET請求都應(yīng)該是可以被緩存的,所有的PUT請求都應(yīng)該是冪等的。我們通過將接口操作約束成明確的HTTP方法或者是其他統(tǒng)一方式,從而減少在接口對接查看時的溝通成本。
常用的HTTP方法主要是:GET、POST、PUT、DELETE。但是更早版本的客戶端可能只有GET和POST。而根據(jù)協(xié)議的升級則支持LOCK、UNLOCK等方法以及自定義方法。但通常企業(yè)內(nèi)會根據(jù)自己的主要受眾設(shè)備進(jìn)行調(diào)整這些設(shè)計。
從Restful出發(fā)的接口規(guī)范
對于一個接口,其中的URI部分應(yīng)該只用于描述操作時針對哪個資源的。而“HTTP方法”應(yīng)該才用于解釋操作的類型的。但是企業(yè)中如果要推行REST接口的規(guī)范的話,仍然有一些問題需要調(diào)整確認(rèn),原因可能是內(nèi)部歷史原因或者當(dāng)前框架與REST并不適配,本小節(jié)舉例其中的兩個例子用于各位參考。
向下兼容
由于在業(yè)務(wù)實(shí)際開發(fā)的過程中,可能會出現(xiàn)業(yè)務(wù)的邏輯變更,我們處理這種問題的主要方法就是通過對接口添加版本信息來實(shí)現(xiàn)的。
但是由上文可知,URI本身應(yīng)該用于定義資源的名稱和地址。所以對于同一個資源來說,內(nèi)容變了就是變了,資源本身是沒有版本的概念,我們實(shí)際上調(diào)整的是資源的不同的表征方式,而這個方式才對應(yīng)的“版本”的概念,而非資源本身。如果是從這個角度出發(fā),對于REST的設(shè)計來說,這個版本的概念就不應(yīng)該出現(xiàn)在URI的資源定位符上,因?yàn)橘Y源的名稱都是同一個。那么對于REST我們可以通過在Accept響應(yīng)頭追加版本信息(version)來區(qū)分具體的表征方式。例如:
- Accept: version=1.0。
- Accept: version=2.1。
- Accept: version=3.0。
但是在實(shí)際的企業(yè)中,我們通常不會這么做。原因有很多,其中一種是因?yàn)樵跇I(yè)務(wù)溝通的時候,通常只聚焦在URI和HTTP方法上。在實(shí)際溝通中我們將URI和HTTP方法作為互相溝通的主要方法,并可以通過一行就表示出全部信息,例如:
- {GET} http://api.example.com/trade/order/1。
所以如果在URI地址上直接添加版本信息,就可以通過以下方式表示:
- {GET} http://api.example.com/trade/v1/order/1。
- {GET} http://api.example.com/trade/v2/order/1。
- {GET} http://api.example.com/trade/v3/order/1。
這樣的好處就是可以通過URI直接描述兼容性信息,而缺點(diǎn)就是破壞了REST原教旨主義的資源定位方法。但REST推出到當(dāng)前已有20多年時間,而實(shí)際業(yè)務(wù)中“資源”數(shù)量也已經(jīng)爆炸數(shù)量增長。所以在服務(wù)治理等各種新概念成為必要需求的今天,符合現(xiàn)狀的調(diào)整才是合適的。
定制的操作
在REST中我們需要用HTTP的方法來定義操作的類型,那么就有一個主要問題:已有方法無法描述當(dāng)前操作怎么辦。一些常見的操作(Postman中有的)是PATCH(github有)、COPY、LINK等。除此之外常見的還有BATCH- CREATE等批量操作。
如果根據(jù)REST原教旨主義則應(yīng)該在HTTP方法中進(jìn)行擴(kuò)展,但由于系統(tǒng)兼容性等問題,我們希望保證各種版本的客戶端可以對方法進(jìn)行支持。
首先,不論我們是使用什么方法來表示自定義操作,都需要滿足統(tǒng)一表達(dá),并且可以應(yīng)用在所有的方法中。根據(jù)這種情況,本小結(jié)列舉出幾種方案作為參考:
擴(kuò)展方法
HTTP中的方法可以自己擴(kuò)展,Github新增了PATCH方法,WebDAV中擴(kuò)展了LOCK、UPLOCK等方法,但這些都不是HTTP中的標(biāo)準(zhǔn)方法,如果使用該方法需要考慮客戶端的支持能力。
參數(shù)定義
通過使用預(yù)留參數(shù)定義擴(kuò)展方法,例如使用_method=DELETE來對請求方法來定義,并在服務(wù)端對具體方法進(jìn)行路由。這種方法的好處是可以將參數(shù)代入到URI中,并不影響URI原版本的資源定位的意義。
URI后綴描述
可以通過在URI添加后綴來描述動作,例如在后綴添加/actions:delete來定義,或者在URI最后使用/actions標(biāo)記,并在請求體中第一層來描述擴(kuò)展的動作類型。這樣雖然導(dǎo)致URI定義中加入了額外的資源地址之外的額外信息,但是可以保證整體訪問接口中的信息是完整的。
總體來說,對于確定定制操作的擴(kuò)展方案要在客戶端支持成都、下游網(wǎng)關(guān)改造難度以及接口可讀性中做出平衡。
Restful風(fēng)格的問題
使用Restful風(fēng)格的構(gòu)建項(xiàng)目中主要需要的問題就是關(guān)于path-variable的處理問題。在一些項(xiàng)目治理項(xiàng)目中默認(rèn)是認(rèn)為有path-variable引起的不同path是不同的URL,所以無法直接使用,需要具有開源軟件二次開發(fā)的能力和需求。
舉一個Sentinel的例子,Sentinel是根據(jù)url生成的資源名稱,而因?yàn)镽EST中用path-variable來定義資源,所以就導(dǎo)致了同一類資源的定義符被識別成了不同的資源。
但隨著查看Sentinel中的代碼我們可以發(fā)現(xiàn),Sentinel中默認(rèn)使用CommonFilter來處理請求的url,并且主要是通過UrlCleaner接口中的clean方法來對資源進(jìn)行重命名,所以我們可以通過重寫clean方法實(shí)現(xiàn)滿足rest的資源明明問題(事實(shí)上官方也提供了
sentinel-spring-webmvc-adapter來支持rest風(fēng)格定義的接口)。
由于REST風(fēng)格是主流的接口規(guī)范風(fēng)格之一,所以使用量較大的中間件一會都會有對應(yīng)的解決方案,但是對于自研的工具需要考慮REST風(fēng)格的兼容性,可以參考一些開源軟件如Swagger的匹配來實(shí)現(xiàn)。
最后
本文討論了REST中的相關(guān)概念,并非完全照本宣科的進(jìn)行陳述,如REST的6大指導(dǎo)原則也并沒有介紹。REST本身是一個比較大的概念,但是在如今前已經(jīng)后端分離、微服務(wù)化等概念逐漸普及,原本的REST概念并非完全的適用。但是REST的核心理念對我們的接口規(guī)范設(shè)計有十分重要的指導(dǎo)性意見。