分佈式系統核心:REST風格的架構,REST成熟度模型及REST API管理

成熟度模型

正如前文所述,正確、完整地使用REST是困難的,關鍵在於RoyFielding所定義的REST只是一種架構風格,它並不是規範,所以也就缺乏可以直接參考的依據。好在Leonard Richardson補充了這方面的不足。

他提出的關於REST的成熟度模型(Richardson Maturity Model),將REST的實現劃分爲不同的等級。圖8-1展示了不同等級的成熟度模型。


第0級:使用HTTP作爲傳輸方式

在第0級中,Web服務只是使用HTTP作爲傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。SOAP和XML-RPC都屬於此級別。比如,在一個醫院掛號系統中,醫院會通過某個URI來暴露出該掛號服務端點(Service Endpoint)。然後患者會向該URI發送一個文檔作爲請求,文檔中包含了請求的所有細節。


POST /appointmentService HTTP/1.1
[省略了其他頭的信息……]
<openSlotRequest date = "2010-01-04" doctor = "mjones"/>

然後服務器會傳回一個包含了所需信息的文檔。

HTTP/1.1 200 OK
[省略了其他頭的信息……]
<openSlotList>
<slot start = "1400" end = "1450">
<doctor id = "mjones"/>
</slot>
<slot start = "1600" end = "1650">
<doctor id = "mjones"/>
</slot>
</openSlotList>

在這個例子中我們使用了XML,但是內容實際上可以是任何格式,比如JSON、YAML、鍵值對等,或者其他自定義的格式。

有了這些信息,下一步就是創建一個預約。這同樣可以通過向某個端點發送一個文檔來完成。

POST /appointmentService HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointmentRequest>

如果一切正常,那開發者能夠收到一個預約成功的響應。

HTTP/1.1 200 OK
[省略了其他頭的信息……]<appointment>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>

如果發生了問題,比如有人在我前面預約上了,那我會在響應體中收到某條錯誤信息。

HTTP/1.1 200 OK
[省略了其他頭的信息……]
<appointmentRequestFailure>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
<reason>Slot not available</reason>
</appointmentRequestFailure>

到目前爲止,這都是非常直觀的基於RPC風格的系統。它是簡單的,因爲只有Plain Old XML(POX)在這個過程中被傳輸。如果你使用SOAP或者XML-RPC,原理也是基本相同的,唯一的不同是——你將XML消息包含在了某種特定的格式中。


第1級:引入了資源的概念

在第1級中,Web服務引入了“資源”的概念,每個資源有對應的標識符和表達。所以,不是將所有的請求發送到單個服務端點,而是和單獨的資源進行交互。

因此在我們的首個請求中,對指定醫生會有一個對應的資源。

POST /doctors/mjones HTTP/1.1
[省略了其他頭的信息……]
<openSlotRequest date = "2010-01-04"/>

響應會包含一些基本信息,包含了各個時間段的就診時間信息,這些就診時間可以被作爲資源單獨處理。

HTTP/1.1 200 OK[省略了其他頭的信息……]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>

有了這些資源後,創建一個預約就是向某個特定的就診時間發送請求。

POST /slots/1234 HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>

如果一切順利,則會收到和前面類似的響應:

HTTP/1.1 200 OK
[省略了其他頭的信息……]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>

第2級:根據語義使用HTTP動詞

在第2級中,Web服務使用不同的HTTP方法來進行不同的操作,並且使用HTTP狀態碼來表示不同的結果。例如,GET方法用來獲取資源,DELETE方法用來刪除資源。

在醫院掛號系統中,獲取醫生的就診時間信息需要使用GET。

GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk

響應和之前使用POST發送請求時一致。

HTTP/1.1 200 OK
[省略了其他頭的信息……]
<openSlotList><slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>

像上面那樣使用GET來發送一個請求是至關重要的。HTTP將GET定義爲一個安全的操作,它並不會對任何事物的狀態造成影響。這也就允許我們可以用不同的順序若干次調用GET請求而每次還能夠獲取到相同的結果。一個重要的結論就是,GET允許參與到路由中的參與者使用緩存機制,該機制是讓目前的Web運轉良好的關鍵因素之一。HTTP包含許多方法來支持緩存,這些方法可以在通信過程中被所有的參與者使用。通過遵守HTTP的規則,我們可以很好地利用該緩存。

