為什麼需要自定義異常?
| 場景 | 說明 |
| 統一對外錯誤碼 | 微服務間、前後端分離約定統一 code/message 格式 |
| 業務語義表達 | 如 InsufficientBalanceException 比 RuntimeException 更清晰 |
| 隱藏底層細節 | 封裝 SQL/網絡異常,避免洩露堆棧敏感信息 |
| 快速定位 | 異常類名即業務含義,日誌檢索效率高 |
| 控制流終止 | 校驗失敗直接 throw,配合 @ControllerAdvice 統一響應 |
自定義異常體系設計
基礎異常類
public class BizException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final String errorCode; // 業務錯誤碼
private final boolean i18nKey; // message 是否為 i18n key
public BizException(String errorCode, String message) {
this(errorCode, message, true, null);
}
public BizException(String errorCode, String message, Throwable cause) {
this(errorCode, message, true, cause);
}
public BizException(String errorCode, String message, boolean i18nKey, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.i18nKey = i18nKey;
}
// getters...
}
具體業務異常(枚舉式管理)
public enum ErrorCode {
USER_NOT_FOUND("USER_001", "用戶不存在"),
INSUFFICIENT_BALANCE("PAY_002", "餘額不足"),
PARAM_INVALID("SYS_003", "參數校驗失敗: {0}");
private final String code, template;
ErrorCode(String code, String template) { this.code=code; this.template=template; }
public BizException newException(Object... args) {
String msg = MessageFormat.format(template, args);
return new BizException(code, msg);
}
}
// 使用
throw ErrorCode.USER_NOT_FOUND.newException(userId);
throw ErrorCode.PARAM_INVALID.newException("email");
Spring MVC 統一異常處理
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BizException.class)
public R handleBiz(BizException e) {
log.warn("業務異常: {} - {}", e.getErrorCode(), e.getMessage());
return R.error(e.getErrorCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining("; "));
return R.error("SYS_003", msg);
}
@ExceptionHandler(Exception.class)
public R handleSys(Exception e) {
log.error("系統異常", e);
return R.error("SYS_999", "系統繁忙,請稍後重試");
}
}
測試驗證
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
if (id <= 0) throw ErrorCode.PARAM_INVALID.newException("id");
User user = userService.findById(id);
if (user == null) throw ErrorCode.USER_NOT_FOUND.newException(id);
return user;
}
}
避坑指南
- 異常濫用:非異常流程(如分頁無數據)勿用異常,返回空集合/Optional
- 丟失堆棧:
throw new BizException(...) 而非 throw new BizException(e.getMessage()),保留 cause
- 過度繼承:業務異常扁平化,避免深層繼承樹
- 國際化:message 存 i18n key,前端按 locale 渲染
- 性能:高頻場景(如參數校驗)預構建異常實例或用錯誤碼枚舉
完整代碼結構
com.example.exception
├── BizException.java // 基礎異常
├── ErrorCode.java // 錯誤碼枚舉
├── GlobalExceptionHandler.java // 統一處理
└── R.java // 統一響應封裝