在理解其真正概念前,咱們先來明確: REST它的核心思想是面向資源的抽象(相對於RPC就是面向過程抽象),它是一種設計風格的指導,而非具備較強約束的協議。前端
REST源於Roy Thomas Fielding在2000年發表的博士論文「Architectural Stylesand the Design of Network-based Software Architectures」[1]提出的一種編程思想,併爲這種程序設計風格取了一個不少人難以理解,可是今天已經廣爲人知的名字——REST(Representational State Transfer,表徵狀態轉移)。程序員
若是拆分紅獨立單詞Representational
、State
、Transfer
,咱們知道它們分別是表徵
、狀態
、轉移
的意思。可是合在一塊兒,好像又不明白它想要表達的意思了。咱們不妨先去理解什麼是HTTP(畢竟REST是創建在HTTP之上的),你會發現REST其實是「HTT」(Hypertext Transfer)的進一步抽象,二者的關係就如同接口與實現類的關係通常。REST是對資源的抽象,何爲資源?web
資源(Resource):譬如你如今正在閱讀一篇名爲《REST設計》的文章,這篇文章的內容自己(你能夠將其理解爲蘊含的信息、數據)稱之爲「資源」。不管你是經過閱讀購買的圖書、瀏覽器上的網頁仍是打印出來的文稿,不管是在電腦屏幕上閱讀仍是在手機上閱讀,儘管呈現的樣子各不相同,但其中的信息是不變的,你所閱讀的還是同一份「資源」。數據庫
而後咱們以此文章爲資源,來看看表徵
、狀態
、轉移
在閱讀文章過程的中含義:編程
表徵(Representation):當你經過瀏覽器閱讀此文章時,瀏覽器會向服務端發出「我須要這個資源的HTML格式」的請求,服務端向瀏覽器返回的這個HTML就被稱爲「表徵」,你也能夠經過其餘方式拿到本文的PDF、Markdown、RSS等其餘形式的版本,它們一樣是一個資源的多種表徵。可見「表徵」是指信息與用戶交互時的表示形式,這與咱們軟件分層架構中常說的「表示層」(PresentationLayer)的語義實際上是一致的。json
狀態(State):當你讀完了這篇文章,想看後面是什麼內容時,你向服務端發出「給我下一篇文章」的請求。可是「下一篇」是個相對概念,必須依賴「當前你正在閱讀的文章是哪一篇」才能正確迴應,這類在特定語境中才能產生的上下文信息被稱爲「狀態」。咱們所說的有狀態(Stateful)抑或是無狀態(Stateless),都是隻相對於服務端來講的,服務端要完成「取下一篇」的請求,要麼本身記住用戶的狀態,如這個用戶如今閱讀的是哪一篇文章,這稱爲有狀態;要麼由客戶端來記住狀態,在請求的時候明確告訴服務端,如我正在閱讀某某文章,如今要讀它的下一篇,這稱爲無狀態。後端
轉移(Transfer):不管狀態是由服務端仍是由客戶端來提供,「取下一篇文章」這個行爲邏輯只能由服務端來提供,由於只有服務端擁有該資源
及其表徵
形式。服務端經過某種方式,把「用戶當前閱讀的文章」轉變成「下一篇文章」,這就被稱爲「表徵狀態轉移」。瀏覽器
將用戶界面所關注的邏輯和數據存儲所關注的邏輯分離開來,有助於提升用戶界面的跨平臺的可移植性。相較以往的徹底基於服務端控制和渲染(如JSP這類)的模式已甚少,一方面代碼倉庫的便捷性和易管理性成爲了敏捷開發的障礙,另外一方面得益於前端技術(從ES規範,到語言實現,再到前端框架等)在近年來的高速發展,造就了現現在的**先後端分離*模式:後端控制數據,前端控制渲染。緩存
REST但願服務端不用負責維護狀態,每一次從客戶端發送的請求中,應包括全部必要的上下文信息,會話信息也由客戶端負責保存維護,服務端只依據客戶端傳遞的狀態來執行業務處理邏輯,驅動整個應用的狀態變遷。安全
但現實是骨感的,大型系統的上下文狀態數量徹底可能膨脹到客戶端沒法承受的程度,在服務端的內存、會話、數據庫或者緩存等地方持有必定的狀態成爲一種事實上存在,並將長期存在、被普遍使用的主流方案。
無狀態服務雖然提高了系統的可見性、可靠性和可伸縮性,但下降了系統的網絡性。「下降網絡性」的通俗解釋是某個功能使用有狀態的設計時只須要一次(或少許)請求就能完成,使用無狀態的設計時則可能會須要屢次請求,或者在請求中帶有額外冗餘的信息。爲了緩解這個矛盾,REST但願軟件系統可以如同萬維網同樣,容許客戶端和中間的通訊傳遞者(譬如代理)將部分服務端的應答緩存起來。固然,爲了緩存可以正確地運做,服務端的應答中必須直接或者間接地代表自己是否能夠進行緩存、能夠緩存多長時間,以免客戶端在未來進行請求的時候獲得過期的數據。運做良好的緩存機制能夠減小客戶端、服務端之間的交互,甚至有些場景中能夠徹底避免交互,這就進一步提升了性能。
這裏所指的分層並非表示層、服務層、持久層這種意義上的分層,而是指客戶端通常不須要知道是否直接鏈接到了最終的服務器,抑或鏈接到路徑上的中間服務器。中間服務器能夠經過負載均衡和共享緩存的機制提升系統的可擴展性,這樣也便於緩存、伸縮和安全策略的部署。該原則的典型應用是內容分發網絡(ContentDistribution Network,CDN)。若是你是經過網站瀏覽到這篇文章的話,你所發出的請求通常(假設你在中國境內的話)並非直接訪問位於GitHub Pages的源服務器,而是訪問了位於國內的CDN服務器,但做爲用戶,你徹底不須要感知到這一點。
這是REST的另外一條核心原則,REST但願開發者面向資源編程,但願軟件系統設計的重點放在抽象系統該有哪些資源,而不是抽象系統該有哪些行爲(服務)上。這條原則你能夠類比計算機中對文件管理的操做來理解,管理文件可能會涉及建立、修改、刪除、移動等操做,這些操做數量是可數的,並且對全部文件都是固定、統一的。若是面向資源來設計系統,一樣會具備相似的操做特徵,因爲REST並無設計新的協議,因此這些操做都借用了HTTP協議中固有的操做命令來完成。
統一接口也是REST最容易陷入爭論的地方,基於網絡的軟件系統,究竟是面向資源合適,仍是面向服務更合適,這個問題恐怕在很長時間裏都不會有定論,也許永遠都沒有。可是,已經有一個基本清晰的結論是:面向資源編程的抽象程度一般更高。抽象程度高帶來的壞處是距離人類的思惟方式每每會更遠,而好處是通用程度每每會更好。用這樣的語言去詮釋REST,仍是有些抽象,下面以一個例子來講明:譬如,對於幾乎每一個系統都有的登陸和註銷功能,若是你理解成登陸對應於login()服務,註銷對應於logout()服務這樣兩個獨立服務,這是「符合人類思惟」的;若是你理解成登陸是PUT Session,註銷是DELETE Session,這樣你只須要設計一種「Session資源」便可知足需求,甚至之後對Session的其餘需求,如查詢登陸用戶的信息,就是GET Session而已,其餘操做如修改用戶信息等也均可以被這同一套設計囊括在內,這即是「抽象程度更高」帶來的好處。
若是想要在架構設計中合理恰當地利用統一接口,Fielding建議系統應能作到每次請求中都包含資源的ID,全部操做均經過資源ID來進行;建議每一個資源都應該是自描述的消息;建議經過超文原本驅動應用狀態的轉移
按需代碼被Fielding列爲一條可選原則。它是指任何按照客戶端(譬如瀏覽器)的請求,將可執行的軟件程序從服務端發送到客戶端的技術。按需代碼賦予了客戶端無須事先知道全部來自服務端的信息應該如何處理、如何運行的寬容度。舉個具體例子,之前的Java Applet技術,今天的WebAssembly等都屬於典型的按需代碼,蘊含着具體執行邏輯的代碼是存放在服務端,只有當客戶端請求了某個JavaApplet以後,代碼纔會被傳輸並在客戶端機器中運行,結束後一般也會隨即在客戶端中被銷燬。將按需代碼列爲可選原則的緣由並不是是它特別難以達到,更可能是出於必要性和性價比的實際考慮。
RESTful Web APIs和RESTful Web Services的做者Leonard Richardson曾提出一個衡量「服務有多麼REST」的Richardson成熟度模型(Richardson MaturityModel,RMM),以便讓那些本來不使用REST的系統,可以逐步地導入REST。Richardson將服務接口「REST的程度」從低到高,分爲0至3級。
下面借用Martin Fowler撰寫的關於RMM的文章中的實際例子(原文是XML寫的,這裏簡化爲JSON表示),來具體展現一下四種不一樣程度的REST反映到實際接口中會是怎樣的。假設你是那名程序員,你會怎麼設計:
醫生預定系統
做爲一名病人,我想要從系統中得知指定日期內我熟悉的醫生是否具備空閒時間,以便於我向該醫生預定就診。 請設計兩個RESTful接口:一個查詢空閒時間接口,一個預定就診接口。
醫院開放了一個/appointmentService的Web API,傳入日期、醫生姓名等參數,能夠獲得該時間段內該名醫生的空閒時間,該API的一次HTTP調用以下所示:
POST /appointmentService?query HTTP/1.1
{"data": "2020-03-04", "doctor": "mjones"} 複製代碼
而後服務器會傳回一個包含了所需信息的迴應:
HTTP/1.1 200 OK
[ {"start":"14:00", "end":"14:50", "doctor": "mjones"}, {"start":"16:00", "end":"16:50", "doctor": "mjones"} ] 複製代碼
獲得了醫生空閒的結果後,筆者以爲14:00比較合適,因而進行預定確認,並提交了我的基本信息:
POST /appointmentService?action=confirm HTTP/1.1
{ "appointment": {"date": "2020-03-04", "start":"14:00", "end":"14:50", "doctor": "mjones"}, "patient": {"name": "zio", "age": 30, ...} } 複製代碼
若是預定成功,那我可以收到一個預定成功的響應:
HTTP/1.1 200 OK
{ "code": 0, "message": "Successful confirmation of appiontment" } 複製代碼
若是出現問題,譬若有人在我前面搶先預定了,那麼我會在響應中收到某種錯誤消息:
HTTP/1.1 200 OK
{ "code": 1, "message": "doctor not available" } 複製代碼
至此,整個預定服務宣告完成,直接明瞭,咱們採用的是很是直觀的基於RPC風格的服務設計
第0級是RPC的風格,若是需求永遠不會變化,那它徹底能夠良好地工做下去。可是,若是你不想爲預定醫生以外的其餘操做、爲獲取空閒時間以外的其餘信息去編寫額外的方法,或者改動現有方法的接口,那仍是應該考慮一下如何使用REST來抽象資源。
通往REST的第一步是引入資源的概念,在API中的基本體現是圍繞資源而不是過程來設計服務,說得直白一點,能夠理解爲服務的Endpoint應該是一個名詞而不是動詞。此外,每次請求中都應包含資源的ID,全部操做均經過資源ID來進行,譬如,獲取醫生指定時間的空閒檔期:
GET /doctors/mjones?date="2020-03-04" HTTP/1.1
複製代碼
而後服務器傳回一組包含了ID信息的檔期清單,注意,ID是資源的惟一編號,有ID即表明「醫生的檔期」被視爲一種資源:
HTTP/1.1 200 OK
[ {"id": 1, "start":"14:00", "end":"14:50", "doctor": "mjones"}, {"id": 2, "start":"16:00", "end":"16:50", "doctor": "mjones"} ] 複製代碼
筆者仍是以爲14:00的時間比較合適,因而又進行預定確認,並提交了我的基本信息:
POST /schedules/1 HTTP/1.1
{"name": "zio", "age":30, ...} 複製代碼
後面預定成功或者失敗的響應消息在這個級別裏面與以前一致,就不重複了。比起第0級,第1級的特徵是引入了資源,經過資源ID做爲主要線索與服務交互,但第1級至少還有三個問題沒有解決:一是隻處理了查詢和預定,若是臨時想換個時間,要調整預定,或者病突然好了,想刪除預定,這都須要提供新的服務接口;二是處理結果響應時,只能依靠結果中的code、message這些字段作分支判斷,每一套服務都要設計可能發生錯誤的code,這很難考慮全面,並且也不利於對某些通用的錯誤作統一處理;三是沒有考慮認證受權等安全方面的內容,譬如要求只有登陸用戶才容許查詢醫生檔期時間,某些醫生可能只對VIP開放,須要特定級別的病人才能預定,等等。
第1級遺留的三個問題均可以經過引入統一接口來解決。HTTP協議的七個標準方法是通過精心設計的,只要架構師的抽象能力夠用,它們幾乎能涵蓋資源可能遇到的全部操做場景。REST的具體作法是:把不一樣業務需求抽象爲對資源的增長、修改、刪除等操做來解決第一個問題;使用HTTP協議的Status Code,它能夠涵蓋大多數資源操做可能出現的異常,也能夠自定義擴展,以此解決第二個問題;依靠HTTP Header中攜帶的額外認證、受權信息來解決第三個問題,這個在實戰中並無體現。
按這個思路,獲取醫生檔期,應採用具備查詢語義的GET操做進行:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
複製代碼
而後服務器會傳回一個包含了所需信息的迴應:
HTTP/1.1 200 OK
[ {"id": 1, "start":"14:00", "end":"14:50", "doctor": "mjones"}, {"id": 2, "start":"16:00", "end":"16:50", "doctor": "mjones"} ] 複製代碼
筆者仍然以爲14:00的時間比較合適,因而進行預定確認,並提交了我的基本信息,用以建立預定,這是符合POST的語義的:
POST /schedules/1 HTTP/1.1
{"name": "zio", "age":30, ...} 複製代碼
若是預定成功,那筆者可以收到一個預定成功的響應:
HTTP/1.1 201 Created
Successful confirmation of appointment 複製代碼
[插圖]若是出現問題,譬若有人搶先預定了,那麼筆者會在響應中收到某種錯誤消息:
HTTP/1.1 409 Conflict
doctor not available 複製代碼
第2級是目前絕大多數系統所到達的REST級別,但仍不是完美的,至少還存在一個問題:你是如何知道預定mjones醫生的檔期是須要訪問「/schedules/1234」這個服務Endpoint的?也許你第一時間甚至沒法理解爲什麼我會有這樣的疑問,這固然是程序代碼寫的呀!但REST並不認同這種已烙在程序員腦海中許久的想法。RMM中的超文本控制、Fielding論文中的HATEOAS和如今提的比較多的「超文本驅動」,所但願的是除了第一個請求是由你在瀏覽器地址欄輸入驅動以外,其餘的請求都應該可以本身描述清楚後續可能發生的狀態轉移,由超文本自身來驅動。因此,當你輸入了查詢的指令以後:
GET /doctors/mjones/schedule?date=2020-03-04&status=open HTTP/1.1
複製代碼
服務器傳回的響應信息應該包括諸如如何預定檔期、如何瞭解醫生信息等可能的後續操做:
HTTP/1.1 200 OK
[ { "id": 1, "start":"14:00", "end":"14:50", "doctor": "mjones", "links": [ {"rel": "confirm schedule", "href": "/schedule/1"} ] }, { "id": 2, "start":"16:00", "end":"16:50", "doctor": "mjones", "links": [ {"rel": "confirm schedule", "href": "/schedule/2"} ] } ] 複製代碼
若是作到了第3級REST,那服務端的API和客戶端也是徹底解耦的,此時若是你要調整服務數量,或者對同一個服務作API升級時將會變得很是簡單。
對於第3級須要明確:若是客戶端指的是移動端這類發佈升級成本較高的場景,這樣的設計確實友誼頗高;但若是是客戶端是web前端,它們的發佈成本和服務端無差,那麼能夠case by case的去看待,是否要把這類「href」信息維護在服務端,這樣作是否有悖於「先後端分離」的思想。