Java异常处理最佳实践:异常分类、传播与全局处理器的设计规范
Java异常处理最佳实践异常分类、传播与全局处理器的设计规范见过太多项目里的catch (Exception e) { e.printStackTrace(); }也见过不少把所有异常都包装成 RuntimeException 然后往上抛的统一处理。异常处理看似基础但做得好的团队寥寥。这篇文章梳理一套生产级的 Java 异常处理规范。一、Checked 与 Unchecked 异常的语义划分Java 的 Checked / Unchecked 异常机制可能是被误解最多的语言特性。常见误区有两个一是Checked 异常应该全部转成 Unchecked二是只要是业务异常就该继承 RuntimeException。正确的划分逻辑应该从调用方是否有能力处理出发graph TB A[异常发生] -- B{调用方能br/合理处理吗} B --|是 - 可恢复| C[Checked Exception] B --|否 - 不可恢复| D{什么原因} D --|程序Bug| E[Unchecked - 继承 RuntimeException] D --|外部资源故障| F[Unchecked - 继承 RuntimeException] D --|业务规则不满足| G[Unchecked - 业务异常体系] C -- C1[如文件未找到br/网络超时可重试br/锁获取超时] E -- E1[如NPE、数组越界br/不应该去catch] F -- F1[如数据库连接断开br/Redis不可用] G -- G1[如库存不足br/订单状态不允许] style C fill:#e3f2fd style E fill:#ffebee style F fill:#fff3e0 style G fill:#e8f5e9这个划分框架意味着Checked 异常只用于调用方可以从中恢复的场景。典型的如IOException重试、TimeoutException降级。如果你的调用方除了打日志什么都做不了那就不该用 Checked 异常。业务异常应该继承RuntimeException。比如库存不足订单已关闭用户未认证——这些是业务规则的违反调用方通常无法在代码层面恢复应该由全局异常处理器统一转换为用户可读的错误响应。程序 BugNPE、数组越界、类型转换错误不应该被 catch。让它们快速失败通过监控和告警来修复代码。二、业务异常体系设计2.1 错误码体系业务异常体系需要一个结构化的错误码而不是散落的字符串/** * 统一错误码接口 * 所有业务错误码必须实现此接口 */ public interface ErrorCode { /** 错误码模块前缀 序号 */ String getCode(); /** HTTP 状态码 */ int getHttpStatus(); /** 错误消息模板支持占位符 */ String getMessageTemplate(); /** 错误级别 */ ErrorLevel getLevel(); } /** * 错误级别枚举 */ public enum ErrorLevel { /** 提示级别 - 正常的业务拒绝不需要告警 */ INFO, /** 警告级别 - 异常但可忽略的模式 */ WARN, /** 错误级别 - 需要关注的系统异常 */ ERROR, /** 致命级别 - 需要立即通知 on-call */ FATAL } /** * 订单域错误码枚举 */ Getter AllArgsConstructor public enum OrderErrorCode implements ErrorCode { ORDER_NOT_FOUND(ORD-0001, 404, 订单不存在: orderId{0}, ErrorLevel.INFO), ORDER_STATUS_INVALID(ORD-0002, 409, 订单状态不允许此操作: orderId{0}, currentStatus{1}, requiredStatus{2}, ErrorLevel.INFO), INSUFFICIENT_INVENTORY(ORD-0003, 409, 库存不足: skuId{0}, requested{1}, available{2}, ErrorLevel.INFO), PAYMENT_FAILED(ORD-0004, 402, 支付失败: orderId{0}, reason{1}, ErrorLevel.WARN), ORDER_CREATE_DB_ERROR(ORD-0005, 500, 订单创建数据库异常: {0}, ErrorLevel.ERROR), ORDER_DUPLICATE_SUBMIT(ORD-0006, 409, 重复提交: orderId{0}, idempotencyKey{1}, ErrorLevel.INFO), ORDER_PRICE_MISMATCH(ORD-0007, 409, 订单价格不一致: orderId{0}, clientPrice{1}, serverPrice{2}, ErrorLevel.WARN); private final String code; private final int httpStatus; private final String messageTemplate; private final ErrorLevel level; /** * 使用 MessageFormat 格式化错误消息 */ public String formatMessage(Object... args) { return new MessageFormat(this.messageTemplate).format(args); } }2.2 业务异常基类/** * 统一业务异常基类 * 所有需要返回给客户端的业务异常都继承此类 */ Getter public class BusinessException extends RuntimeException { private final ErrorCode errorCode; private final Object[] messageArgs; private final String traceId; private final Instant timestamp; public BusinessException(ErrorCode errorCode, Object... messageArgs) { super(errorCode.formatMessage(messageArgs)); this.errorCode errorCode; this.messageArgs messageArgs; this.traceId MDC.get(traceId); this.timestamp Instant.now(); } public BusinessException(ErrorCode errorCode, Throwable cause, Object... messageArgs) { super(errorCode.formatMessage(messageArgs), cause); this.errorCode errorCode; this.messageArgs messageArgs; this.traceId MDC.get(traceId); this.timestamp Instant.now(); } /** * 创建异常但不填充调用栈性能优化 * 适用于高频、可预期的业务异常如参数校验失败 */ public static BusinessException withoutStackTrace(ErrorCode errorCode, Object... messageArgs) { BusinessException ex new BusinessException(errorCode, messageArgs); ex.setStackTrace(new StackTraceElement[0]); return ex; } } /** * 系统异常 - 不应该暴露给客户端的内部异常 */ Getter public class SystemException extends RuntimeException { private final String errorCode; private final String traceId; public SystemException(String message, Throwable cause) { super(message, cause); this.errorCode SYS-9999; this.traceId MDC.get(traceId); } public SystemException(String errorCode, String message, Throwable cause) { super(message, cause); this.errorCode errorCode; this.traceId MDC.get(traceId); } }2.3 异常使用的语义约定/** * 订单服务 - 展示异常使用的正确姿势 */ Service Slf4j public class OrderService { private final OrderRepository orderRepository; private final InventoryService inventoryService; private final PaymentGateway paymentGateway; public OrderService(OrderRepository orderRepository, InventoryService inventoryService, PaymentGateway paymentGateway) { this.orderRepository orderRepository; this.inventoryService inventoryService; this.paymentGateway paymentGateway; } /** * 创建订单 - 展示业务异常的规范使用 */ Transactional public OrderDTO createOrder(CreateOrderRequest request) { // 1. 参数校验失败 → 快速失败, 无需栈追踪 if (request.getItems() null || request.getItems().isEmpty()) { throw BusinessException.withoutStackTrace( CommonErrorCode.PARAM_INVALID, items, 订单项不能为空); } // 2. 库存扣减 → 业务异常可恢复调用方可以提示用户减少数量 try { inventoryService.deduct(request.getItems()); } catch (InsufficientInventoryException e) { throw new BusinessException( OrderErrorCode.INSUFFICIENT_INVENTORY, e.getSkuId(), e.getRequested(), e.getAvailable()); } // 3. 数据库操作 → 系统异常不可恢复需要告警 OrderEntity order; try { order orderRepository.save(buildOrderEntity(request)); } catch (DataAccessException e) { log.error(订单持久化失败: userId{}, request.getUserId(), e); throw new SystemException( ORD-PERSIST, 订单持久化失败, e); } return toDTO(order); } /** * 支付订单 - 展示异常链的保留 */ public PaymentResult payOrder(Long orderId, PaymentRequest paymentRequest) { OrderEntity order orderRepository.findById(orderId) .orElseThrow(() - new BusinessException( OrderErrorCode.ORDER_NOT_FOUND, orderId)); // 状态校验 if (order.getStatus() ! OrderStatus.PENDING_PAYMENT) { throw new BusinessException(OrderErrorCode.ORDER_STATUS_INVALID, orderId, order.getStatus(), OrderStatus.PENDING_PAYMENT); } // 外部支付调用 - 保留原始异常链 try { PaymentGatewayResponse response paymentGateway.charge( order.getPaymentId(), paymentRequest); order.setStatus(OrderStatus.PAID); orderRepository.save(order); return PaymentResult.success(response.getTransactionId()); } catch (PaymentGatewayException e) { // 保留原始异常链方便后续排查 log.error(支付网关调用失败: orderId{}, paymentId{}, orderId, order.getPaymentId(), e); throw new BusinessException(OrderErrorCode.PAYMENT_FAILED, e, orderId, e.getMessage()); } } }三、全局异常处理器的统一拦截全局异常处理器是异常体系对外的门面。它的职责是把内部异常转换为客户端可理解的 HTTP 响应同时做好日志脱敏和监控计数。/** * 全局异常处理器 * 统一拦截所有未处理的异常转换为标准化错误响应 */ RestControllerAdvice Slf4j public class GlobalExceptionHandler { private final MeterRegistry meterRegistry; private final AlertService alertService; public GlobalExceptionHandler(MeterRegistry meterRegistry, AlertService alertService) { this.meterRegistry meterRegistry; this.alertService alertService; } /** * 业务异常处理 * 返回结构化的错误信息给客户端 */ ExceptionHandler(BusinessException.class) public ResponseEntityErrorResponse handleBusinessException( BusinessException ex, WebRequest request) { ErrorCode errorCode ex.getErrorCode(); String traceId ex.getTraceId(); // 记录指标 recordExceptionMetrics(business, errorCode.getCode(), errorCode.getLevel()); // WARN 级别以上记录详细日志 if (errorCode.getLevel() ErrorLevel.WARN || errorCode.getLevel() ErrorLevel.ERROR) { log.warn(业务异常: code{}, message{}, traceId{}, path{}, errorCode.getCode(), ex.getMessage(), traceId, ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest().getRequestURI()); } // 构建标准错误响应 ErrorResponse response ErrorResponse.builder() .code(errorCode.getCode()) .message(ex.getMessage()) // 已经通过 MessageFormat 格式化 .traceId(traceId) .timestamp(ex.getTimestamp()) .path(request.getDescription(false).replace(uri, )) .build(); return ResponseEntity.status(errorCode.getHttpStatus()).body(response); } /** * 系统异常处理 * 不暴露内部细节返回通用的服务器错误 */ ExceptionHandler(SystemException.class) public ResponseEntityErrorResponse handleSystemException( SystemException ex, WebRequest request) { String traceId ex.getTraceId(); // 记录带完整堆栈的日志 log.error(系统异常: code{}, traceId{}, ex.getErrorCode(), traceId, ex); // 触发告警 alertService.sendAlert(AlertLevel.HIGH, 系统异常: %s, traceId%s.formatted(ex.getErrorCode(), traceId)); // 增加错误计数 recordExceptionMetrics(system, ex.getErrorCode(), ErrorLevel.ERROR); // 对外返回脱敏消息 ErrorResponse response ErrorResponse.builder() .code(SYS-INTERNAL-ERROR) .message(服务器内部错误请稍后重试) // 不暴露内部细节 .traceId(traceId) .timestamp(Instant.now()) .path(extractPath(request)) .build(); return ResponseEntity.status(500).body(response); } /** * 参数校验异常JSR-303 Validation */ ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntityErrorResponse handleValidationException( MethodArgumentNotValidException ex, WebRequest request) { String errorDetails ex.getBindingResult().getFieldErrors().stream() .map(fe - fe.getField() : fe.getDefaultMessage()) .collect(Collectors.joining(; )); ErrorResponse response ErrorResponse.builder() .code(PARAM-VALIDATION-ERROR) .message(请求参数校验失败: errorDetails) .traceId(MDC.get(traceId)) .timestamp(Instant.now()) .path(extractPath(request)) .build(); return ResponseEntity.status(400).body(response); } /** * 兜底处理 - 捕获所有未预见的异常 */ ExceptionHandler(Exception.class) public ResponseEntityErrorResponse handleUnknownException( Exception ex, WebRequest request) { String traceId MDC.get(traceId) ! null ? MDC.get(traceId) : UUID.randomUUID().toString(); log.error(未预期的异常: traceId{}, traceId, ex); // 强制告警 alertService.sendAlert(AlertLevel.CRITICAL, 未预期异常: %s, traceId%s.formatted(ex.getClass().getName(), traceId)); recordExceptionMetrics(unknown, ex.getClass().getSimpleName(), ErrorLevel.FATAL); ErrorResponse response ErrorResponse.builder() .code(SYS-UNKNOWN-ERROR) .message(服务器内部错误请稍后重试) .traceId(traceId) .timestamp(Instant.now()) .path(extractPath(request)) .build(); return ResponseEntity.status(500).body(response); } private void recordExceptionMetrics(String type, String code, ErrorLevel level) { try { meterRegistry.counter(application.errors, type, type, code, code, level, level.name().toLowerCase() ).increment(); } catch (Exception ignored) { // 指标记录失败不应影响异常处理主流程 } } private String extractPath(WebRequest request) { return request.getDescription(false).replace(uri, ); } }统一错误响应体/** * 标准化错误响应 */ Data Builder JsonInclude(JsonInclude.Include.NON_NULL) public class ErrorResponse { /** 错误码 */ private String code; /** 错误消息面向用户已脱敏 */ private String message; /** 链路追踪ID */ private String traceId; /** 异常时间 */ private Instant timestamp; /** 请求路径 */ private String path; /** 详细错误信息仅在开发/测试环境展示 */ JsonIgnore private String developerMessage; }四、异常链保留与日志脱敏4.1 异常链的保留原则每当你 catch 一个异常并抛出新异常时必须将原异常作为 cause 传入新异常的构造器。丢失异常链等于销毁了排查问题的线索// ❌ 错误吞掉了原始异常丢失根因 try { paymentGateway.charge(order); } catch (PaymentGatewayException e) { throw new BusinessException(OrderErrorCode.PAYMENT_FAILED, order.getId()); } // ✅ 正确保留原始异常链 try { paymentGateway.charge(order); } catch (PaymentGatewayException e) { throw new BusinessException(OrderErrorCode.PAYMENT_FAILED, e, order.getId()); }4.2 日志脱敏异常消息中可能包含敏感信息手机号、身份证、银行卡号在记录日志前需要脱敏/** * 日志脱敏工具 */ public class LogSanitizer { private static final Pattern PHONE_PATTERN Pattern.compile((1[3-9]\\d)\\d{4}(\\d{4})); private static final Pattern ID_CARD_PATTERN Pattern.compile((\\d{6})\\d{8}(\\d{4}[\\dXx])); private static final Pattern BANK_CARD_PATTERN Pattern.compile((\\d{4})\\d{8,12}(\\d{4})); /** * 对日志消息中的敏感信息脱敏 */ public static String sanitize(String message) { if (message null) return null; message PHONE_PATTERN.matcher(message).replaceAll($1****$2); message ID_CARD_PATTERN.matcher(message).replaceAll($1********$2); message BANK_CARD_PATTERN.matcher(message).replaceAll($1****$2); return message; } } /** * 脱敏日志包装器 - 在 log.error 之前自动脱敏 */ public class SanitizedLog { public static void error(Logger log, String message, Object... args) { log.error(LogSanitizer.sanitize(message), args); } }五、总结一套好的 Java 异常处理体系由四个层次构成第一层是异常分类。Checked vs Unchecked 的区分标准应该是调用方是否有能力处理而非简单的是不是业务异常。业务异常继承 RuntimeException系统异常包装为 SystemException程序 Bug 让它直接崩溃。第二层是错误码体系。错误码不是可有可无的——它是异常与监控、告警、文档之间的桥梁。错误码的设计遵循模块前缀 序号的格式绑定 HTTP 状态码和错误级别支持模板化的消息格式化。第三层是全局异常处理器。它是面向客户端的统一出口负责三件事将内部异常映射为标准化错误响应、对系统异常做消息脱敏、记录异常指标用于监控告警。第四层是异常链保留与日志脱敏。每次异常转换都必须保留 cause 链每条包含用户数据的日志都必须脱敏。这四层不是做一个就行而是缺一不可。只做全局异常处理器不做错误码体系等于提供了一个漂亮的门面但屋内一团乱只做错误码不做异常链保留等于给每个问题贴了标签但找不到根因。