爲了創建一個預約,我們需要使用一個能夠改變狀態的請求方式。

這裏使用和前面相同的一個POST請求。

POST /slots/1234 HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>

如果一切順利,則服務會返回一個201響應來表明新增了一個資源。這是與第1級的POST響應完全不同的。第2級中的操作響應都有統一的返回狀態碼。

HTTP/1.1 201 Created
Location: slots/1234/appointment
[省略了其他頭的信息……]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>

201響應包含了一個Location屬性,它是一個URI。將來客戶端可以通過GET請求獲得該資源的狀態。以上的響應還包含該資源的信息,從而省去了一個獲取該資源的請求。當出現問題時,第2級和第1級還有一個不同之處。比如某人預約了該時段:

HTTP/1.1 409 Conflict
[various headers]
<openSlotList>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>

在上例中,409表明該資源已經被更新了。與使用200作爲響應碼再附帶一個錯誤信息相比,在第2級中我們會明確響應碼的含義,以及其所對應的響應信息。

第3級:使用HATEOAS

在第3級中,Web服務使用HATEOAS。HATEOAS是Hypertext AsThe Engine Of Application State的縮寫,是指在資源的表達中包含了鏈接信息,客戶端可以根據鏈接來發現可以執行的動作。

從上述REST成熟度模型中可以看到,使用HATEOAS的REST服務是成熟度最高的,也是Roy Fielding所推薦的“超文本驅動”的做法。對於不使用HATEOAS的REST服務,客戶端和服務器的實現之間是緊密耦合的。客戶端需要根據服務器提供的相關文檔來了解所暴露的資源和對應的操作。當服務器發生變化(如修改了資源的URI)時,客戶端也需要進行相應的修改。而在使用HATEOAS的REST服務中,客戶端可以通過服務器提供的資源的表達來智能地發現可以執行的操作。當服務器發生了變化時,客戶端並不需要做出修改,因爲資源的URI和其他信息都是被動態發現的。下面是一個HATEOAS的例子。

{
"id": 711,
"manufacturer": "bmw",
"model": "X5",
"seats": 5,"drivers": [
{
"id": "23",
"name": "Way Lau",
"links": [
{
"rel": "self",
"href": "/api/v1/drivers/23"
}
]
}
]
}

回到我們的醫院掛號系統案例中,還是使用在第2級中使用過的GET作爲首個請求。

GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk

但是響應中添加了一個新元素。

HTTP/1.1 200 OK
[省略了其他頭的信息……]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450">
<link rel = "/linkrels/slot/book"
uri = "/slots/1234"/>
</slot>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650">
<link rel = "/linkrels/slot/book"
uri = "/slots/5678"/>
</slot>
</openSlotList>

每個就診時間信息現在都包含一個URI,用來告訴我們如何創建一個預約。

超媒體控制(Hypermedia Control)的關鍵在於:它告訴我們下一步能夠做什麼,以及相應資源的URI。比如,我們事先就可以知道去哪個地址發送預約請求,因爲響應中的超媒體控制直接在響應體中告訴了我們該如何做。

預約的POST請求與第2級中類似。

POST /slots/1234 HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>

響應包含了一系列的超媒體控制,用來告訴我們後面可以進行什麼操作。

HTTP/1.1 201 Created
Location: http://royalhope.nhs.uk/slots/1234/appointment
[省略了其他頭的信息……]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
<link rel = "/linkrels/appointment/cancel"
uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/addTest"
uri = "/slots/1234/appointment/tests"/>
<link rel = "self"
uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/changeTime"
uri = "/doctors/mjones/slots?date=20100104@status=open"/>
<link rel = "/linkrels/appointment/updateContactInfo"
uri = "/patients/jsmith/contactInfo"/>
<link rel = "/linkrels/help"
uri = "/help/appointment"/>
</appointment>

