今天講點干貨,來點思想上的碰撞,代碼風(fēng)格的討論,我們來探討一下怎么合理使用異常,來使代碼流程簡潔易懂,使寫代碼寫得舒服。
異常類型
先說一下java的異常類型有:

檢查性異常(checked exceptions) 是必須在在方法的
throws子句中聲明的異常。它們擴(kuò)展了異常,旨在成為一種“在你面前”的異常類型。JAVA希望你能夠處理它們,因為它們以某種方式依賴于程序之外的外部因素。檢查的異常表示在正常系統(tǒng)操作期間可能發(fā)生的預(yù)期問題。 當(dāng)你嘗試通過網(wǎng)絡(luò)或文件系統(tǒng)使用外部系統(tǒng)時,通常會發(fā)生這些異常。 大多數(shù)情況下,對檢查性異常的正確響應(yīng)應(yīng)該是稍后重試,或者提示用戶修改其輸入。
非檢查性異常(unchecked Exceptions) 是不需要在throws子句中聲明的異常。 由于程序錯誤,JVM并不會強(qiáng)制你處理它們,因為它們大多數(shù)是在運(yùn)行時生成的。 它們擴(kuò)展了RuntimeException。 最常見的例子是NullPointerException[相當(dāng)可怕..是不是?]。 未經(jīng)檢查的異常可能不應(yīng)該重試,正確的操作通常應(yīng)該是什么都不做,并讓它從你的方法和執(zhí)行堆棧中出來。 在高層次的執(zhí)行中,應(yīng)該記錄這種類型的異常。
錯誤(errors) 是嚴(yán)重的運(yùn)行時系統(tǒng)問題,幾乎肯定無法恢復(fù)。 例如OutOfMemoryError,LinkageError和StackOverflowError, 它們通常會讓程序崩潰或程序的一部分。 只有良好的日志練習(xí)才能幫助你確定錯誤的確切原因。
補(bǔ)充完以上的概念后,我們再來探討一下這三種異常怎么用,什么時候用,該用哪種。Error是系統(tǒng)異常就不說了,我們直接講Exception和RuntimeException。
怎么用
直接通過場景對比來剖析異常什么時候用比較好,該用哪種類型異常。
場景分析
用戶在系統(tǒng)中輸入證件號查詢用戶信息,如果成功返回{"code":200, "result":{用戶信息對象}},驗證不通過則返回{"code":-100, "error":"系統(tǒng)驗證錯誤"},那么這里的驗證不通過,怎么處理才比較好。
方案一
return的方式,定義通用結(jié)果對象CommonResult:
public class CommonResult {
private int code;
private UseInfo result;
private String error;
//===== 忽略getset ====
}
如果只是單層方法里面的驗證邏輯,可能代碼是這樣的:
public CommonResult handle(String phone) {
// 驗證邏輯1
// 驗證邏輯2
if ("123".equals(phone)) {
return new CommonResult(-100, null, "驗證邏輯2不通過");
}
// 驗證邏輯3
//用戶信息填充
UseInfo userinfo = new UseInfo();
return new CommonResult(200, userinfo, null);
}
但是如果是兩層嵌套方法,那么我就需要將嵌在里面的方法也返回這種通用的CommonResult對象,不然只通過return的方法無法處理里面方法的校驗不通過情況,最后的結(jié)果是,所有方法都必須返回通用結(jié)果對象。
造成這種現(xiàn)象的本質(zhì)原因是,我們將正確的結(jié)果返回和錯誤的結(jié)果返回混在一起了,就會造成極其混亂的結(jié)果返回邏輯。我覺得正確的、舒服的代碼結(jié)構(gòu)方式,應(yīng)該是我只需要舒服、專注地處理正確的業(yè)務(wù)流程就好,不正確的流程都throw異常出去。
(曾經(jīng)聽過一些老一輩的程序員覺得拋異常會影響性能,所以會采用上面那種處理方式,我覺得完全是多慮和沒有必要的,先不討論影響性能多少,以現(xiàn)在的機(jī)器配置,這點消耗是完全可以忽略的)
以上,我覺得就該用異常來處理了,那么第二個問題就是,我是該拋Exception還是RuntimeException呢?
方案二
拋出Exception的方案,先定義通用CommonException如下:
public class CommonException extends Exception {
private int code;
private String error;
public CommonException(int code, String error) {
this.code = code;
this.error = error;
}
//==== 其他構(gòu)造方法 ====
}
最后方法里面就變成如下:
public UserInfo handle2(String phone) throws CommonException {
// 驗證邏輯1
// 驗證邏輯2
if ("123".equals(phone)) {
throw new CommonException(-100, "驗證邏輯2不通過");
}
// 驗證邏輯3
//用戶信息填充
UserInfo userinfo = new UserInfo();
return userinfo;
}
變成這種結(jié)構(gòu)之后,則需要在controller或者全局過濾器里面,將結(jié)果和異常捕獲,在封裝成CommonResult序列化返回,整個流程暫時也是很舒服的。
但是如果有多層嵌套的方法,則需要每個方法都拋出這種通用異常CommonException,但是這種異常并不是由調(diào)用方來處理的,而是最外層的controller或者全局過濾器統(tǒng)一處理的,這個是不合理也不舒服的地方。
造成這種現(xiàn)象的本質(zhì)原因是,沒有搞清楚Exception和RuntimeException的分別使用場景,我覺得Exception的設(shè)計考慮是,需要調(diào)用方處理這種異常情況,并且需要調(diào)用方針對不同的Exception做不同的處理,影響的是業(yè)務(wù)流轉(zhuǎn)方向,不會中斷業(yè)務(wù)流程。
RuntimeException的場景則是,某個校驗不通過,整個流程其實都跑不下去的,需要中斷整個流程。
方案三
定義通用RuntimeException如下:
public class CommonRuntimeException extends RuntimeException {
private int code;
private String error;
public CommonRuntimeException(int code, String error) {
this.code = code;
this.error = error;
}
//==== 其他構(gòu)造方法 ====
}
方法邏輯如下:
public UserInfo handle3(String phone) {
// 驗證邏輯1
// 驗證邏輯2
if ("123".equals(phone)) {
throw new CommonRuntimeException(-100, "驗證邏輯2不通過");
}
// 驗證邏輯3
//用戶信息填充
UserInfo userinfo = new UserInfo();
return userinfo;
}
這種的話,無論幾層的方法嵌套,我都不需要顯式的拋出異常,我只需要舒服地專注于寫正常流程的代碼即可,異常的、校驗不通過的,并且流程跑不下去的情況,我只需拋CommonRuntimeException出來即可,其他就不需要管了。
補(bǔ)充個全局過濾器的實現(xiàn):
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest hreq = (HttpServletRequest) request;
HttpServletResponse hres = (HttpServletResponse) response;
try {
before(hreq, hres);
chain.doFilter(request, response);
} catch (Exception e) {
error(hreq, hres, e);
} finally {
after(hreq, hres);
}
}
public void error(HttpServletRequest hreq, HttpServletResponse hres, Exception e) throws IOException {
if (e instanceof CommonRuntimeException) {
CommonRuntimeException ex = (CommonRuntimeException) e;
ResponseUtil.fail(hreq, hres, ex.getCode(), ex.getError());
return;
} else if (e.getCause() instanceof CommonRuntimeException) {
//處理spring框架包了一層的情況
CommonRuntimeException ex = (CommonRuntimeException) e.getCause();
ResponseUtil.fail(hreq, hres, ex.getCode(), ex.getError());
return;
}
}
//其他非通用異常的默認(rèn)處理
ResponseUtil.response(hreq, hres, getCode(defErr), getError(defErr));
return;
}
至此,使用方案三的話,在處理業(yè)務(wù)邏輯時,我就完全不需要處理異常的流轉(zhuǎn)邏輯了,專注于正常的業(yè)務(wù)流轉(zhuǎn)邏輯開發(fā)。
當(dāng)然,如果有一些異常的場景,是影響到整個業(yè)務(wù)的流轉(zhuǎn)方向的,那么自定義Exception并顯示地提示調(diào)用方處理,還是很有必要的,所以并不能濫用CommonRuntimeException。
補(bǔ)充,每次拋CommonRuntimeException之前,必須打印上下文的日志,本文為了講述清晰,代碼例子都沒添加日志。
btw,如果文章有幫助,請點贊轉(zhuǎn)發(fā)。
更多技術(shù)文章可查看:https://www.edjdhbb.com/categories/