在異常處理流程中,有時需要在捕獲異常後記錄日誌、包裝資訊,再向上層拋出。Java 提供兩種重新拋出方式:直接 throw e 保留原始堆疊,或呼叫 fillInStackTrace() 重設起點。本文將深入剖析兩者差異、適用場景,以及涉及 Throwable 型別時的編譯期檢查細節。
最簡單的方式是直接拋出捕獲的異常參考:
catch (Exception e) {
log.error("操作失敗", e);
throw e; // 保留原始堆疊追蹤
}
關鍵特性:
try 區塊後續的 catch 子句會被略過getStackTrace() 看到完整呼叫鏈呼叫 e.fillInStackTrace() 會將目前執行點填入異常物件,回傳新的 Throwable 參考:
catch (Exception e) {
log.error("操作失敗", e);
throw e.fillInStackTrace(); // 堆疊起點變成這一行
}
以下程式分別展示兩種方式的輸出差異:
public class Rethrowing {
public static void f() throws Exception {
throw new Exception("thrown from f()");
}
public static void g() throws Throwable {
try { f(); }
catch (Exception e) {
e.printStackTrace(); // 印出原始堆疊
// throw e; // 方式 1:保留原始
throw e.fillInStackTrace(); // 方式 2:重設起點
}
}
public static void main(String[] args) throws Throwable {
try { g(); }
catch (Exception e) { e.printStackTrace(); }
}
}
方式 1(throw e)輸出:
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
兩次印出的堆疊完全相同,皆指向 f() 第 8 行。
方式 2(fillInStackTrace)輸出:
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
java.lang.Exception: thrown from f()
at Rethrowing.g(Rethrowing.java:18) // ← 新起點!
at Rethrowing.main(Rethrowing.java:24)
第二次堆疊追蹤的起點變成 g() 第 18 行(fillInStackTrace() 呼叫處),原始 f() 資訊遺失。
fillInStackTrace() 回傳型別為 Throwable,而非 Exception。因此:
throws Throwable(而非 throws Exception)catch (Exception e) 捕獲,將漏接此異常,導致編譯錯誤或執行期未處理示範:
public static void main(String[] args) throws Throwable { // 必須 throws Throwable
try { g(); }
catch (Exception e) { ... } // 若 g() 拋出 fillInStackTrace() 結果,此處捕獲不到
}
若在 catch 區塊建立新異常物件拋出:
catch (Exception e) {
throw new NullPointerException("from main"); // 原始異常資訊完全遺失
}
輸出僅顯示新異常的起點(main()),原始 f() 堆疊消失。若需保留因果關係,應使用異常鏈:
throw new CustomException("業務失敗", e); // 將原始異常作為 cause 傳入
異常物件同樣受垃圾收集器管理,重新拋出不會造成記憶體洩漏,無需手動清除。
| 場景 | 建議做法 |
|---|---|
| 單純記錄日誌後向上拋出 | throw e; 保留完整堆疊 |
| 需包裝業務語意、保留原始原因 | 建立新異常:throw new BizException("訊息", e); |
| 安全性考量,隱藏內部實作細節 | throw e.fillInStackTrace(); 切斷內部堆疊(謹慎使用) |
| 轉換為不同異常類型 | 務必帶入 cause 參數維持異常鏈 |
掌握重新拋出的細微差異,能讓你在異常傳遞鏈中精準控制診斷資訊的保留與揭露程度。