Java 異常分為兩大派系,體現了設計者對「可控性」的不同預期:
| 維度 | 受檢異常 (Checked Exception) | 非受檢異常 (Unchecked Exception) |
|---|---|---|
| 繼承體系 | Exception (非 RuntimeException) | RuntimeException、Error |
| 編譯器強制 | ✅ 必須 catch 或 throws | ❌ 無強制要求 |
| 典型代表 | IOException、SQLException、TimeoutException | NullPointerException、IllegalArgumentException、ArithmeticException |
| 業務含義 | 外部依賴失敗、資源不可用、可預期且可恢復 | 編程邏輯錯誤、不可預期、通常不可恢復 |
| 處理策略 | 捕獲轉換、重試、降級、用戶友好提示 | 修復代碼、添加防禦檢查、全局捕獲記錄 |
// 根異常:攜帶錯誤碼、國際化鍵、動態參數
public class BizException extends RuntimeException {
private final String code;
private final Object[] args;
public BizException(String code, Object... args) {
super(code); // message 存 code,便於日誌檢索
this.code = code;
this.args = args;
}
public String getCode() { return code; }
public Object[] getArgs() { return args; }
}
// 業務細分異常 (可選,便於針對性捕獲)
public class ResourceNotFoundException extends BizException {
public ResourceNotFoundException(String resourceType, String id) {
super("error.resource.notfound", resourceType, id);
}
}
public class BusinessRuleViolationException extends BizException {
public BusinessRuleViolationException(String rule, Object... args) {
super("error.rule.violation", args);
}
}
// 系統級異常 (包裝第三方異常)
public class SystemException extends RuntimeException {
public SystemException(String message, Throwable cause) {
super(message, cause);
}
}
<error-page>
<error-code>404</error-code>
<location>/error/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error/500.html</location>
</error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error/uncaught.html</location>
</error-page>
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="com.xxx.BizException">error/biz</prop>
<prop key="org.springframework.dao.DataAccessException">error/db</prop>
<prop key="java.lang.Exception">error/500</prop>
</props>
</property>
<property name="defaultErrorView" value="error/500"/>
<property name="warnLogCategory" value="WARN"/>
</bean>
@Component
public class GlobalExceptionResolver implements HandlerExceptionResolver {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionResolver.class);
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
// 1. 識別 AJAX/REST 請求
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"))
|| request.getRequestURI().startsWith("/api/");
// 2. 構建統一響應
ApiResponse resp = buildResponse(ex);
if (isAjax) {
// JSON 響應
writeJsonResponse(response, resp);
return null; // 已寫入響應,不需視圖解析
} else {
// 頁面跳轉
ModelAndView mav = new ModelAndView("error/page");
mav.addObject("error", resp);
return mav;
}
}
private ApiResponse buildResponse(Exception ex) {
if (ex instanceof BizException) {
BizException be = (BizException) ex;
return ApiResponse.fail(be.getCode(), be.getArgs());
} else if (ex instanceof ConstraintViolationException) {
return ApiResponse.fail("error.validation", ex.getMessage());
} else if (ex instanceof DataAccessException) {
log.error("數據庫訪問異常", ex);
return ApiResponse.fail("error.database");
} else {
log.error("系統未知異常", ex);
return ApiResponse.fail("error.system");
}
}
private void writeJsonResponse(HttpServletResponse response, ApiResponse resp) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
try {
objectMapper.writeValue(response.getWriter(), resp);
} catch (IOException e) {
log.error("異常響應序列化失敗", e);
}
}
}
// 統一響應結構
public class ApiResponse {
private boolean success;
private String code;
private String message;
private Object data;
private long timestamp = System.currentTimeMillis();
public static ApiResponse ok(Object data) { ... }
public static ApiResponse fail(String code, Object... args) { ... }
// getter/setter
}
v.getPropertyPath() + " " + v.getMessage())
.collect(Collectors.joining("; "));
return ApiResponse.fail("error.validation", msg);
}
@ExceptionHandler(DataAccessException.class)
public ApiResponse handleDataAccess(DataAccessException ex) {
log.error("數據庫異常", ex);
return ApiResponse.fail("error.database");
}
@ExceptionHandler(Exception.class)
public ApiResponse handleSystem(Exception ex) {
log.error("系統異常", ex);
return ApiResponse.fail("error.system");
}
// 異步處理異常 (CompletableFuture、@Async)
@ExceptionHandler(AsyncRequestTimeoutException.class)
public ApiResponse handleAsyncTimeout(AsyncRequestTimeoutException ex) {
return ApiResponse.fail("error.timeout");
}
}
throw new BizException(..., cause) 保留堆棧HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation-failed",
"title": "請求參數校驗失敗",
"status": 422,
"detail": "手機號格式不正確;郵箱已被註冊",
"instance": "/api/v1/users/123",
"errors": [
{"field": "phone", "code": "pattern.mismatch", "message": "手機號格式不正確"},
{"field": "email", "code": "unique.violation", "message": "郵箱已被註冊"}
],
"timestamp": "2024-01-15T10:30:00Z",
"traceId": "a1b2c3d4e5f6"
}
@Component
public class ExceptionMetrics {
private final MeterRegistry registry;
private final Counter bizExceptionCounter;
private final Counter systemExceptionCounter;
public ExceptionMetrics(MeterRegistry registry) {
this.registry = registry;
this.bizExceptionCounter = Counter.builder("exception.biz")
.tag("type", "biz").register(registry);
this.systemExceptionCounter = Counter.builder("exception.system")
.tag("type", "system").register(registry);
}
public void recordBizException(String code) {
bizExceptionCounter.increment(Tag.of("code", code));
}
public void recordSystemException(Class<? extends Throwable> clazz) {
systemExceptionCounter.increment(Tag.of("class", clazz.getSimpleName()));
}
}
// 在 GlobalExceptionHandler 中調用
@Autowired private ExceptionMetrics metrics;
@ExceptionHandler(BizException.class)
public ApiResponse handleBiz(BizException ex) {
metrics.recordBizException(ex.getCode());
return ApiResponse.fail(ex.getCode(), ex.getArgs());
}
| 反模式 | 危害 | 正確做法 |
|---|---|---|
| catch (Exception e) { log.error(); return null; } | 吞噬異常、調用方 NPE、丟失堆棧 | 拋出業務異常或系統異常 |
| throws Exception | 調用方無法針對性處理、文檔不清 | 拋具體異常類型或自定義異常 |
| 用異常控制業務流程 | 性能差、代碼難讀、調試困難 | 用條件判斷、狀態機、Optional |
| 自定義異常不帶原因 | 丟失根因、排查困難 | 構造器傳入 cause |
| 全局處理器返回 200 OK + 錯誤碼 | 違反 HTTP 語義、客戶端難識別 | 返回對應 4xx/5xx 狀態碼 |
建立統一異常處理體系,不僅能提升代碼可維護性、用戶體驗、運維可觀測性,更能倒逼業務邊界清晰化、錯誤語義標準化。核心在於「分層轉換、統一響應、語義豐富、可觀測」。