針對(duì)異常的一般操作
轉(zhuǎn)換,即轉(zhuǎn)換新的異常拋出。對(duì)于新拋出的異常,最好具有特定的分類(lèi)和明確的異常消息,而不是隨便拋一個(gè)無(wú)關(guān)或沒(méi)有任何信息的異常,并最好通過(guò) cause 關(guān)聯(lián)老異常。
重試,即重試之前的操作。比如遠(yuǎn)程調(diào)用服務(wù)端過(guò)載超時(shí)的情況,盲目重試會(huì)讓問(wèn)題更嚴(yán)重,需要考慮當(dāng)前情況是否適合重試。
恢復(fù),即嘗試進(jìn)行降級(jí)處理,或使用默認(rèn)值來(lái)替代原始數(shù)據(jù)
一些錯(cuò)誤示例
一:不應(yīng)該用 AOP 對(duì)所有方法進(jìn)行統(tǒng)一異常處理,異常要么不捕獲不處理,要么根據(jù)不同的業(yè)務(wù)邏輯、不同的異常類(lèi)型進(jìn)行精細(xì)化、針對(duì)性處理;
如
IllegalArgumentException: 入?yún)㈠e(cuò)誤,比如參數(shù)類(lèi)型int輸入string。
IllegalStateException: 狀態(tài)錯(cuò)誤,比如訂單已經(jīng)支付完成,二次請(qǐng)求支付接口。
UnsupportedOperationException: 不支持操作錯(cuò)誤,比如對(duì)一筆不能退款的訂單退款。
其他異常
SecurityException: 權(quán)限錯(cuò)誤,比如未登陸用戶(hù)調(diào)用修改用戶(hù)信息接口。

