關於REST, 經常容易引起困惑的一個問題是:在 Machine-to-machine 的 REST 應用中, 客戶端怎麼可能在沒有任何預先知識的情況下就能跟隨服務器端返回的超文本來自適應的將應用程序邏輯進行下去?
答案是不可能 . Client端必須提前瞭解足夠的 server端返回的超文本的知識 , 才能跟隨其中的指示將邏輯進行下去. 那跟 SOAP, WSDL之類的又有啥區別 ? 答案在於 REST 要求的 “足夠知識 ”要遠遠弱於 WSDL對服務接口定義的約束 , 其靈活性則高於 WSDL. 畢竟 , REST 架構風格着眼於生命週期較長的 , 不斷演化的 , 跨組織邊界的應用程序 . 可以用來滿足 HATEOAS這個 REST約束的武器就是 Media Type: 擴展已有的 Media Type, 發明新的Media Type, 並在合適的範圍內標準化
來看兩個例子 .
Web的成功與其說是 HTTP的成功 , 不如說是 HTML的成功 , 或者說 text/html這種 Media Type的成功 . HTML定義了一組有限的 , 標準化的 , 特定領域的標籤 . 所有的 HTML客戶端都能理解並按照自己的能力渲染出 Web服務器返回的任何合法的 HTML. 從全功能的 Chrome/FireFox /Safari/IE, 到只是顯示文本的 lynx, 到資源受限環境下如手機中的各種瀏覽器 , 都能將多姿多彩的 Web呈現給最終用戶 , 並引導他們進行下一步交互
最經常被拿來作爲 machine-to-machine REST 應用成功案例的則是 RSS/ATOM. 這族標準定義了新的 Media Type, application/rss+xml , application/atom+xml . 不出意外的 , 這組 Media Types定義了一組有限的 , 標準化的, 特定領域的標籤 . 每一個 RSS/ATOM客戶端應用都可以從某個資源的 application/rss (atom)+xml的表述開始 ,順藤摸瓜的遍歷每個感興趣的資源 . 這些客戶端都理解每一個 RSS/ATOM定義的標籤 . 服務端可以自由的改變新資源的 URI/URL Template而不必擔心破壞現有的 Client, 因爲這些 Client並不依賴於預先設定的 URL Template,而僅僅依賴於一個 Root的表述 , 及標準的 link relations
回到我們的問題 . 那麼 REST要求的 “足夠知識 ”要弱到什麼程度 , 才能既使客戶端能理解服務器給出的線索 , 又不致於耦合的太緊以致服務器和客戶端無法獨立演化 ? 觀察上面兩個例子 , 它們有以下共同特點 :
1. Response是 Well-formed ,可以被客戶端解析 . 這是廢話 , 除了 AI應用 , 絕大部分網絡應用都得有明確定義的協議 . 但這意味着 REST 應用中也必須明確定義客戶端和服務器數據交換的格式
2. Response中包含了當前上下文 ( 或資源 )相關的信息 , 但對我們這個問題來說更重要的是包含了對其它資源進行訪問的線索 . 是的 , 它是用最最基本 , 最最普通的 link 來實現的 . 足夠弱 . 那語義是否足夠清晰到客戶端能理解每一個 link, 能夠選擇正確的 link並知道如何訪問呢 ? 我們來看看 link上都能承載啥語義 (from http://www.ietf.org/rfc/rfc5988.txt):
Link = "Link" ":" #link-value link-value = "<" URI-Reference ">" *( ";" link-param ) link-param = ( ( "rel" "=" relation-types ) | ( "anchor" "=" <"> URI-Reference <"> ) | ( "rev" "=" relation-types ) | ( "hreflang" "=" Language-Tag ) | ( "media" "=" ( MediaDesc | ( <"> MediaDesc <"> ) ) ) | ( "title" "=" quoted-string ) | ( "title*" "=" ext-value ) | ( "type" "=" ( media-type | quoted-mt ) ) | ( link-extension ) ) link-extension = ( parmname [ "=" ( ptoken | quoted-string ) ] ) | ( ext-name-star "=" ext-value ) ext-name-star = parmname "*" ; reserved for RFC2231-profiled ; extensions. Whitespace NOT ; allowed in between. ptoken = 1*ptokenchar ptokenchar = "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+" | "-" | "." | "/" | DIGIT | ":" | "<" | "=" | ">" | "?" | "@" | ALPHA | "[" | "]" | "^" | "_" | "`" | "{" | "|" | "}" | "~" media-type = type-name "/" subtype-name quoted-mt = <"> media-type <"> relation-types = relation-type | <"> relation-type *( 1*SP relation-type ) <"> relation-type = reg-rel-type | ext-rel-type reg-rel-type = LOALPHA *( LOALPHA | DIGIT | "." | "-" ) ext-rel-type = URI
這裏我們重點考察一下 rel 和 media-type 屬性
在 ATOM(http://www.ietf.org/rfc/rfc4287.txt)裏 , 缺省定義了 5種 link relations, 分別是 alternate, related, self, enclosure和 via. 標準賦予它們明確的語義 , 比如 alternate表示 link所指向的目標是當前資源的備用版本 . 客戶端因此可以理解每個 link的含義 , 自動或引導用戶完成後面的操作 . 如何完成?則牽扯到 media-type . 比如如果media-type定義的是 audio/mp4,客戶端則可以顯示播放選項或自動播放或乾脆忽略 .
這裏 link以及 media-type等屬性幫助形成了一個遞歸的過程 . 我們從某個 link開始 , 知曉它的 media-type(所謂知曉它的 media-type, 就是能理解這種 media-type定義的每一個元素的語義 ), 我們就能夠得到這個 link所指向的資源以這種 media-type表現出來的一種表述 , 其中包含了其它可用的 link以及 media-type, 可以讓這個過程一直繼續下去 , 直到客戶端覺得可以了或者服務端返回了不包含任何其它 link的表述 .
然而初始的 media-type和 link relations總是有限的 , 我們如何應對現有 media-type表達不了的語義?這就是HTML和 RSS/ATOM例子包含的第三個要素
3. 定義領域相關的 media-type . HTML的問題域是如何表達各種顯示效果 , RSS/ATOM則是如何發佈信息 .media-type以及 link relations等都是可擴展的 . 我們要做的就是爲我們的特定領域的應用定義擴展 , 如果現存的被標準化的元素不夠用的話 . 這裏有一個問題 , 就是 REST應用的範圍的問題 . 如果我們的應用是面向 internet的, 面向無數已知的未知的客戶端應用的 , 則意味着我們必須儘可能標準化我們擴展的 media-type, 或者至少讓它廣泛接受 . 而對於企業內部以 REST架構的應用 , 我們同樣面臨標準化的問題 , 只不過範圍可能小一點 , 至少是企業內部需要有共同接受的 media-type
前面我們看到了良好定義的 media-type是如何幫助客戶應用理解 server端的 response而又不致緊密耦合服務端的實現的 . 我們需要一個企業開發的例子來驗證一下我們的理解 . 比如要開發一個企業內部不同應用之間共享用戶信息的應用 , 包括權限信息 , 當前可以執行的操作 , 可以訪問的應用 , 以及應用之間共享的 Preference設置等 .我們可以定義一種叫 application/vnd.tw.account+xml 的 media-type 可以有以下的片段
- <account>
- <preference>
- <link rel="preference" media-type="application/vnd.tw.account.preference+xml" href="http://xxx/preference" mce_href="http://xxx/preference" />
- <link rel="edit" media-type="text/html" href="http://xxx/preference/editForm" mce_href="http://xxx/preference/editForm">
- </preference>
- <subscribed-services>
- <subscribed-service>
- <name>Taxes</name>
- <link rel="subscription" media-type="text/html" href="http://taxes.xxx.com" mce_href="http://taxes.xxx.com" />
- </subscribed-service>
- <subscribed-service>
- <name>Audit</name>
- <link rel="subscription" media-type="text/html" href="http://audit.xxx.com" mce_href="http://audit.xxx.com" />
- </subscribed-service>
- </subscribed-services>
- <contacts>
- <contact>
- <name>Tom</name>
- <link rel="contact" media-type="application/xfn+xml" href="http://account.xxx.com/tom" mce_href="http://account.xxx.com/tom" />
- </contact>
- </contacts>
- </account>
URL Template Considered Harmful
如果這些都實現的話 , 就可以有一個推論: URL Template不是必須的,甚至是有害的 . 它限制了服務器端的變化. 客戶應用應總是從 Root Resource開始 , 在特定 Media Type的引導下解析出其它的 URI, 給予服務程序演化的靈活性而不是按照 URL Template這種預先的知識來推算 .
用REST做過很多項目的Xu Hao 對Url template也有同樣的看法:"URL template在我看來是有害的,它是一種隱含的服務器和客戶端約定。此外還有一點,由於大多數URL template是用來表達state transfer的URL的,比如/xxx/approve之類的,使得客戶端必須瞭解服務器的狀態轉移的細節,這在我看來是一個更大的問題。這使得客戶 端和服務器的耦合變得更加緊密,同時這種風格極度鼓勵服務器藉由客戶端來維持狀態的一致性"
很多REST相關的文章都把大量的篇幅給了 HTTP, 比如 HTTP Verbs POST/GET/PUT/DELETE, HTTP Status Code等 , 而 media type着墨不多 . Xu Hao在社區中曾很多次的提起自定義Media Type, 對REST應用不多可能當時不太明白, 現在越來越多的人認識到擴展和標準化更多的 media type才能更多的發揮 REST的潛力
參考資料
http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 尤其後面的討論
http://www.iana.org/assignments/link-relations/link-relations.xml