本文用於定義一種統一的RESTful接口設計方案,但願具備參考價值。本文所描述的方案比較學院派,在上一家公司提出沒有被採納,在所瞭解到的有限的若干家聲稱採用了RESTful風格的公司裏,發現他們也偏離甚遠。固然,他們這麼作是有理由的,我也理解,這只是取捨問題。這篇文章實際上是舊文了,2016年年末就已經寫好,可是一直躺在電腦的硬盤裏,不想白費了當時的功夫,所以在此公開。html
目的是爲了服務端與客戶端的解耦。SOA僅僅是從結構上將先後端分離,可是實際上數據邏輯仍是沒有實現解耦,服務端接口升級每每會影響客戶端,二者的行爲須要嚴格約定。而REST採用HTTP協議進行約定,客戶端僅僅須要按照HTTP協議來理解服務端返回的數據,雖然與業務相關的數據結構仍是須要約定,可是這確實進一步解耦了服務端與客戶端。前端
另外,因爲嚴格遵守HTTP協議進行數據返回,對於安全的接口,能夠在返回的Header裏設置緩存策略(接口安全性的概念在下文會解釋)。git
第二部分將闡述關於RESTful的若干個關鍵的概念,明確第二部分闡述的幾個概念有利於設計、實現優雅規範的接口。github
第三部分就URL命名的問題進行約定。spring
第四部分對消息實體進行約定。數據庫
第五部分對『向RESTful接口發起請求』進行闡述,約定要實現的方法,約定請求的頭部和body的格式。json
第六部分對接口的響應格式進行約定,包括響應消息的頭部、狀態碼、JSON實體。後端
第七部分對版本控制的問題進行約定。緩存
第八部分對RESTful接口的實現提出了實現工具的建議。安全
明確一些關鍵的概念是很重要的,雖然RESTful風格的API設計方案並無統一的標準,可是仍是須要符合必定的原則進行設計,不然就不能稱爲RESTful風格的API。由於許多人並無對REST進行充分的瞭解就宣稱本身的API是RESTful風格的API,以致於RESTful的提出者Fielding博士本人沒法忍受,在2008年爲此專門寫了一篇博客『REST APIs must be hypertext-driven』,hypertext-driven與HATEOAS是同一個概念的不一樣表述,在下文會進行闡述。
REST不是一種協議,也不是一種文件格式,更不是一種開發框架。它是一系列的設計約束的集合:無狀態性、將超媒體做爲應用狀態的引擎等。REST是Representation State Transfer的縮寫,中文是『表述性狀態轉移』,這裏就涉及到資源的表述與狀態兩個概念。
簡單地說,資源能夠看做是服務器上存儲的全部數據,資源的表述則是服務器對外提供的指向這些資源的方式,使用JSON、XML等都可,一個資源能夠有多種表述;資源的狀態則是服務器的數據存儲狀態,例如在t時刻,服務器中存儲了m條數據,這時候客戶端向服務端提交了一個建立數據的請求,服務器處理了此請求並建立了一條數據,那麼在t+1時刻,服務器中就存儲了m+1條數據,這兩個時刻的資源狀態就是不同的,t時刻發生的請求致使了資源狀態的改變。
Hypermedia As The Engine Of Application State,超媒體做爲應用程序狀態的引擎。這是REST區別於其餘SOA風格的主要特色。客戶端與服務端進行互動的時候,徹底是經過服務端動態提供的超媒體進行的。除了對超媒體的通常理解,客戶端不須要知道其餘額外的知識。相反,在一些SOA接口的設計中,客戶端與服務端的通訊是要事先進行約定的,例如經過文檔或者接口描述語言(Interface Description Language, IDL)。而基於HTTP協議的REST設計裏,通常採用的就是請求與響應的Header來體現HATEOAS原則(具體請參考:https://en.wikipedia.org/wiki/HATEOAS)。這裏也隱含這樣一層含義:REST應儘量地利用HTTP標準中現有的東西,例如Header、標準方法與狀態碼。
從標準的角度看,HTTP標準是一項RFC標準,世界承認;而其餘自定義的SOA標準則多是一項我的標準或者公司標準,最可能是一項互聯網草案(這對大部分公司來講都不可能),而一項標準越是被廣爲承認接受,其實現的通用性就越強。我的標準和公司標準都五花八門,這樣對每個標準都要參照其相關文檔實現相應的行爲邏輯是很麻煩的。
一個方法被調用1次與被調用0次是同樣的,此方法就是安全的,不然就是不安全的。例如,一個方法A僅僅是讀取數據,並不建立或者修改數據,不論A方法被調用多少次,都不對數據記錄產生任何影響,A方法是安全的。而假若有另外一個方法B對數據進行刪除,B方法被調用1次後,數據會被刪除(或者標識位被修改),系統裏的數據發生了變化,那麼B方法是不安全的。
一個方法被一樣地調用1次與被調用屢次是同樣的,即一樣的輸入會獲得一樣的輸出,此方法就是冪等的,不然就不是冪等的。
2.3節中A方法與B方法都是冪等的,一個安全的方法必定是冪等的,一個冪等的方法不必定是安全的。
假設一個方法C對某個全局計數器執行自增操做並寫入數據庫,每次調用C方法都會對系統數據產生影響,那麼C方法就不是冪等的。
URL用於標識資源,所以URL應該以名詞進行命名,例如/users
, /users/children
等。
通常URL會內嵌參數,例如要獲取id爲313的user的信息,那麼URL應該爲/users/313
,前面的user採用複數,若是要列出其全部後代,則URL應爲/users/313/children
,children爲複數形式,若是要獲取其id爲499的後代,則URL應爲/users/313/children/499
消息實體,就是請求和響應消息中的entity-body
(也稱爲body),消息實體採用JSON字符串格式。
使用HTTP標準定義的請求方法。
獲取資源,單個參數通常寫在URL上,多個參數則做爲query parameter附在URL後面,例如:
get方法應爲冪等的,而且不對數據記錄產生影響。對於漢字與特殊字符,應該進行urlencode。
建立資源,請求的headers裏設置Content-type
爲application/json
,參數爲json類型。
根據約定,在建立成功以後,返回的狀態碼應該是201(Created),而且在response的Header裏設置Location爲新建立的資源的URL,例如,建立了一個新的user,該user建立後id爲888,那麼Header裏應該設置Location
爲/users/888
,固然,這應該是一個完整的URL,這裏只是給出了一個相對路徑的URI以做爲說明。返回了這些數據後,客戶端能夠自定義後續行爲,或者查看建立後的user,或者刷新當前的user列表,這些行爲服務端並不關心。
若是重複提交了相同的數據,第一次應該返回201,之後則應返回409(Conflict),而且在response的Header裏設置Location指向已經存在的資源,說明衝突的來源。
更新資源,對現有資源進行修改,請求的headers與post同樣,參數也是。此方法應該是冪等的。
刪除資源。此方法應是冪等的。
Content-type應設爲application/json。
另外應設置一個version,指明所使用的接口版本。這不屬於HTTP協議中的一部分,是自定義的,出於版本控制的考量,具體見第七章。
採用JSON字符串,具體的結構有待商定,這不屬於HTTP協議的一部分,是自定義的。
這裏主要放置業務相關的數據。
根據響應的狀態碼不一樣,相應地設置頭部,具體見下一節。
可是在我所瞭解的公司裏,作法都是統一返回200,而後在返回的JSON字符串裏設置消息碼。我是不能理解的。據一位前端同窗說,前端代碼接收到了請求之後,不方便獲取Http狀態碼。其實我也寫過前端,不深刻,可是一些基本的知識仍是有的,我以爲這並不難作到,估計是他的代碼封裝的時候沒有考慮到這一點,如今要改比較麻煩,因此不想大動干戈、傷筋動骨。
狀態碼 | 語義 | 使用場景 |
---|---|---|
200 | OK | 正常返回消息,什麼問題也沒有 |
201 | Created | 建立資源成功,Header裏應設置Location指向新建立的資源 |
202 | Accepted | 請求已被接收,可是處理過程較長,不能立刻返回結果 |
304 | Not Modified | 沒有任何修改發生 |
401 | Unauthorized | 缺少權限,指已經登陸可是缺少請求這個資源的權限 |
403 | Forbidden | 拒絕訪問,可用於未登陸時攔截返回的狀態碼,此時Header裏應設置Location爲登陸頁面的URL |
404 | Not Found | 不存在所請求的資源 |
406 | Not Acceptable | 請求沒有被接收,參數約束校驗不經過,或者其餘業務類型的錯誤均可以返回這個狀態碼,response的body裏應有表示錯誤信息的JSON實體。 |
409 | Conflict | 請求的資源有衝突,例如屢次提交同樣的建立請求,response的Header裏應設置Location爲產生衝突的資源的URL |
500 | Internal Server Error | 服務器的非業務類錯誤,response的body裏應有表示錯誤信息的JSON實體 |
JSON的結構分爲兩種:成功、失敗。
通常而言,只有返回200的時候才須要讀取成功的JSON,只有返回406和500的時候才須要讀取失敗的JSON,對於其餘的狀態碼,客戶端不須要服務器提供額外的消息。
對於成功的JSON,裏面應該只包含一個result對象,而失敗的JSON應該使用這樣的結構:
{ error: { code: xxx, message: "xxx", data: {...} } }
失敗的JSON只有一個error對象,包含錯誤碼、消息及相關數據,message應該是直接可讀的消息,客戶端毋需理解發生了什麼錯誤,客戶端只需將消息展現出來便可。在收到406的時候,客戶端只需知道發生的錯誤是由客戶端形成的便可,具體是什麼類型並不須要知道,將消息直接展現出來,讓使用的人知道是什麼便可,因此message應該是人類能夠理解的文本。同理,收到500的時候,只需知道這個錯誤是服務端的問題便可,客戶端也毋需知道具體的錯誤類型,最多就將錯誤碼和消息展現出來,讓使用者有反饋的依據便可。
考慮到接口有可能升級,升級的類型有幾種:
其中,前兩種升級並不會影響客戶端,所以毋需處理。然後面三種會致使使用舊接口的客戶端不能正常工做。
通常服務端升級與客戶端升級都不是同步的,客戶端升級每每會滯後,所以在服務端升級後應該保留舊版本的接口繼續運行一段時間,讓未升級的客戶端能夠繼續工做一段時間,同時能夠上線新版本的客戶端。過一段時間後再將舊版本的接口下線。
而版本控制應該是向下兼容的,即假設當前版本是1.2,若是客戶端請求1.3版本的服務,應當用當前版本提供服務。若是沒有註明請求的版本號,應當提供當前版本的服務。
通常狀況下,客戶端請求須要帶版本號,可是服務端並不須要對此進行處理,除非是同時運行新舊版本的同一個接口,才須要作差別處理。
Spring HATEOAS能夠很方便地與Spring MVC結合來開發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/
個人技術博客: https://bungder.github.io