# 异常处理契约 v1.0 ## 1. 异常体系 DomainException(基类) ├── 携带 ErrorCode(枚举) ├── 携带 message(面向用户的描述) └── 可选携带 cause(原始异常) 所有业务异常必须继承 DomainException 并携带 ErrorCode。 不允许直接抛出 RuntimeException / IllegalStateException 等通用异常表达业务规则。 ## 2. ErrorCode 组织方式 按模块前缀分组(锁定): COMMON_VALIDATION_ERROR COMMON_INTERNAL_ERROR COMMON_OPTIMISTIC_LOCK_CONFLICT AUTH_INVALID_CREDENTIALS AUTH_TOKEN_EXPIRED AUTH_ACCESS_DENIED ORDER_NOT_FOUND ORDER_ALREADY_CANCELLED ORDER_STATUS_INVALID ... 命名规则:{MODULE}_{SCENE},全大写蛇形。 新增模块时在 ErrorCode 枚举中添加对应前缀组。 ## 3. ErrorCode → HTTP Status 映射表 | ErrorCode 前缀/具体值 | HTTP Status | 说明 | |----------------------|-------------|------| | COMMON_VALIDATION_ERROR | 400 | 参数校验失败 | | AUTH_INVALID_CREDENTIALS | 401 | 认证失败 | | AUTH_TOKEN_EXPIRED | 401 | 令牌过期 | | AUTH_ACCESS_DENIED | 403 | 无权限 | | *_NOT_FOUND | 404 | 资源不存在 | | *_ALREADY_* / *_STATUS_INVALID | 409 | 状态冲突 | | COMMON_OPTIMISTIC_LOCK_CONFLICT | 409 | 乐观锁冲突 | | COMMON_INTERNAL_ERROR | 500 | 系统内部错误 | ## 4. 统一错误响应格式 { "code": 404, // HTTP Status(固定,便于前端/网关) "errorCode": "ORDER_NOT_FOUND", // 稳定枚举值(便于埋点/告警/统计) "message": "订单不存在", // 面向用户,可本地化 "traceId": "abc-123-def", // 链路追踪 ID "timestamp": "2026-02-11T10:30:00Z", // 服务器时间 "details": null // 仅 VALIDATION_ERROR 时填充字段级明细 } 各字段职责: - code:前端快速判断错误大类(4xx / 5xx) - errorCode:后端精确定位错误场景,用于日志检索 / 告警规则 / 监控大盘 - message:展示给终端用户 - details:VALIDATION_ERROR 时返回 [{"field": "xxx", "message": "xxx"}] - 生产环境【禁止】在响应中返回堆栈信息 ## 5. 异常转换规则 ### 规则 1:异常只在 API 层的 GlobalExceptionHandler 转换为 HTTP 响应 - domain/application 层只抛 DomainException - infrastructure 层将技术异常包装为 DomainException: - DataAccessException → COMMON_INTERNAL_ERROR - OptimisticLockingFailureException → COMMON_OPTIMISTIC_LOCK_CONFLICT - ConnectException / TimeoutException → COMMON_INTERNAL_ERROR ### 规则 2:禁止吞没异常 - 任何 catch 块必须:要么重新抛出,要么记录 WARN/ERROR 日志 - 空 catch 块会被 SpotBugs 检测并阻断 CI ### 规则 3:参数校验 vs 业务校验 - 参数格式校验:Jakarta Validation(@NotNull, @Size, @Pattern 等), Controller 层 @Valid/@Validated 拦截,GlobalExceptionHandler 捕获 MethodArgumentNotValidException → 400 + COMMON_VALIDATION_ERROR + details - 业务规则校验:Domain 层方法内判断并抛出具体 DomainException --- ## ✅ 可自动检查项 | 规则 | 检查方式 | |------|---------| | 所有自定义异常继承 DomainException | ArchUnit: classes in ..exception.. that extend RuntimeException should also extend DomainException | | 禁止空 catch 块 | SpotBugs: DE_MIGHT_IGNORE + REC_CATCH_EXCEPTION | | GlobalExceptionHandler 存在且标注 @RestControllerAdvice | ArchUnit: 至少有一个类标注 @RestControllerAdvice 且在 ..api.exception.. 包下 | | Controller 中 @Valid/@Validated 注解存在 | 人工评审 + Prompt 约束 | ## ❌ 典型违规示例 ### 违规 1:直接抛 RuntimeException 表达业务规则 // 错误! if (order == null) { throw new RuntimeException("订单不存在"); // 无 ErrorCode,GlobalExceptionHandler 只能兜底 500 } // 正确 if (order == null) { throw new DomainException(ErrorCode.ORDER_NOT_FOUND); } ### 违规 2:Infrastructure 层不包装技术异常 // 错误!让 DataAccessException 直接穿透到 API 层 public class OrderPersistenceAdapter implements OrderPersistencePort { public Order findById(OrderId id, String customerId) { return repo.findByIdAndCustomerId(id.value(), customerId) .map(mapper::toDomain) .orElse(null); // 返回 null,让上层 NPE → 500 } } // 正确:包装为领域异常 public Order findById(OrderId id, String customerId) { return repo.findByIdAndCustomerId(id.value(), customerId) .map(mapper::toDomain) .orElseThrow(() -> new DomainException(ErrorCode.ORDER_NOT_FOUND)); }