若你的項目中已經(jīng)在使用spring,然后你又需要提供rest接口,那么springmvc是一個不錯的選擇。
不過,由于rest并不包含用戶界面(rest更傾向于用純文本表達(dá)),而springmvc則老是想著“生成用戶界面、生成用戶界面”,所以,想要用springmvc來更restful地表述錯誤或問題,并沒有那么容易。
那么我們應(yīng)該如何用springmvc產(chǎn)出更符合restful的錯誤信息呢?
restful異常處理設(shè)計
若有異常發(fā)生,rest建議我們通過設(shè)置HTTP狀態(tài)碼的方式大體地區(qū)分失敗的原因。大多數(shù)rest API設(shè)計者認(rèn)為,盡可能地重用HTTP規(guī)范定義的狀態(tài)碼是最好的,因為許許多多的http客戶端都能理解這些錯誤情況的絕大多數(shù),并且,“重用”這件事鼓勵行為的一致性,這對開發(fā)有好處。
然而,原生HTTP規(guī)范只有24種狀態(tài)碼用來描述錯誤情況:其中18種4xx狀態(tài)碼描述客戶端錯誤,6種5xx狀態(tài)碼描述服務(wù)端錯誤(也有其他規(guī)范定義了更多的狀態(tài)碼,比如WebDav,但它們流傳不廣)。這就有一個問題:這24種狀態(tài)碼太過泛化——它們有可能并不能描述一個特定問題的所有細(xì)節(jié)。
最好給你的restAPI使用者們盡量多的信息,以便他們診斷和修復(fù)問題。你的restAPI越容易使用,他們就越可能用你的服務(wù)(譯注:這年頭,連要服務(wù)別人都競爭激烈)。
rest錯誤情況的表述
既然狀態(tài)碼很可能不夠用,那么當(dāng)最終用戶遭遇錯誤情況時,我們可以提供什么其他東西來協(xié)助他們呢?顯然可以提供可讀的錯誤信息,方便開發(fā)者查看。但我們其實(shí)還可以增加更多信息,以提供一個又直觀又很有幫助的錯誤描述。
Apigee公司(Apigee.com)有人在博客上整理了一篇值得一看的關(guān)于如何表述restful錯誤情況文章(http://blog.apigee.com/detail/restful_api_design_what_about_errors),還有一些很好的視頻(http://www.youtube.com/watch?v=QpAhXa12xvU)。我們要做類似的事情。
下面的例子是我覺得比較好的rest錯誤情況表述(例子是json格式的。xml的類似):
{
"status": 404,
"code": 40483,
"message": "Oops! It looks like that file does not exist.",
"developerMessage": "File resource for path /uploads/foobar.txt does not exist. Please wait 10 minutes until the upload batch completes before checking again.",
"moreInfo": "http://www.mycompany.com/errors/40483"
}
后面我將詳述這些屬性。
狀態(tài)/status
“狀態(tài)”屬性是整型的,而且跟http狀態(tài)碼值相同。這是一個便捷通道:把狀態(tài)碼在響應(yīng)體里也放一份,那么所有rest客戶端處理錯誤時,只需要看響應(yīng)體這一個地方就可以完整地理解錯誤:錯誤自表述了,不需要去檢查響應(yīng)頭或其他地方才能明白了。
探討
首先說思想,響應(yīng)對象也是個對象,該用就用什么屬性就用什么屬性,該用響應(yīng)頭就用響應(yīng)頭,沒必要把響應(yīng)頭視為(比響應(yīng)體)低人一等。甚至理論上嚴(yán)格來說,響應(yīng)體放的是uri指向的資源,響應(yīng)頭放的是描述資源和本次請求--響應(yīng)的元信息,而錯誤情況的描述文本恰好屬于“本次請求--響應(yīng)的元信息”或“資源的元數(shù)據(jù)”,所以把錯誤情況放在響應(yīng)體里是錯誤的,應(yīng)該放在響應(yīng)頭里。
再看方案,其實(shí)并不能解決問題。復(fù)制一個狀態(tài)碼放在響應(yīng)體里不是不可以,但是“讓客戶端不需要去響應(yīng)頭里看狀態(tài)碼”是無法達(dá)成的。因為有些錯誤很有可能不是服務(wù)端業(yè)務(wù)代碼產(chǎn)生的,很有可能是諸如nginx、tomcat、springmvc、struts之類的框架、中間件產(chǎn)生的,甚至還有可能是在服務(wù)端-客戶端之間網(wǎng)絡(luò)的中間節(jié)點(diǎn)(比如dns、代理節(jié)點(diǎn)、網(wǎng)關(guān)blabla)就掛了,服務(wù)端根本就沒收到請求。服務(wù)端無法保證這些節(jié)點(diǎn)發(fā)生錯誤也會遵照作者上述的做法,所以客戶端就無論如何都得考慮處理這些情況,而處理這些情況就必須從響應(yīng)頭里獲取狀態(tài)碼。而既然都已經(jīng)通過響應(yīng)頭獲取狀態(tài)碼了,又何必再去響應(yīng)體里獲取一遍?多此一舉。
我認(rèn)為在使用http客戶端時,處理響應(yīng)的流程如下:
要捕獲住所使用的http客戶端組件聲明的所有異常。此時請求可能都還沒有發(fā)出去,問題的原因一般是程序員使用有誤、參數(shù)有誤、此http客戶端組件有bug、網(wǎng)絡(luò)問題。遇到這種情況,應(yīng)將組件特有異常轉(zhuǎn)譯成自定義的異常拋出。
調(diào)用http客戶端組件發(fā)起請求,得到響應(yīng)對象,通常先檢查是否為null。若為null,原因一般是此http客戶端組件設(shè)計得不好,沒有很好地定義自己的行為結(jié)果,令使用者無法得知當(dāng)前狀態(tài)。遇到這種情況只能當(dāng)“未知異常”拋出(好的http客戶端不會來到這里,要么觸發(fā)1要么觸發(fā)3)。
若http客戶端組件的響應(yīng)對象自定義了類似于“查看本次請求--響應(yīng)狀態(tài)”這樣的接口,可以考慮調(diào)用它來判斷。這時要具體情況具體分析,該重試重試,該拋異常拋異常。
查看響應(yīng)對象的http狀態(tài)碼值。對于那些有可能是中間結(jié)點(diǎn)返回的錯誤響應(yīng)(常見的包括401、403、404、405、406、408、409、429、500、502、503、504)要特別注意,它們的響應(yīng)體未必符合http接口文檔里聲明的格式,所以需要檢查響應(yīng)頭(比如檢查Content-Type頭是否符合期望),然后才是嘗試解析。嘗試解析時也需要捕獲住所使用解析組件的所有異常(比如用jackson解析json響應(yīng)體,需要捕獲所有可能會被拋出來的jackson的異常)。
中間節(jié)點(diǎn)不會使用的那些狀態(tài)碼,是服務(wù)端主動觸發(fā)的,就直接按http接口文檔約定的異常情況處理即可。
解析得到符合http接口約定的異常響應(yīng)體后,就可以開展業(yè)務(wù)處理流程了。這時也需要注意,更嚴(yán)謹(jǐn)一些的話,也需要捕獲住一些特定的異常,比如空指針、NumberFormatException等。這么做是為了避免接口做了不兼容修改而接口文檔沒有及時更新導(dǎo)致的錯誤。
錯誤碼/code
一個“錯誤碼”屬性通常用來表示錯誤場景下的一個特定信息。
由于通用的HTTP錯誤碼過少導(dǎo)致了一定的局限性,所以推薦使用自定義錯誤碼,可以用來表達(dá)更多更豐富的特定的失敗原因。再次強(qiáng)調(diào),API客戶端獲得的信息越多越好。
在上面的例子中,錯誤碼屬性的值是40483。通用的那個“狀態(tài)碼”(404)表明沒找到該資源,然后有一個應(yīng)用特有的錯誤碼40483,來表明該資源不光是沒找到,而且還表明了是因為尚未被上傳到服務(wù)器。
探討
作者的意思應(yīng)該是可以從“存在性”維度來區(qū)別諸如“未存在過”、“曾經(jīng)擁有現(xiàn)已搬走”、“曾經(jīng)擁有現(xiàn)不知所蹤”、“暫時不在稍后回來”等不同的細(xì)分情況。若是從業(yè)務(wù)維度來細(xì)分錯誤碼,我認(rèn)為是可行的,但這里是從一個非業(yè)務(wù)維度細(xì)分,值得商榷(作者至少應(yīng)該拿出更好的例子來)。
由于rest/http是按無狀態(tài)設(shè)計的,這里的“無狀態(tài)”是指不考慮歷史取值、值的變化情況,對“曾經(jīng)”和“未曾”一視同仁,更看重結(jié)果和未來。
所以在“存在性”維度,以結(jié)果和未來導(dǎo)向的細(xì)分情況如下:
1,資源不會再出現(xiàn)在當(dāng)前位置(uri)
1.1,資源當(dāng)前位置已知:即已知的永久遷移。使用301狀態(tài)碼。
1.2,資源當(dāng)前位置未知:類似于死亡。使用410狀態(tài)碼。
2,資源可能再出現(xiàn)在當(dāng)前位置(uri)
2.1,資源當(dāng)前位置已知:即已知的臨時遷移。使用302狀態(tài)碼。
2.2,資源當(dāng)前位置未知:由于無狀態(tài)不考慮歷史變遷因素,兩種子情況一視同仁,都使用404狀態(tài)碼。
2.2.1,資源曾經(jīng)存在:即失蹤。這里僅羅列一下細(xì)分情況。
2.2.2,資源未曾出現(xiàn)過:類似于未出生。這里僅羅列一下細(xì)分情況。
這里“上傳文件”的例子看起來有點(diǎn)太刻意了,但這里關(guān)鍵是說你的API使用自定義的錯誤碼,可以表達(dá)更豐富的錯誤信息。
提示:若你對某一特殊錯誤沒有自定義錯誤碼,那么可以讓錯誤碼屬性的值=狀態(tài)碼的值。這樣確保錯誤碼永遠(yuǎn)會有值,客戶端不需要檢查它是否為null。這對API使用者更容易和優(yōu)雅,能提高接受度。
友好提示/message
“友好提示”屬性是人類可讀的錯誤信息,可以直接顯示給應(yīng)用的最終用戶(非開發(fā)人員)看。所以它應(yīng)該是友好而且容易理解的,是描述錯誤為什么發(fā)生的簡明摘要。它不應(yīng)帶有技術(shù)信息,技術(shù)信息應(yīng)放在“調(diào)試信息”屬性(見下文)。
這樣做有什么好處?
若你的restAPI使用者希望把消息展示給最終用戶,他們就可以這么做了。這樣他們就可以很快而且不用做太多工作地寫出用戶界面來支持他們自己的最終用戶。讓API使用者在使用時節(jié)省更多時間的事情,做得越多越好。
調(diào)試信息/developerMessage
“調(diào)試信息”屬性可以用來放與技術(shù)有關(guān)的信息,對調(diào)用你restAPI的開發(fā)者很有用。你可以把異常信息、堆?;蛉魏文阌X得對使用者有幫助的信息放在里面。
詳情/moreInfo
“詳情”屬性指定一個url,可以展示給看到錯誤信息的人,他們可以點(diǎn)擊或把它復(fù)制粘貼到瀏覽器里。url指向的目標(biāo)網(wǎng)頁應(yīng)該有完整的錯誤詳情以及解決方案,幫助他們解決問題。
這可能是最重要的屬性,因為你可以在目標(biāo)網(wǎng)頁上更好地提供信息。你可以提供指向支持部門的鏈接,可以做一個“在線求助”對話框,或你覺得有幫助的隨便什么東西。展現(xiàn)一下程序猿滿滿的愛心吧,他們就會繼續(xù)用你的API了。