每層架構(gòu)的工作性質(zhì)不同,且從業(yè)務(wù)性質(zhì)上異常可能分為業(yè)務(wù)異常和系統(tǒng)異常兩大類(lèi),這就決定了很難進(jìn)行統(tǒng)一的異常處理。我們從底向上看一下三層架構(gòu):
Repository 層出現(xiàn)異?;蛟S可以忽略,或許可以降級(jí),或許需要轉(zhuǎn)化為一個(gè)友好的異常。如果一律捕獲異常僅記錄日志,很可能業(yè)務(wù)邏輯已經(jīng)出錯(cuò),而用戶(hù)和程序本身完全感知不到。
Service 層往往涉及數(shù)據(jù)庫(kù)事務(wù),出現(xiàn)異常同樣不適合捕獲,否則事務(wù)無(wú)法自動(dòng)回滾。此外 Service 層涉及業(yè)務(wù)邏輯,有些業(yè)務(wù)邏輯執(zhí)行中遇到業(yè)務(wù)異常,可能需要在異常后轉(zhuǎn)入分支業(yè)務(wù)流程。如果業(yè)務(wù)異常都被框架捕獲了,業(yè)務(wù)功能就會(huì)不正常。
如果下層異常上升到 Controller 層還是無(wú)法處理的話,Controller 層往往會(huì)給予用戶(hù)友好提示,或是根據(jù)每一個(gè) API 的異常表返回指定的異常類(lèi)型,同樣無(wú)法對(duì)所有異常一視同仁。因此,我不建議在框架層面進(jìn)行異常的自動(dòng)、統(tǒng)一處理,尤其不要隨意捕獲異常。但,框架可以做兜底工作。如果異常上升到最上層邏輯還是無(wú)法處理的話,可以以統(tǒng)一的方式進(jìn)行異常轉(zhuǎn)換,比如通過(guò) @RestControllerAdvice + @ExceptionHandler,來(lái)捕獲這些“未處理”異常:
對(duì)于自定義的業(yè)務(wù)異常,以 Warn 級(jí)別的日志記錄異常以及當(dāng)前 URL、執(zhí)行方法等信息后,提取異常中的錯(cuò)誤碼和消息等信息,轉(zhuǎn)換為合適的 API 包裝體返回給 API 調(diào)用方;
對(duì)于無(wú)法處理的系統(tǒng)異常,以 Error 級(jí)別的日志記錄異常和上下文信息(比如 URL、參數(shù)、用戶(hù) ID)后,轉(zhuǎn)換為普適的“服務(wù)器忙,請(qǐng)稍后再試”異常信息,同樣以 API 包裝體返回給調(diào)用方
比如,下面這段代碼的做法
@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
private static int GENERIC_SERVER_ERROR_CODE = 2000;
private static String GENERIC_SERVER_ERROR_MESSAGE = "服務(wù)器忙,請(qǐng)稍后再試";
@ExceptionHandler
public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
if (ex instanceof BusinessException) {
BusinessException exception = (BusinessException) ex;
log.warn(String.format("訪問(wèn) %s -> %s 出現(xiàn)業(yè)務(wù)異常!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(false, null, exception.getCode(), exception.getMessage());
} else {
log.error(String.format("訪問(wèn) %s -> %s 出現(xiàn)系統(tǒng)異常!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
}
}
}
出現(xiàn)運(yùn)行時(shí)系統(tǒng)異常后,異常處理程序會(huì)直接把異常轉(zhuǎn)換為 JSON 返回給調(diào)用方:

二:其次,處理異常應(yīng)該杜絕生吞,并確保異常棧信息得到保留;
三:小心異常被覆蓋
1.異常生吞
說(shuō)明:
在任何時(shí)候,我們捕獲了異常都不應(yīng)該生吞,也就是直接丟棄異常不記錄、不拋出。這樣的處理方式還不如不捕獲異常,因?yàn)楸簧痰舻漠惓R坏?dǎo)致 Bug,就很難在程序中找到蛛絲馬跡,使得 Bug 排查工作難上加難。
2.丟棄異常的原始信息
錯(cuò)誤示例1
private void readFile() throws IOException {
Files.readAllLines(Paths.get("a_file"));
}
說(shuō)明
像這樣調(diào)用 readFile 方法,捕獲異常后,完全不記錄原始異常,直接拋出一個(gè)轉(zhuǎn)換后異常,導(dǎo)致出了問(wèn)題不知道 IOException 具體是哪里引起的:
錯(cuò)誤示例2
public void wrong1(){
try {
readFile();
} catch (IOException e) {
//原始異常信息丟失
throw new RuntimeException("系統(tǒng)忙請(qǐng)稍后再試");
}
}
說(shuō)明
像這樣調(diào)用 readFile 方法,捕獲異常后,完全不記錄原始異常,直接拋出一個(gè)轉(zhuǎn)換后異常,導(dǎo)致出了問(wèn)題不知道 IOException 具體是哪里引起的:
錯(cuò)誤示例3
catch (IOException e) {
//只保留了異常消息,棧沒(méi)有記錄
log.error("文件讀取錯(cuò)誤, {}", e.getMessage());
throw new RuntimeException("系統(tǒng)忙請(qǐng)稍后再試");
}
說(shuō)明
只記錄了異常消息,卻丟失了異常的類(lèi)型、棧等重要信息:
修改方式
catch (IOException e) {
throw new RuntimeException("系統(tǒng)忙請(qǐng)稍后再試", e);
}
如果需要重新拋出異常的話,請(qǐng)使用具有意義的異常類(lèi)型和異常消息。
錯(cuò)誤示例4
public void wrong() {
try {
log.info("try");
//異常丟失
throw new RuntimeException("try");
} finally {
log.info("finally");
throw new RuntimeException("finally");
}
}
說(shuō)明
try里面的異常被finally里面的異常覆蓋了
修復(fù)方式
把 try 中的異常作為主異常拋出,使用 addSuppressed 方法把 finally 中的異常附加到主異常上:
public void right2() throws Exception {
Exception e = null;
try {
log.info("try");
throw new RuntimeException("try");
} catch (Exception ex) {
e = ex;
} finally {
log.info("finally");
try {
throw new RuntimeException("finally");
} catch (Exception ex) {
if (e != null) {
e.addSuppressed(ex);
} else {
e = ex;
}
}
}
throw e;
}
備注
對(duì)于實(shí)現(xiàn)了 AutoCloseable 接口的資源,建議使用 try-with-resources 來(lái)釋放資源,否則也可能會(huì)產(chǎn)生剛才提到的,釋放資源時(shí)出現(xiàn)的異常覆蓋主異常的問(wèn)題。
確保正確處理了線程池中任務(wù)的異常
如果任務(wù)通過(guò) execute 提交,那么出現(xiàn)異常會(huì)導(dǎo)致線程退出,大量的異常會(huì)導(dǎo)致線程重復(fù)創(chuàng)建引起性能問(wèn)題,我們應(yīng)該盡可能確保任務(wù)不出異常,同時(shí)設(shè)置默認(rèn)的未捕獲異常處理程序來(lái)兜底;
如果任務(wù)通過(guò) submit 提交意味著我們關(guān)心任務(wù)的執(zhí)行結(jié)果,應(yīng)該通過(guò)拿到的 Future 調(diào)用其 get 方法來(lái)獲得任務(wù)運(yùn)行結(jié)果和可能出現(xiàn)的異常,否則異??赡芫捅簧?/strong>了。
修復(fù)方式
1.以 execute 方法提交到線程池的異步任務(wù),最好在任務(wù)內(nèi)部做好異常處理;
new ThreadFactoryBuilder().
setNameFormat(prefix + "%d").
setUncaughtExceptionHandler((thread, throwable) ->
log.error("ThreadPool {} got exception", thread, throwable))
.get()
2.設(shè)置自定義的異常處理程序作為保底,比如在聲明線程池時(shí)自定義線程池的未捕獲異常處理程序
設(shè)置全局的默認(rèn)未捕獲異常處理程序
static { Thread.setDefaultUncaughtExceptionHandler((thread, throwable)->
log.error("Thread {} got exception", thread, throwable));
}
異常棧是跟據(jù)當(dāng)前上下文生成,不可用static定義異常,否則會(huì)導(dǎo)致異常棧固化,從而影響問(wèn)題定位(認(rèn)為是走到其他分支了)。
錯(cuò)誤示例
public class Exceptions {
public static BusinessException ORDEREXISTS = new BusinessException("訂單已經(jīng)存在", 3001);
}
說(shuō)明
把異常定義為靜態(tài)變量會(huì)導(dǎo)致異常信息固化,這就和異常的棧一定是需要根據(jù)當(dāng)前調(diào)用來(lái)動(dòng)態(tài)獲取相矛盾。
修復(fù)方式
public class Exceptions {
public static BusinessException orderExists(){
return new BusinessException("訂單已經(jīng)存在", 3001);
}
}