- Add comprehensive docs/ directory with standardized structure - Include architecture specification and contract documents - Add prompt templates for development workflows - Establish M1 deliverables as frozen specifications Co-authored-by: Qwen-Coder <[email protected]>
4.6 KiB
异常处理契约 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)); }