異常體系速覽
Throwable
├── Error (嚴重錯誤,不可恢復:OOM、StackOverflow、ClassFormatError)
└── Exception (可捕獲處理)
├── RuntimeException (非檢查異常,編譯器不強制處理)
│ ├── NullPointerException
│ ├── ClassCastException
│ ├── IllegalArgumentException
│ ├── IndexOutOfBoundsException
│ ├── ArithmeticException
│ ├── NumberFormatException
│ ├── ConcurrentModificationException
│ └── UnsupportedOperationException
└── Checked Exceptions (檢查異常,必須 try-catch 或 throws)
├── IOException
├── SQLException
├── ClassNotFoundException
├── InterruptedException
└── NoSuchMethodException
高頻 RuntimeException 詳細檔案
| 異常 | 典型成因 | 代碼示例 | 預防/處理 |
| NullPointerException | 呼叫 null 物件的方法/屬性、陣列元素為 null、自動拆箱 null | String s = null; s.length();
Integer i = null; int n = i; | @NotNull 驗證、Optional、Objects.requireNonNull、防禦性檢查 |
| ClassCastException | 強制轉型不相容類型、泛型擦除導致運行期轉型失敗 | Object o = new ArrayList(); String s = (String) o; | instanceof 守衛、編譯期泛型約束、避免原始類型 |
| IllegalArgumentException | 傳遞不符合預期的參數 (負數、空字串、超出範圍) | setAge(-5); thread.sleep(-100); | 參數驗證 (Bean Validation @Min/@Max)、Preconditions.checkArgument |
| IndexOutOfBoundsException | 陣列/列表/字串索引越界 (負數或 ≥ size) | list.get(10); // size=5
str.charAt(-1); | 邊界檢查、for-each 替代索引、List.getOrDefault() |
| ArithmeticException | 整數除以零、BigDecimal 非終結小數除法無精度設定 | int x = 10 / 0;
BigDecimal.ONE.divide(BigDecimal.valueOf(3)) | 除數檢查、BigDecimal.divide(divisor, scale, RoundingMode) |
| NumberFormatException | 字串解析為數字格式錯誤 (含空格、非數字字元、溢位) | Integer.parseInt("12a3");
Long.parseLong(""); | 正則預檢、try-catch 包裝、NumberUtils.isParsable() |
| ConcurrentModificationException | 迭代器遍歷時集合被修改 (單執行緒或多執行緒) | for (String s : list) { if (s.equals("x")) list.remove(s); } | Iterator.remove()、CopyOnWriteArrayList、CompletableFuture.allOf、Stream API |
| UnsupportedOperationException | 調用不可選操作 (Arrays.asList().add()、Collections.unmodifiableList().add()) | List.of(1,2).add(3); | 使用可變列表 new ArrayList<>(List.of(...))、檢查文檔 |
| ArrayStoreException | 陣列存入非相容類型 (Object[] arr = new String[1]; arr[0] = new Integer(1);) | Object[] arr = new String[1]; arr[0] = 1; | 避免陣列協變、使用 List<T>、泛型陣列工廠模式 |
| NegativeArraySizeException | 建立負長度陣列 new int[-5] | int size = -1; new byte[size]; | 長度參數驗證 if (size < 0) throw new IllegalArgumentException() |
Error 類 (不可恢復,通常不捕獲)
| Error | 成因 | 應對 |
| OutOfMemoryError | 堆/元空間/直接記憶體耗盡、記憶體洩漏、堆過小 | 分析 Heap Dump (MAT)、調大 -Xmx、修復洩漏、檢查 Metaspace |
| StackOverflowError | 遞歸過深、循環調用、線程棧過小 (-Xss) | 改迭代、尾遞歸優化、增大 -Xss、檢查循環依賴 |
| NoClassDefFoundError | 類載入失敗 (依賴缺失、靜態初始化拋異常、類路徑衝突) | 檢查依賴傳遞、mvn dependency:tree、排除衝突版本 |
| UnsatisfiedLinkError | JNI 本地庫找不到 (路徑錯、架構不匹配、庫缺失) | 配置 java.library.path、確認 32/64 位匹配 |
| VerifyError / ClassFormatError | 字節碼損壞、版本不兼容 (高版本編譯低版本運行) | 統一 JDK 版本、清理 target、檢查編譯目標版本 |
Checked Exception 代表 (必須處理)
| 異常 | 場景 | 處理策略 |
| IOException | 檔案/網路/流操作失敗 | try-with-resources、重試機制、熔斷器 |
| SQLException | 資料庫操作失敗 | 事務回滾、連線池驗證、死鎖重試 |
| ClassNotFoundException | 反射/動態載入類失敗 | 檢查類路徑、模組系統 opens、依賴管理 |
| InterruptedException | 線程睡眠/等待被中斷 | 重置中斷旗標 Thread.currentThread().interrupt()、拋出或終止任務 |
| TimeoutException | Future.get(timeout)、連線/讀取超時 | 超時重試、降級、熔斷 |
異常處理最佳實踐
- 具體捕獲:
catch (SQLException | IOException e) 而非 catch (Exception e)。
- 異常鏈保留:
throw new BusinessException("訂單建立失敗", e) 保留根因堆疊。
- 不吞噬異常:空
catch 塊是大忌,至少記錄日誌。
- 業務異常封裝:自訂
BusinessException(code, message),控制器統一處理返回標準錯誤碼。
- 資源確實釋放:
try (Resource r = acquire()) { ... } 自動關閉。
- 避免流程控制用異常:異常成本高,業務判斷用條件檢查。
- 全局異常處理:
@RestControllerAdvice + @ExceptionHandler 統一響應格式。
- 日誌記錄原則:
log.error("業務失敗", e) 而非 log.error(e.getMessage()) (丟失堆疊)。
自訂業務異常範例
@Getter
public class BusinessException extends RuntimeException {
private final int code;
private final String userMessage;
public BusinessException(ErrorCode errorCode, String... args) {
super(errorCode.getMessage(args));
this.code = errorCode.getCode();
this.userMessage = errorCode.getMessage(args);
}
public static BusinessException of(ErrorCode errorCode, Object... args) {
return new BusinessException(errorCode, args);
}
}
public enum ErrorCode {
ORDER_NOT_FOUND(4001, "訂單不存在: {0}"),
INSUFFICIENT_STOCK(4002, "庫存不足: 商品 {0} 僅剩 {1} 件"),
PAYMENT_FAILED(4003, "支付失敗: {0}");
private final int code;
private final String template;
ErrorCode(int code, String template) { this.code = code; this.template = template; }
public String getMessage(Object... args) { return MessageFormat.format(template, args); }
}
全局異常處理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException e) {
log.warn("業務異常: code={}, msg={}", e.getCode(), e.getUserMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getCode(), e.getUserMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
return ResponseEntity.badRequest().body(ApiResponse.error(4000, msg));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleUnknown(Exception e) {
log.error("系統異常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(5000, "系統內部錯誤,請稍後再試"));
}
}