Java 編譯器強制檢查受檢異常,但 JVM 運行時不保證方法拋出的異常與聲明一致。調用非受信代碼 (外部載入、動態生成、惡意實作) 時,僅捕獲 Exception 會導致 Error 或未聲明的受檢異常逃脫,造成線程死鎖、資源洩漏、安全漏洞。
public interface IFoo {
void bar(); // 未聲明任何 checked exception
}
try {
IFoo foo = ... // 從外部載入實作
foo.bar();
} catch (RuntimeException e) { /* 處理 */ }
catch (Error e) { /* 處理或重拋 */ }
錯誤假設:編譯器保證 bar() 不拋受檢異常,故無需捕獲 IOException 等。
public class EvilFoo implements IFoo {
private Throwable payload;
public EvilFoo(Throwable t) { this.payload = t; }
public void bar() { EvilThrow.throwThrowable(payload); }
}
class EvilThrow {
public static void throwThrowable(Throwable t) throws Throwable { throw t; }
// 關鍵:編譯後用 BCEL/Jasmin 移除方法簽名中的 throws Throwable
}
> java -cp classes Main
Exception in thread "main" java.io.IOException: SURPRISE!
at Main.main(Main.java:23)
// IOException 逃脫捕獲,主線程崩潰
throws 聲明try {
IFoo foo = loadExternalImplementation();
foo.bar();
} catch (Throwable t) { // 必須捕獲 Throwable
if (t instanceof Error) {
// 記錄並決定是否重拋 (如 OutOfMemoryError 應重拋)
logError(t);
throw (Error) t;
}
// 統一處理 Exception (含受檢與非受檢)
handleException(t);
}
public void dispatch() {
if (catchExceptions) {
try { runnable.run(); }
catch (Exception e) { exception = e; } // 只捕獲 Exception!
} else { runnable.run(); }
if (notifier != null) {
synchronized (notifier) { notifier.notifyAll(); } // 異常時不執行!
}
}
EvilFoo implements Runnable 至 SwingUtilities.invokeAndWait()run() 拋出 Throwable (非 Exception)dispatch() 的 catch (Exception) 捕獲不到notifier.notifyAll() 永不執行lock.wait() 永久阻塞 → **EDT 死鎖,GUI 完全凍結**| 場景 | 捕獲策略 | 說明 |
|---|---|---|
| 調用非受信代碼 (外掛、腳本、動態載入) | catch (Throwable) | 過濾 Error 重拋,統一處理 Exception |
| 框架/容器開發 (Servlet、EJB、Spring) | catch (Throwable) | 防止惡意/錯誤組件拖垮容器 |
| 一般業務邏輯 | catch (Exception) | 避免吞噬 Error,保持快速失敗 |
| 清理資源 (finally/try-with-resources) | 不依賴捕獲類型 | 確保資源釋放不受異常類型影響 |
@SuppressWarnings("unchecked") 等註解時同樣謹慎,字節碼層面可繞過