相關博文:html
SpringBoot 構建RestFul API 含單元測試 github
REST是目前業界至關火熱的術語,彷佛發佈的API不帶個REST前綴,你都很差意思和別人打招呼了。 然而大部分號稱REST的API實際上並無達到Richardson成熟度模型的第三個級別:Hypermedia。 而REST的發明者Roy Fielding博士更是直言「Hypermedia做爲應用引擎」是REST的前提, 這不是一個可選項,若是沒有Hypermedia,那就不是REST。(摘自Infoq對Fielding博士的第二段訪談)json
那究竟什麼是Hypermedia? 採用Hypermedia的API在響應(response)中除了返回資源(resource)自己外,還會額外返回一組連接(link)。 這組連接描述了對於該資源,消費者(consumer)接下來能夠作什麼以及怎麼作。api
舉例來講,假設向API發起一次get請求,獲取指定訂單的資源表述(representation),那麼它應該長得像這樣:app
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/hal+json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 05 Jun 2015 02:54:57 GMT { "tracking_id": "123456", "status": "WAIT_PAYMENT", "items": [ { "name": "potato", "quantity": 1 } ], "_links": { "self": { "href": "http://localhost:57900/orders/123456" }, "cancel": { "href": "http://localhost:57900/orders/123456" }, "payment": { "href": "http://localhost:57900/orders/123456/payments" } } }
這看起來頗有趣,然而這對API的消費者來講有什麼好處呢?post
不知道在你的開發生涯中有沒有遇到過這樣的事情:單元測試
有一天,產品經理跟我說,咱們要和某某酒店集團對接,在線銷售它們的酒店,這是他們的聯繫人和詳細的API說明文檔。 API說明文檔真夠詳細,有好幾十頁,憑着豐富的行業經驗,我知道我須要找到其中的哪些API來實現基本的業務場景。 幾天後,我實現了大部分的API集成,如今能夠預訂酒店了,訂單已經在對方的測試環境生成,大功告成。 等等,這個「添加訂單財務信息」的API是幹嗎的?在和對方的聯繫人聯繫後,被告知「沒什麼用」,好了,真的大功告成了,上線!
兩週後,咱們的API使用權限被對方關閉了,緣由是「全部的訂單都沒有財務信息,沒法確認對帳」。
「等等,那誰誰誰不是說這個API沒用嗎?」
「噢,他已經離職了,大家若是要恢復使用,儘快完成這個API的集成吧」
「我。。。」
固然,這裏面還有許多別的因素,可是消費者的開發人員每每很難將業務場景和實現業務場景的API聯繫起來。 他們經常面對是:測試
Hypermedia帶來的API自描述特性,使用連接的方式提示接下來作什麼和怎麼作,正好能夠緩解這樣的窘境。 若是API服務方能夠提供測試環境供消費者測試,那麼開發人員能夠實際動手探索業務場景的銜接,這時再配合API文檔狀況就好多了。 回到酒店訂單的例子,若是是這樣,我可能就不會挨批了:spa
// 預訂後,提示確認訂單,那麼不熟悉爲何要確認以及不確認的後果的同窗就能夠想到去問啦 { "tracking_id": "123456", "Hotel": "A ZHAO DAI SUO", "status": "WAIT_ACKNOWLEDGED", "_links": { "self": { "href": "http://zhaodaisuo.com/orders/123456" }, "cancel": { "href": "http://zhaodaisuo.com/orders/123456" }, "acknowledge": { "href": "http://zhaodaisuo.com/orders/123456/payments" } } } // 確認後,提示添加財務信息,不熟悉的同窗就能夠問了,然而我真的已經問了呀。。。。 { "tracking_id": "123456", "Hotel": "A ZHAO DAI SUO", "status": "ACKNOWLEDGED", "_links": { "self": { "href": "http://zhaodaisuo.com/orders/123456" }, "billing": { "href": "http://zhaodaisuo.com/orders/123456/bill" } } }
不知道在你的開發生涯中有沒有遇到過這樣的事情:
有一天,產品經理跟我說,咱們要實現一個新功能blablabla,可是依賴的API版本太老了,這是他們的聯繫人。
「你好呀,請問咱們須要這個信息,可是如今1.3的版本中沒有提供,有什麼辦法嗎?」
「你能夠升級到2.1的版本就有了」
「那這個版本是否是向後兼容的啊?咱們用了其中不少接口哦,我不想其它的集成點出問題」
「那固然,放心吧」
結果固然是個悲傷的故事,「你給我過來,我保證不打死你」。 API的發佈方也須要增長新功能,API自身也會隨着需求變化,因而產生了版本號
http://www.zhaodaisuo.com/api/v1.2
然而一套API通常會包括多個API,爲整套API版本化的粒度太粗了。 一旦消費者但願得到其中某個API的新特性,他/她只能選擇全盤升級並仔細測試或者爲每一個集成點配置單獨的uri。 這都不夠好,而Hypermedia能夠改變這種局面。 因爲提供了連接來告訴消費者資源的uri,相對「傳統」的REST API,uri變成了一種弱耦合, Hypermedia API只須要公佈少許入口uri就能夠了。好比,以以前酒店訂單的例子,只需發佈
http://www.zhaodaisuo.com/orders
後續的確認、財務信息的uri是在實際API調用的時候拿到的,無需事先準備。 消費者和發佈者之間的強耦合實際上只剩下入口uri和服務契約(解釋資源的含義), 當服務契約新增或是發生破壞性的變化時(例如修改了或刪除了參數),只須要在資源表述中增長新的連接。
{ "tracking_id": "123456", "Hotel": "A ZHAO DAI SUO", "status": "ACKNOWLEDGED", "_links": { "self": { "href": "http://zhaodaisuo.com/orders/123456" }, "billing": { "href": "http://zhaodaisuo.com/orders/123456/bill" }, "billing-v1.1": { //billing發生了破壞性變化 "href": "http://zhaodaisuo.com/orders/123456/bill/v1" }, "coupon": { //新增了優惠券抵用的契約 "href": "http://zhaodaisuo.com/orders/123456/coupon" } } }
這樣版本化的粒度就下移到了服務契約的級別,這時消費者就靈活多了,只要按需修改對應的集成點就好了。
不知道在你的開發生涯中有沒有遇到過這樣的事情:
有一天,產品經理跟我說,咱們依賴的一個API發佈者通知我,如今判斷訂單是否可以用優惠券的條件變化,這是他們的聯繫人。
「你好呀,請問如今訂單可否用優惠券的判斷條件有什麼變化?」
「原來,大家能夠經過訂單的狀態來判斷,如今還須要結合訂單的來源,參加秒殺活動的訂單不能使用優惠券」
「好吧。。。」
因而我在集成代碼中作了以下修改:
if (order.status().equals("WAIT_PAYMENT")) { if (order.source().equals("miaosha")) { couponButtonEnabled = false; } else { //.... } } else { //... }
好糾結,就不能把API設計成這樣嗎?
{ "tracking_id": "123456", "Hotel": "A ZHAO DAI SUO", "status": "WAIT_PAYMENT", "source": "normal", "_links": { "coupon": { "href": "http://zhaodaisuo.com/orders/123456/coupon" } } } { "tracking_id": "123456", "Hotel": "A ZHAO DAI SUO", "status": "WAIT_PAYMENT", "source": "miaosha", "_links": {} //秒殺來源的訂單不返回含優惠券連接的資源表述。 }
這樣客戶端代碼就簡單了,依賴於抽象的業務場景,而不是依賴於具體的實現
if (order.containsLink("coupon")) { couponButtonEnabled = true; } else { couponButtonEnabled = false; }