Java自定义业务异常应继承RuntimeException,封装错误码、消息、详情等结构化字段,保留原始异常链,并通过全局处理器统一返回精简JSON,避免暴露敏感信息。

Java 中自定义业务异常类,本质是把“业务语义”和“错误上下文”封装进一个对象,而不是简单抛出 RuntimeException 或 IllegalArgumentException。关键不在“抛”,而在“怎么封装、怎么传递、怎么用”。
明确继承 RuntimeException,避免强制捕获
业务异常属于运行时异常,不应强迫调用方写 try-catch。直接继承 RuntimeException 即可:
- 不打断正常代码流,符合 REST API 的失败响应习惯
- 避免在 Service 层堆砌大量无意义的 try-catch
- 与 Spring 的声明式事务兼容(默认对 RuntimeException 回滚)
携带结构化元数据:错误码 + 消息 + 可选扩展字段
仅靠 message 字符串无法支撑前端提示、日志分类、监控告警。推荐在异常类中固化以下字段:
-
code:字符串或整型错误码(如
"USER_NOT_FOUND"或40401),便于前端 switch 分支处理 -
message:面向用户/开发者的可读描述(支持 i18n 占位符,如
"用户 {id} 不存在") -
details(可选):Map<String, Object> 类型,存入上下文数据(如
{"userId": 123, "requestId": "req-abc"}) - timestamp(可选):记录异常发生时刻,辅助链路追踪
构造时建议优先使用枚举(如 RespStatus.USER_NOT_FOUND),保证 code 和 message 一致性,避免硬编码散落。
立即学习“Java免费学习笔记(深入)”;
必须保留原始异常链(cause)
业务异常不是终点,而是错误信息的“翻译层”。DAO 抛出 SQLException、Feign 抛出 HttpClientErrorException,都应作为 cause 传入业务异常:
- 构造函数中显式接收
Throwable cause参数 - Service 层 catch 后,必须用
new BusinessException(code, msg, e)包装再抛出 - 全局异常处理器中,日志打印需带 cause:
log.error("业务异常[{}]", ex.getCode(), ex)(注意参数顺序,第三个参数才是异常对象)
这样排查问题时,既能一眼看到业务含义(“库存不足”),又能顺藤摸到根因(“MySQL Deadlock found”)。
配合全局处理器统一输出格式
在 @RestControllerAdvice 中拦截自定义异常,只返回精简、协议友好的 JSON:
- 响应体仅含
code、msg、timestamp(或requestId) - 不暴露堆栈、不返回 cause 的 toString(),防止敏感信息泄露
- 其他异常(如空指针、NPE)走兜底逻辑,返回通用 500 错误
前端收到 {"code":"ORDER_TIMEOUT","msg":"订单已超时,请重新下单"},就能精准展示提示,无需解析模糊的 “Internal Server Error”。


















