自定义异常类核心是错误码与异常链协同设计:错误码需分层枚举并含元信息,异常类须继承RuntimeException、支持cause传递及业务字段,严禁吞掉原始异常链,统一处理器中响应体精简而日志完整输出堆栈。

自定义异常类不是为了“造轮子”,而是让业务错误可识别、可追踪、可处理。核心在于两点:错误码与异常链的协同设计——错误码用于快速定位业务语义,异常链用于保留原始上下文(如数据库超时、网络中断等底层原因)。
错误码应分层且可枚举
避免用字符串或裸数字硬编码错误码。推荐定义统一的错误码枚举类,按模块划分,并包含描述、HTTP状态码(如适用)、是否可重试等元信息:
- USER_NOT_FOUND("U001", "用户不存在", HttpStatus.NOT_FOUND, false)
- ORDER_CREATE_FAILED("O002", "订单创建失败", HttpStatus.INTERNAL_SERVER_ERROR, true)
在自定义异常构造时直接传入枚举实例,确保错误码来源唯一、含义明确、便于国际化和日志聚合。
异常类需支持异常链与业务字段共存
继承 RuntimeException(或 Exception,视是否强制捕获而定),同时提供多参数构造器:既接收原始 cause(保持异常链),也接收错误码枚举、业务扩展字段(如订单ID、用户ID):
- public BusinessException(ErrorCode code, String... args) { ... }
- public BusinessException(ErrorCode code, Throwable cause, String... args) { super(code.getMessage(args), cause); ... }
这样既能通过 getCause() 向上追溯根本原因,又可通过 getErrorCode() 获取结构化业务标识,日志打印或网关统一封装时两者皆可利用。
避免“吞掉”原始异常链
常见反模式是在 service 层 catch 异常后仅抛出新异常却不传 cause,导致底层 SQLException 或 FeignException 丢失。务必检查每一层包装逻辑:
- DAO 层抛出 DataAccessException → Service 层应封装为 BusinessException(ORDER_DB_ERROR, e)
- 调用第三方 API 失败 → 包装时保留 FeignException 或 HttpClientErrorException 作为 cause
异常链完整,才能在排查时区分是业务规则拒绝,还是依赖服务不可用。
统一异常处理器中解耦响应与日志
在 @ControllerAdvice 中处理自定义异常时,响应体只返回错误码、消息、时间戳等必要字段;而日志中应额外打印 cause 的完整堆栈(包括 root cause):
- log.error("业务异常[{}], traceId={}", ex.getErrorCode(), traceId, ex);
- 响应 JSON 示例:{"code":"O002","msg":"订单创建失败","timestamp":"2024-06-15T10:30:00Z"}
这样前端可按 code 做友好提示,运维可通过日志快速下钻到真实故障点,无需翻查多层代码。

















