首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 異常分類體系與統一異常處理最佳實踐

     編輯:Java 相關知識

異常分類哲學

Java 異常分為兩大派系,體現了設計者對「可控性」的不同預期:

維度受檢異常 (Checked Exception)非受檢異常 (Unchecked Exception)
繼承體系Exception (非 RuntimeException)RuntimeException、Error
編譯器強制✅ 必須 catch 或 throws❌ 無強制要求
典型代表IOException、SQLException、TimeoutExceptionNullPointerException、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);
    }
}

統一異常處理三層架構

第一層:容器級 (Web.xml / Servlet 容器)

<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>
  • 優點:容器啟動階段異常、靜態資源 404、Filter 鏈異常均可捕獲
  • 缺點:無法處理 AJAX/REST 請求、無法獲取業務上下文、頁面跳轉體驗差

第二層:框架級 (Spring MVC HandlerExceptionResolver)

方案 A:SimpleMappingExceptionResolver (傳統頁面)
<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>
方案 B:自定義 HandlerExceptionResolver (推薦,支持 REST/AJAX)
@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
}
方案 C:@ControllerAdvice + @ExceptionHandler (Spring 3.2+ 推薦)
 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");
    }
}

第三層:業務代碼層 (try-catch 處理可恢復異常)

異常處理核心原則

  1. 異常即流程:不要用返回碼/判空代替異常,異常自帶堆棧、上下文、類型層次
  2. 失快原則:參數校驗、前置條件檢查盡早拋出異常
  3. 包裝不吞噬:捕獲異常必須 throw new BizException(..., cause) 保留堆棧
  4. 分層轉換:DAO 層拋 SQLException → Service 層包裝為 DataAccessException → Controller 層由全局處理器轉為 ApiResponse
  5. 業務異常用非受檢:BizException 繼承 RuntimeException,減少 throws 傳播污染
  6. 系統異常記錄 ERROR:業務異常記錄 WARN,系統異常記錄 ERROR 並告警
  7. 幂等性與重試:對於可重試操作 (網絡抖動),使用 @Retryable 或手動重試

RESTful API 錯誤響應規範 (RFC 7807)

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 狀態碼

建立統一異常處理體系,不僅能提升代碼可維護性、用戶體驗、運維可觀測性,更能倒逼業務邊界清晰化、錯誤語義標準化。核心在於「分層轉換、統一響應、語義豐富、可觀測」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved