阿里郎,還記得你們公司《手冊》中例外處理給出的這些建議嗎?


2 "吞掉"例外?
2.1 簡介
即,處理后不再將例外傳給上層,其中包括 catch 到例外并處理(列印日志、發通知等)后不再扔給上層;捕捉到例外后給上層回傳 null 值等行為,
前一小節的強制 5就屬于該種措施,
2.2 為什么要手動回滾
先看事務的執行入口:
- TransactionInterceptor#invoke

- TransactionAspectSupport#invokeWithinTransaction :
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
...
帶 @Transaction 注解的事務函式中捕獲到例外后,執行
- TransactionAspectSupport#completeTransactionAfterThrowing
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}
可以看到,如果設定了事務屬性且當前例外滿足 rollbackOn 指定的例外(默認為 RuntimeException 型別及其子類以及Error 及其子類),則會將當前事務回滾,否則提交,
因此如果 catch 例外后沒有再次將例外拋出或者不手動回滾,將會導致事務提交,
封裝二方介面時,很多人會吞例外,如下:
@Component
public class DemoClient {
@Resource
private XXServcie xxServcie;
public XXInfo someMethod(Long id) {
try {
return xxServcie.getXXInfo(id);
} catch (Exception e) {
log.warn("呼叫xx服務例外,引數:{}", id, e);
return null;
}
}
}
當呼叫例外時列印例外資訊后直接回傳 null,
此時如果呼叫方直接拿到回傳值物件而未做判空處理直接使用其屬性,易 NPE,
而且吞二方介面例外,有些業務例外中包含的錯誤原因(如包含xxx敏感詞匯、標題不能超過20個字等)無法傳給上層再封裝給前端,可能會造成出錯后用戶懵逼,被投訴,
比如用戶輸入了某個敏感詞匯,呼叫二方介面時 “吞掉” 了敏感詞匯的業務例外提示(輸入中包含 xx敏感詞),用戶通過技術支持咨詢,開發人員要查日志才能知道具體的錯誤原因(如果此處沒列印日志,可能連日志都沒得查),非常低效,
所以要根據具體業務場景慎重確定是否要吞例外,
3 回圈中的例外處理問題
特別注意回圈的代碼例外處理的對程式的影響,
案例1

在寫代碼時這種場景非常常見,如果不對回圈代碼進行捕捉,如果回圈中出現例外,后續代碼則無法執行,
但是如果在 for 回圈外部捕捉例外,雖然for回圈后如果有代碼依然可以執行,但是串列中的非最后一個元素作為引數呼叫 doSomeRemoteInvoke 出現例外,后續資料無法繼續執行,
try {
for (String str : data) {
// 遠程方法呼叫(可能出現例外)
String result = doSomeRemoteInvoke(str);
System.out.println(result);
}
} catch (Exception e) {
log.error("程式出錯,引數data:{},錯誤詳情", JSON.toJSONString(data), e);
}
因此需要對 for 回圈代碼內對可能出現的例外進行捕捉:
for (String str : data) {
try {
// 遠程方法呼叫(可能出現例外)
String result = doSomeRemoteInvoke(str);
System.out.println(result);
} catch (Exception e) {
log.error("程式出錯,引數data:{},錯誤詳情", JSON.toJSONString(data), e);
}
}
案例 2
思考兩個問題:
- 分別呼叫兩個函式 pirntList1 和 printList2 輸出的結果有何不同
- 哪個不需要捕捉例外也不會造成中間有一個出錯后續處理中斷
代碼如下:

- 在函式 pirntList1

和上面的代碼非常相似,for 回圈在執行緒池代碼外部,每次回圈呼叫執行緒池去執行判斷和列印陳述句,
此時依次傳入 a、ab、abc、abcd 四個字串;當執行到 ab 時會拋出 IllegalArgumentException,此時執行緒池中的唯一的執行緒銷毀;當執行到 abc 字串時,再次在執行緒池中執行,執行緒池創建新的執行緒來執行,依然可以正常執行,

- 而在函式 pirntList2

中 for 回圈在 執行緒池 execute 引數的lambda運算式內,所有的回圈執行都在同一個執行緒內,當執行到 ab 字串時,拋出了例外,導致整個執行緒銷毀,無法繼續執行,

因此為了不讓一個資料出錯導致后續的代碼都無法執行,如果采用第二種方式來執行可以對代碼做出如下修改:

在實際業務開發程序中,這種問題比較隱蔽,尤其是在異步執行緒中執行時,如果不加留意,很容易出現上面所描述的問題,
4 最佳實踐
4.1 權衡是否吞例外
在二方服務封裝時,如捕捉例外,應列印出查詢引數和例外詳情,
實際開發中,一般都不會吞例外,遇到吞例外場景要慎重思考是否合理,
另外,正如第二部分給出的范例所示,如果呼叫二方介面出現例外沒有列印日志,將對排查問題造成很大的困難,
受檢例外 非受檢例外
Java 中的例外主要分為兩類:受檢例外和非受檢例外,
根據 JLS 例外部分的描述
- 受檢例外主要指編譯時強制檢查的例外,包括非受檢例外之外的其他 Throwable 的子類
- 非受檢例外主要指編譯器免檢例外,通常包括運行時例外類和 Error相關類

Error 和 Exception 都是 Throwable的子類, RuntimeException 和其子類都屬于運行時例外,Error 類和其子類都屬于錯誤類,RuntimeException 及其子類 和 Error類及其子類 屬于非受檢例外,除此之外的 其他 Throwable 子類屬于受檢例外,
開發中自定義的業務例外(BusinessException)屬非受檢例外,會定義為 RuntimeException 的子類,
有人將業務例外定義為受檢例外,會導致底層拋出后上層呼叫每層都要被迫處理,
努力使失敗保持原子性,
《Effective Java》第 3 版 第 76條 努力使失敗保持原子性[^2] 所提到的那樣,
我們可以在函式核心代碼執行前對引數進行檢查,對不滿足的條件拋出適當的例外,
實際開發中通常可以使用 com.google.common.base.Preconditions 或者 org.apache.commons.lang3.Validate 第三方庫提供的引數檢查工具類來實作,
如果忽略例外,請給出理由
如果 catch 住例外卻沒有進行撰寫任何處理代碼,請在注釋中給出充分的理由,避免其他人產生困惑,避免留坑,
大家可以參考 org.springframework.context.support.AbstractApplicationContext#close 原始碼:

上面的原始碼捕捉到 IllegalStateException 例外以后沒有處理,給出了處理方式和原因: 忽略此例外,因為虛擬機已經正在關閉,
5.總結
本節主要講例外的一些處理建議,包括是否要 “吞掉” 例外,回圈中的例外處理,以及一些補充建議,希望大家可以重視例外,少趟坑,
參考
- 阿里巴巴Java 開發手冊 華山版
- 《Effective Java 中文版 (原書第 3 版)》
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/277652.html
標籤:java
