異常實踐
前言
本文是我在項目中設(shè)計和處理異常的一些實踐,主要是圍繞著常見的web項目,歡迎大家指正。
本文分為兩個個部分
- 異常設(shè)計
- 異常處理
異常設(shè)計
通常考慮異常設(shè)計時大致分為三個部分
- 接口層
- 業(yè)務(wù)層
- 類庫
接口層就是我們通常說的 controller 層,以及提供 rpc 服務(wù)的接口層。
業(yè)務(wù)層就是主要的業(yè)務(wù)代碼模塊,主要是 service 層。
類庫主要是指一些公共模塊,可以在各個項目中使用的,比如 json,分布式鎖等。
業(yè)務(wù)層
在業(yè)務(wù)層,我們主要是要設(shè)計業(yè)務(wù)異常。什么是業(yè)務(wù)異常?業(yè)務(wù)異常就是我們能夠人為的判斷出業(yè)務(wù)邏輯走到某一個位置是不對的。比如,我們要根據(jù)一個 uid 來修改一個 user 的 name,但我們發(fā)現(xiàn)并沒有這個 uid 對應(yīng)的 user 數(shù)據(jù),這時候就應(yīng)該拋出一個業(yè)務(wù)異常。在發(fā)生業(yè)務(wù)異常時,要避免拋出 npe,RuntimeException 等其他內(nèi)置異常,以方便上層來分辨到底是業(yè)務(wù)錯誤還是程序 bug。
我一般會設(shè)計一個業(yè)務(wù)異常的基類 ServiceException
,將所有業(yè)務(wù)異常以這種類型來拋出,并帶有必要的 message。
為了把用戶可讀的消息和開發(fā)人員可讀的消息區(qū)別開,ServiceException
還需要實現(xiàn)一個接口 UserMessage
,并實現(xiàn)其中方法 getUserMessage()
來返回用戶可讀的信息,而 getMessage() 可以攜帶更詳細(xì)的開發(fā)人員可讀的錯誤信息
設(shè)計一個 ServiceErrorException
,繼承 ServiceException
。ServiceErrorException
的主要目的是為了表明這個異常的錯誤程度高,需要記錄 error。
以上就定型了業(yè)務(wù)異常的基本結(jié)構(gòu),上面一些特殊設(shè)計會在異常處理中用到,我們后面來說,再做前后對照。我們可以根據(jù)需要來實現(xiàn)若干子類來表示業(yè)務(wù)層中不同模塊的錯誤。
接口層
對于接口層,特別是rpc調(diào)用,比如我們的dubbo調(diào)用,需要把api的jar包放在調(diào)用方。我們需要把異常類給包括進(jìn)去,但調(diào)用方不能也不應(yīng)該拿到我們業(yè)務(wù)層的 ServiceException
,所以需要在接口層定義新的業(yè)務(wù)異常類型,比如,就叫ApiServiceException
,放在api的jar包里給調(diào)用方。
接口實現(xiàn)需要把業(yè)務(wù)層的ServiceException
給catch到,重新封裝為ApiServiceException
拋出。
這樣,調(diào)用方在判斷調(diào)用時發(fā)生的異常時,有三種可能:
- rpc框架異常。比如又dubbo框架拋出的異常,這一版兩種可能:1. 網(wǎng)絡(luò)異常,我們需要重試;2. 調(diào)用未能達(dá)成,這種一般是接口沒有匹配上,在開發(fā)測試時都可以發(fā)現(xiàn)的錯誤,改掉即可。所以,當(dāng)發(fā)生rpc框架異常時,調(diào)用方的策略就應(yīng)該是重試。
- ApiServiceException。這表示被調(diào)用方出現(xiàn)了業(yè)務(wù)異常,調(diào)用方也需要作為業(yè)務(wù)異常來處理。
- 其他異常。這表示被調(diào)用方的程序有bug報出了異常透傳給了調(diào)用方,這是調(diào)用方應(yīng)及時聯(lián)系接口實現(xiàn)方來修補bug。
以上,就能夠分類準(zhǔn)確應(yīng)對rpc過程中的異常情況。
http方式的接口層也可以這么做,不過由于api并不對外,所以也可以完全由自身來處理異常類型,詳見異常處理部分。
類庫
作為類庫,因為通常沒有業(yè)務(wù)意義,所以在發(fā)生邏輯上的異常時,根本不可能知道需要怎么處理,這就需要直接向上拋出,到交給業(yè)務(wù)層處理。
類庫需要將自身的邏輯上的異常,同一封裝。比如,處理 Json 的類庫,異常最終拋出時,都被封裝成為JsonParseException
或 JsonSerializeExcption
。
這樣調(diào)用方使用類庫時,異常會有兩種:
- 類庫封裝的自定義異常。這種是調(diào)用時出現(xiàn)的邏輯錯誤,調(diào)用方以業(yè)務(wù)異常來處理。
- 其他異常。可以認(rèn)為是類庫bug。
異常處理
有了以上的異常設(shè)計,那么處理時就可以按照以下流程。
以 http 請求的 ExceptionHandler 為例,所有 http 請求異常都會放在這里處理,過程:
- ex 異常傳入。
- 裝飾 ex 異常,
ex = new WebApiException(...,ex)
,其包含有message
,isLogError
屬性- 如果 ex 是非
ServiceException
,那么message = “系統(tǒng)內(nèi)部錯誤”
,isLogError = true
。 - 如果 ex 是
ServiceException
,那么message = ex.getUserMessage()
;更進(jìn)一步,如果是ServiceErrorException
,那么isLogError = true
,否則isLogError = false
。
- 如果 ex 是非
- 如果
ex.isLogError == true
,記錄 error log,否則,記錄 warn log。 - 判斷 http 請求是頁面請求,還是ajax請求。
- 如果是頁面請求。500轉(zhuǎn)錯誤頁,顯示
ex.getMessage()
,如果是debug環(huán)境或者是請求帶有debug參數(shù),也把錯誤堆棧輸出在頁面上。 - 如果是 ajax 請求。返回表示錯誤的 json 消息,同樣,如果是debug環(huán)境或者是請求帶有debug參數(shù),消息中帶上堆棧信息。
- 如果是頁面請求。500轉(zhuǎn)錯誤頁,顯示
以上,就是一個簡單而有效的異常處理機制。
最后
以上是我的個人實踐經(jīng)驗總結(jié),請各位批評指正,歡迎討論。
歡迎加入群 661035226,gradle,spring,activiti 交流