超媒體控制的一個顯著優點是:它能夠在保證客戶端不受影響的條件下,改變服務器返回的URI方案。只要客戶端查詢“addTest”這個URI,後臺開發團隊就可以根據需要隨意修改與之對應的URI(只有最初的入口URI不能被修改)。

超媒體控制的另一個顯著優點是:它能夠幫助客戶端開發人員進行探索。其中的鏈接告訴了客戶端開發人員下面可能需要執行的操作。它並不會告訴所有的信息,但是至少它提供了一個思考的起點,引導開發人員在協議文檔中查看相應的URI。

同樣地,它也讓服務器端的團隊可以通過向響應中添加新的鏈接來增加功能。比如,如果客戶端開發人員發現了一個之前未知的鏈接,那他們就會知道這個鏈接是服務器端提供的新的功能。

REST API管理

下面介紹幾種簡潔的REST API設計的最佳實踐,可以作爲真假REST的一個判別依據。

1.使用的是名詞而不是動詞

使用名詞來定義接口。

/resources
/resources/1024
不應該使用動詞:
/getAllResources
/createNewResource
/deleteAllResources

2.GET方法和查詢參數不能改變資源狀態

如果要改變資源的狀態,要使用PUT、POST和DELETE。下面使用GET方法來修改user的狀態是錯誤的:

GET /users/711?activate
或
GET /users/711/activate

3.使用名詞複數

不要混淆名詞的單複數。保持簡單,只用複數名詞來定義所有資源。

/cars 代替 /car
/users 代替 /user
/products 代替 /product/settings 代替 /setting

4.使用子資源來表達資源間的關係

GET /cars/711/drivers/ 返回 711 號 car 的所有 driver 列表

GET /cars/711/drivers/4 返回 711 號 car 的 4 號 driver

5.使用HTTP header來序列化格式

客戶端、服務器都需要知道相互之間的通信格式。這些格式可以定義在HTTP header裏面。

·Content-Type定義了請求格式。

·Accept定義了接收相應的格式列表。

6.使用HATEOAS約束

HATEOAS是REST架構風格中最複雜的約束,也是構建成熟REST服務的核心。它的重要性在於打破了客戶端和服務器之間嚴格的合約,使得客戶端可以更加智能和自適應,而REST服務本身的演化和更新也變得更加容易。

下面是一個HATEOAS的例子。

{
"id": 711,
"manufacturer": "bmw",
"model": "X5",
"seats": 5,
"drivers": [
{
"id": "23",
"name": "Stefan Jauker",
"links": [
{
"rel": "self",
"href": "/api/v1/drivers/23"
}
]
}
]}

7.提供過濾、排序、字段選擇、分頁

過濾:

GET /cars?color=red
GET /cars?seats<=2

排序:

GET /cars?sort=-manufactorer,+model

字段選擇:

GET /cars?fields=manufacturer,model,id,color

分頁:

GET /cars?offset=10&limit=5

8.API版本化

版本號使用簡單的序號,並避免點號,如2.5等。正確用法如下。

/blog/api/v1

9.充分使用HTTP狀態碼來處理錯誤

HTTP狀態碼(HTTP Status Code)是用來表示網頁服務器HTTP響應狀態的3位數字代碼。它由RFC 2616規範定義,並得到RFC 2518、RFC 2817、RFC 2295、RFC 2774、RFC 4918等規範的擴展。

在設計API處理錯誤時,應該充分使用HTTP狀態碼,而不是簡單地拋出一個“500-Internal Server Error(內部服務器錯誤)”。所有的異常都應該有一個錯誤的payload作爲映射,下面是一個例子。

{
"errors": [
{
"userMessage": "Sorry, the requested resource does not exist",
"internalMessage": "No car found in the database",
"code": 34,
"more info": "http://dev.mwaysolutions.com/blog/api/v1/errors/12345"
}
]
}

最後

拓展乾貨閱讀:一線大廠面試題、高併發等主流技術資料

文章來源:頭條——我不禿頭
整理不易,望珍惜!如果本文對你有所幫助!請點擊頭像查看“傻姑個人簡介”閱讀更多技術乾貨文章,幫助大家一起學習成長!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章