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