例外處理是 Java 程式健壯性的重要保障。正確的例外處理策略可以幫助我們及時發現問題、優雅降級,並為排錯提供關鍵資訊。
例外體系結構#
Throwable
├── Error(不應捕獲)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception
├── RuntimeException(非檢查例外)
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ ├── IllegalArgumentException
│ └── ...
└── 其他 Exception(檢查例外)
├── IOException
├── SQLException
└── ...例外分類#
| 類型 | 說明 | 是否必須處理 |
|---|---|---|
| Error | 嚴重錯誤,JVM 無法恢復 | 不應捕獲 |
| Checked Exception | 編譯器強制處理 | 必須 try-catch 或 throws |
| Unchecked Exception | RuntimeException 及其子類 | 非強制 |
Error 表示 JVM 級別的嚴重問題(如 OOM),程式通常無法恢復,不應該捕獲。Exception 才是程式應該處理的例外。
例外處理最佳實踐#
1. 精確捕獲例外#
// 錯誤:過於寬泛
try {
doSomething();
} catch (Exception e) { // 不好!
log.error("Error", e);
}
// 正確:精確捕獲
try {
doSomething();
} catch (FileNotFoundException e) {
// 處理文件不存在
} catch (IOException e) {
// 處理其他 IO 例外
}捕獲過於寬泛的例外會掩蓋真正的問題,使除錯變得困難。
2. 不要忽略例外#
// 錯誤:吞掉例外
try {
doSomething();
} catch (Exception e) {
// 什麼都不做 - 絕對禁止!
}
// 正確:至少記錄日誌
try {
doSomething();
} catch (Exception e) {
log.error("Failed to do something", e);
// 或者重新拋出
throw new ServiceException("Operation failed", e);
}3. 使用 try-with-resources#
// 老式寫法(容易遺漏關閉)
InputStream is = null;
try {
is = new FileInputStream("file.txt");
// 使用
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// 又一個需要處理的例外
}
}
}
// 正確:try-with-resources(Java 7+)
try (InputStream is = new FileInputStream("file.txt")) {
// 使用
} // 自動關閉,例外也會正確處理4. 例外資訊要有意義#
// 不好
throw new IllegalArgumentException("參數錯誤");
// 好
throw new IllegalArgumentException(
String.format("用戶 ID 必須為正數,當前值: %d", userId));5. 保留原始例外鏈#
// 錯誤:丟失原始例外
try {
parseFile(file);
} catch (IOException e) {
throw new BusinessException("解析失敗"); // 丟失了 e!
}
// 正確:保留例外鏈
try {
parseFile(file);
} catch (IOException e) {
throw new BusinessException("解析失敗", e); // 保留原因
}常見陷阱#
1. finally 中的 return#
// 這段程式碼回傳什麼?
public int test() {
try {
return 1;
} finally {
return 2; // 這個 return 會覆蓋 try 中的
}
}
// 答案:回傳 2永遠不要在 finally 中使用 return。這會覆蓋 try 或 catch 中的回傳值,甚至會吞掉例外。
2. finally 中的例外#
try {
throw new RuntimeException("Original");
} finally {
throw new RuntimeException("Finally"); // 覆蓋了原始例外!
}
// 只會看到 "Finally" 例外3. 循環中的 try-catch#
// 不好:在循環內 try-catch
for (String item : items) {
try {
process(item);
} catch (Exception e) {
log.error("Error", e);
}
}
// 根據業務決定:是否需要繼續處理其他項?
// 如果一個失敗就全部停止:
try {
for (String item : items) {
process(item);
}
} catch (Exception e) {
log.error("Error", e);
}4. 例外與事務#
// Spring 預設只對 RuntimeException 回滾
@Transactional
public void createUser(User user) throws BusinessException {
userRepo.save(user);
if (invalid) {
throw new BusinessException("Invalid"); // 不會回滾!
}
}
// 正確:指定回滾的例外
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) throws BusinessException {
// ...
}檢查例外 vs 非檢查例外的選擇
使用檢查例外(Checked Exception)當:
- 呼叫方可以合理地處理這個例外
- 例如:文件不存在時可以創建、網路失敗時可以重試
使用非檢查例外(RuntimeException)當:
- 例外是程式設計錯誤導致(如 NPE、參數非法)
- 呼叫方通常無法恢復
- 例如:傳入了 null 參數、索引越界
現代趨勢:越來越多的專案(包括 Spring)傾向於使用非檢查例外,因為:
- 程式碼更簡潔
- 避免例外聲明污染介面
- 讓呼叫方自己決定是否處理
自定義例外#
例外層次設計#
// 基礎業務例外
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// 具體業務例外
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(Long userId) {
super("USER_NOT_FOUND", "User not found: " + userId);
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(BigDecimal required, BigDecimal available) {
super("INSUFFICIENT_BALANCE",
String.format("Insufficient balance. Required: %s, Available: %s",
required, available));
}
}統一例外處理(Spring)#
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
log.warn("Business exception: {}", e.getMessage());
return ResponseEntity
.badRequest()
.body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Unexpected error", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "Internal server error"));
}
}例外處理檢查清單#
- 不要捕獲 Throwable 或 Error
- 不要忽略例外(空 catch 塊)
- 不要在 finally 中 return 或 throw
- 使用 try-with-resources 管理資源
- 保留例外鏈(傳遞 cause)
- 提供有意義的例外資訊
- 精確捕獲例外類型
- 根據場景選擇 Checked/Unchecked
- 統一例外處理策略
- 記錄例外日誌時包含堆疊資訊
實踐建議#
- 早拋出,晚捕獲:在發現問題的地方拋出,在能夠處理的地方捕獲
- 為運維提供足夠資訊:例外資訊應包含上下文(用戶 ID、操作類型等)
- 區分業務例外和技術例外:業務例外給用戶友好提示,技術例外記錄詳細日誌
- 考慮重試機制:對於暫時性故障(網路抖動),可以設計重試策略
- 監控例外:將例外資料納入監控系統,及時發現問題