继承 runtimeexception 是因为其为非检查异常,调用方无需强制处理,便于业务异常向上冒泡并由 controller 统一兜底;自定义 businessexception 至少需 errorcode、message 和可选扩展参数,推荐 string 类型 errorcode 与两种构造函数;spring boot 中用 @controlleradvice + @exceptionhandler 返回 responseentity 实现统一 json 错误响应,http 状态码应语义化(如 404/403/409),异常消息避免硬拼接,应分离错误码与参数以支持国际化和演进。

为什么继承 RuntimeException 而不是 Exception
因为 RuntimeException 是非检查异常(unchecked),调用方不用强制 try-catch 或 throws,业务代码更干净。比如服务层抛 BusinessException,Controller 统一捕获并转成 HTTP 错误响应,中间的 service、dao 层完全不用写异常处理逻辑。
如果继承 Exception,每层都得声明或捕获,违背“业务异常应向上冒泡、由统一入口兜底”的设计意图。
自定义 BusinessException 的最小必要字段
一个实用的业务异常至少要携带:错误码(errorCode)、提示语(message)、可选的扩展参数(如订单号、用户 ID)。不建议塞太多字段——日志上下文靠 MDC 补,堆栈靠父类自带,序列化靠框架自动处理。
-
errorCode用String类型,方便区分系统级(如"SYS_001")和业务级(如"ORDER_NOT_FOUND") - 构造函数优先提供
(String errorCode, String message)和(String errorCode, String message, Throwable cause)两种 - 避免重写
getMessage()—— 它会被日志框架直接调用,改了反而干扰排查
Spring Boot 中如何统一捕获并返回 JSON 错误响应
用 @ControllerAdvice + @ExceptionHandler 拦住所有 BusinessException,避免每个 Controller 都写重复逻辑。
注意两点:
- 确保
@ExceptionHandler(BusinessException.class)方法返回的是ResponseEntity>或带@ResponseBody的对象,否则可能被视作视图名 - 不要在 handler 里手动
log.error("", ex)——BusinessException本身不含敏感数据,但堆栈会泄露实现细节;真要打日志,用log.warn("业务异常: {}", ex.getMessage(), ex),既留痕又不泄密 - HTTP 状态码别硬写
400,按语义选:404(资源不存在)、403(权限不足)、409(冲突)更准确
容易被忽略的坑:异常消息拼接与国际化
别在抛异常时用 String.format("订单 %s 不存在", orderId) 拼消息 —— 这会让前端无法做 i18n,也难做错误码聚合统计。
正确做法是把变量作为独立字段传入异常实例,再由统一响应处理器结合 locale 渲染最终提示:
throw new BusinessException("ORDER_NOT_FOUND", "订单不存在", Map.of("orderId", orderId));
这样后端能按需格式化,前端也能拿到原始码和参数自行翻译。如果项目没上 i18n,至少保留参数结构,为后续演进留余地。











