在 Java 開發中,我們常被教導要具體捕獲異常,避免使用 catch (Exception e) 這種「大網捕魚」的寫法。然而,當面對外部庫、動態加載的類或不可信代碼時,這條鐵律可能失效。本文將通過實例深入剖析 Java 異常處理在運行時的真實行為,揭示為什麼必須捕獲 Throwable 而非僅捕獲 Exception,並提供實戰防禦策略。
Java 的受檢異常機制僅在編譯階段生效。JVM 在運行時並不強制驗證方法拋出的異常是否與聲明一致。這意味著,一個聲明 void bar() 無拋出異常的接口方法,在運行時卻可能拋出 IOException 甚至任意 Throwable。
public interface IFoo {
void bar(); // 聲明不拋出任何受檢異常
}
// 惡意實現
public class EvilFoo implements IFoo {
private final Throwable throwable;
public EvilFoo(Throwable t) { this.throwable = t; }
public void bar() { EvilThrow.throwThrowable(throwable); }
}
// 利用 BCEL 修改字節碼,移除 throwThrowable 的 throws 聲明
public class EvilThrow {
public static void throwThrowable(Throwable t) throws Throwable {
throw t;
}
}
當調用 foo.bar() 時,雖然編譯器認為不會拋出受檢異常,但運行時實際拋出了 IOException,直接穿透了只捕獲 RuntimeException 和 Error 的捕獲塊,導致程序崩潰。
throws 聲明僅供編譯器檢查,字節碼中可被篡改或通過 Thread.stop(Throwable) 等過時 API 注入異常。Sun JDK 1.4.1 中 InvocationEvent.dispatch() 僅捕獲 Exception:
try {
runnable.run();
} catch (Exception e) {
exception = e;
}
// 若 runnable 拋出 Throwable (非 Exception),exception 保持 null
// notifier.notifyAll() 永不執行
// 調用線程在 lock.wait() 永久阻塞 → 死鎖
若不可信代碼在 Runnable.run() 中拋出 Throwable,將導致 AWT 事件分發線程死鎖,整個 GUI 凍結。
try {
externalComponent.execute();
} catch (ThreadDeath td) {
throw td; // 必須重拋,否則線程無法正常終止
} catch (Throwable t) {
// 記錄、隔離、降級處理
logger.error("外部組件異常", t);
handleGracefully(t);
}
關鍵點:必須單獨捕獲 ThreadDeath 並重拋,否則會阻礙線程正常停止。
private void handleGracefully(Throwable t) {
if (t instanceof VirtualMachineError) {
// OOM、StackOverflow 等嚴重錯誤,考慮快速失敗
System.exit(1);
} else if (t instanceof ThreadDeath) {
throw (ThreadDeath) t;
} else if (t instanceof InterruptedException) {
Thread.currentThread().interrupt(); // 恢復中斷狀態
} else {
// 業務異常:記錄、告警、返回默認值或拋出自定義受檢異常
}
}
SecurityManager 限制權限(舊版本)Java 異常機制的設計初衷是靜態類型安全,但在動態加載、字節碼操作、外部庫調用等場景下,運行時行為可能違背編譯時契約。作為框架開發者或編寫高可用系統時,必須在邊界處捕獲 Throwable,並妥善處理 Error、ThreadDeath 等特殊情況。記住:捕獲 Exception 是業務邏輯的事,捕獲 Throwable 是系統生存的事。