大家好,我是大明哥,
個人網站:https://www.cmsblogs.com/
前言
我敢說對于很多小伙伴來說,他們以為在 Java 中例外就是 try...catch,稍微有點兒意識的還會用下 throw new Exception,真的有這么簡單嗎?請寬恕小編直言,你對 Java 例外一知半解,以下是小編對 Java 例外的理解,如有錯誤之處,請原諒,資質有限,
引出例外
先問一個問題,在 Java 中,你是贊同使用回傳碼還是例外來規范錯誤呢?
我先來說說使用回傳碼的情況,比如登錄邏輯,我們分為如下幾種情況:
- 0:登錄成功
- 1:用戶名錯誤,沒有該用戶
- 2:用戶密碼輸入錯誤
- 3:第一次登錄,重置密碼
- 4:密碼連續錯 5 次,凍結賬戶
使用回傳碼一般都是如下處理:
int code = userService.login(userName,password);
if (code == 0) {
return "登錄成功";
}else if (code == 1) {
return "用戶名錯誤,請重新輸入";
}else if (code == 2) {
return "密碼輸入錯誤,請重新輸入";
} else if (code == 3) {
return "您是第一次登錄,請重置密碼";
} else if (code == 4) {
return "您密碼已連續輸錯 5 次,凍結一天";
}
當然,您可能有更加優雅的處理方式,比如把 code 直接回傳給前端,讓前端來根據回傳碼來展示不同的例外,如:
return userService.login(userName,password);
這種后端處理方式確實是比較優雅,但是前端呢?如果你后端再增加 5、6 種回傳碼,你覺得你前端的同事不會打死你嗎?
那怎么解決呢?
- 首先約定 code 和 message,例如:0000,表示成功,0001,表示第一次登錄,跳轉到重置頁面,-0001,表示登錄失敗,然后 message 描述回傳資訊,
- 后端統一例外處理,
可能有小伙伴說,這種是前后端的互動,那純后端模式呢?一樣應該使用例外,
如果你使用回傳碼來規范介面,如:
- 0:成功
- 1:例外 1
- 2:例外 2
- 3:例外 3
那么別人在呼叫的寫的方法時,勢必需要區分這幾種情況,比如 code = 0 怎么處理,code = 1 怎么處理,…,剛剛開始他確實也是這樣處理的,而且處理地非常好,你們程式運行也非常好,但是某天,你增加了一個 code = 5的,然后你恰巧忘記告訴他了,嗯,上線后你就等著挨批吧,
其實這種單回傳值還好處理,如果是多回傳值,請問你怎么處理?如果回傳的結果是一個 List 集合呢?你是不是得要構造一個 Map 或者物件,別說你回傳 null 哈,否則我要敲你腦袋了,所以這種通過回傳碼的處理方式還是有一些問題的:
- 程式員不小心忘記回傳值的檢查,從而造成 BUG
- 方法介面變得非常不純潔了,正常值和錯誤值混淆在一起,導致語意問題
大明哥,在真實專案里面看到過這樣處理的邏輯,當時我一看,徑訓瞬間就緊了,驚為天人啊,如下
public class ServiceA {
public String method1() {
doSomething1();
doSomething2();
if (a) {
return "false@xxxx";
} else if (b) {
return "false@zzzzz";
} else if (c) {
return "false@ccccc";
} else {
return "true@vvvvv";
}
}
}
呼叫方:
public class ServiceB {
public void method2() {
String result = method1();
String[] results = result.split("@");
if ("false".equals(results[0])) {
if (results[1].contains("xxxx")) {
doSomething1();
}
if (results[1].contains("zzzzz")) {
doSomething2();
}
if (results[1].contains("ccccc")) {
doSomething3();
}
}
}
}
我就問你怕不怕?
在我看來通過回傳碼來判斷程式的運行結果是否正確,真的是一件吃力不討好的事情,Java 提供了 try...catch 這么優雅的處理方式,他難道不香嗎?
可能有小伙會問,你寫的方法如果拋出了例外,沒有告訴其他同事,是不是也會導致代碼例外?我丟,如果這個例外需要你同事欄位外處理,你不會拋出 checked Exception 么?
可能又有小伙伴說,拋出例外會降低程式性能,這個確實,但是有考慮過一個問題沒有,我們系統大部分情況下都是正常運行,只會偶爾拋出例外,如果你寫的程式一天到晚都在拋出例外,你是不是需要思考下,到底是你的問題還是程式的問題?同時,丟失掉這么一點點性能換來的是高可讀、易維護、優雅的代碼難道不值得嗎?
最佳實踐
既然 Exception 有諸多好處,那么我們應該怎么用好他呢?
我個人認為,程式中的錯誤可以分為三大類:
- 系統錯誤,這類錯誤是程式運行環境的問題,一般我們都無法避免,對于這類錯誤,有些我們是可以處理的,比如請求網路例外,這個我們可以重試幾次,而有些是我們無法處理的,比如記憶體耗盡 OOM 了、堆疊溢位等等,這種我們就只能停止運行,甚至退出整個程式,
- 程式錯誤,這類錯誤一般都是我們程式的 bug,比如空指標,檔案未創建,邏輯計算錯誤,對于這種錯誤,我們必須要記錄下來,而且最好是觸發監控系統告警,
- 用戶錯誤,比如用戶輸入非法引數,重復請求,一般這類的錯誤屬于用戶應用層錯誤,對于這類錯誤,我們只需要提示用戶即可,沒有必要記錄日志,但是我們可以做一些必要的統計,比如某個用戶頻繁輸入非法引數,不斷進行錯誤請求,我們可以將這些用戶納入黑名單等,這樣有利于我們改善系統和偵測是否有惡意的用戶請求,
對于這三種錯誤,我們需要進行區分,不同的例外分類有不同的處理級別,
- 系統錯誤:盡可能的預見例外,在能處理的地方需要進行處理,不能處理的往外拋
- 程式錯誤:我們需要盡可能杜絕,記錄每一個程式處理例外,進行必要的告警,
- 用戶錯誤:引數校驗必須,嚴謹將錯誤引數帶入系統,我們無法避免,只能適當統計和偵測,
同時,我不建議系統中定義太多自定義 Exception,有些小伙伴自定義 Exception 好像著魔了一樣,在系統中定義一大把 Exception,如 UserNotFoundException、UserPasswordErrorException等等,目前我負責的系統中就只有如下幾個例外:
BusinessException:業務例外,繼承 RuntimeExceptionNotFountException:業務例外,繼承 RuntimeExceptionParamValidateException:業務例外,繼承 RuntimeExceptionSystemException:系統例外,繼承 Exception
1、2、3 不需要顯示處理,4 一定需要處理,然后再配合兩個列舉:
BusinessErrorCodeEnumSystemErrorCodeEnum
同時 BusinessException,建構式中有一個 isPrintLog 引數,用來判斷是否需要列印 Error 日志,對于一些不需要我們關注的 Exception,統一傳 false 即可,監控系統會每天將我們系統中所有的 Exception 通過郵件的方式發送給我們團隊的每一個人,我每天都會去關注前一天系統中的 Error 日志,根據實際情況去掉一些不關注的 Exception,而且監控系統會實時將系統的 Error 、Exception 通過郵件和企業微信的方式告知相關的開發人員,這樣我們就能及時發現系統這個的錯誤,及時回應,縮小影響范圍,
下面是耗子叔的例外最佳實踐,總結的非常好:
- 統一分類的錯誤字典,無論你是使用錯誤碼還是例外捕捉,都需要認真并統一地做好錯誤的分類,最好是在一個地方定義相關的錯誤,比如,HTTP 的 4XX 表示客戶端有問題,5XX 則表示服務端有問題,也就是說,你要建立一個錯誤字典,
- 同類錯誤的定義最好是可以擴展的,這一點非常重要,而對于這一點,通過面向物件的繼承或是像 Go 語言那樣的介面多型可以很好地做到,這樣可以方便地重用已有的代碼,
- 定義錯誤的嚴重程度,比如,Fatal 表示重大錯誤,Error 表示資源或需求得不到滿足,Warning 表示并不一定是個錯誤但還是需要引起注意,Info 表示不是錯誤只是一個資訊,Debug 表示這是給內部開發人員用于除錯程式的,
- 錯誤日志的輸出最好使用錯誤碼,而不是錯誤資訊,列印錯誤日志的時候,應該使用統一的格式,但最好不要用錯誤資訊,而應使用相應的錯誤碼,錯誤碼不一定是數字,也可以是一個能從錯誤字典里找到的一個唯一的可以讓人讀懂的關鍵字,這樣,會非常有利于日志分析軟體進行自動化監控,而不是要從錯誤資訊中做語意分析,比如:HTTP 的日志中就會有 HTTP 的回傳碼,如:404,但我更推薦使用像PageNotFound這樣的標識,這樣人和機器都很容易處理,
- 忽略錯誤最好有日志,不然會給維護帶來很大的麻煩,
- 對于同一個地方不停的報錯,最好不要都打到日志里,不然這樣會導致其它日志被淹沒了,也會導致日志檔案太大,最好的實踐是,打出一個錯誤以及出現的次數,不要用錯誤處理邏輯來處理業務邏輯,也就是說,不要使用例外捕捉這樣的方式來處理業務邏輯,而是應該用條件判斷,如果一個邏輯控制可以用 if - else 清楚地表達,那就不建議使用例外方式處理,例外捕捉是用來處理不期望發生的事情,而錯誤碼則用來處理可能會發生的事,
- 對于同類的錯誤處理,用一樣的模式,比如,對于null物件的錯誤,要么都用回傳 null,加上條件檢查的模式,要么都用拋 NullPointerException 的方式處理,不要混用,這樣有助于代碼規范,
- 盡可能在錯誤發生的地方處理錯誤,因為這樣會讓呼叫者變得更簡單,向上盡可能地回傳原始的錯誤,如果一定要把錯誤回傳到更高層去處理,那么,應該回傳原始的錯誤,而不是重新發明一個錯誤,
- 處理錯誤時,總是要清理已分配的資源,這點非常關鍵,使用 RAII 技術,或是 try-catch-finally,或是 Go 的 defer 都可以容易地做到,
- 不推薦在回圈體里處理錯誤,這里說的是 try-catch,絕大多數的情況你不需要這樣做,
- 最好把整個回圈體外放在 try 陳述句塊內,而在外面做 catch,不要把大量的代碼都放在一個 try 陳述句塊內,一個 try 陳述句塊內的陳述句應該是完成一個簡單單一的事情,
- 為你的錯誤定義提供清楚的檔案以及每種錯誤的代碼示例,如果你是做 RESTful API 方面的,使用 Swagger 會幫你很容易搞定這個事,
- 對于異步的方式,推薦使用 Promise 模式處理錯誤,對于這一點,JavaScript 中有很好的實踐,
- 對于分布式的系統,推薦使用 APM 相關的軟體,尤其是使用 Zipkin 這樣的服務呼叫跟蹤的分析來關聯錯誤,
最后用一句總結今天的文章:例外應該出現在它應該出現的地方,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/292944.html
標籤:java
