应定义特定业务异常类而非直接throw RuntimeException,因其能区分系统错误与业务拒绝,支持统一错误码、用户提示、调试详情及HTTP状态码,并通过@ControllerAdvice统一处理,避免污染API和破坏模块边界。

为什么不能直接 throw new RuntimeException("订单不存在")
业务异常不是程序 bug,混用 RuntimeException 会导致调用方无法区分「系统崩溃」和「业务拒绝」。比如支付接口返回 500 还是 400,前端重试策略、监控告警、日志分级都依赖这个边界。一旦所有异常都塞进 RuntimeException,下游只能靠字符串匹配错误信息,脆弱且不可维护。
实操建议:
- 为每类业务场景定义独立的异常类,如
OrderNotFoundException、InsufficientBalanceException - 继承
RuntimeException(非检查异常),避免强制 try-catch 干扰正常流程 - 构造函数至少支持
String message和String code两个参数,code 用于统一错误码体系(如 "ORDER_NOT_FOUND") - 不要在异常类里放业务逻辑方法(如
getSuggestAction()),它只负责携带上下文
自定义异常该带哪些字段才够用
光有 message 和 code 不够。真实场景中,前端需要提示语,运营需要查问题,风控需要原始参数——这些都得靠异常体本身透出,而不是靠日志拼接或全局上下文隐式传递。
推荐字段组合:
立即学习“Java免费学习笔记(深入)”;
-
code:静态错误码(大写+下划线),如"PAY_AMOUNT_MISMATCH" -
message:面向用户的提示(可含占位符,如"支付金额不匹配:期望{expected},收到{actual}") -
details:Map类型,存原始请求 ID、订单号、金额等关键调试字段 -
httpStatus:int 类型,默认 400,特殊场景可设 401/403/422/500
示例:
public class PaymentValidationException extends RuntimeException {
private final String code;
private final Map details;
private final int httpStatus;
public PaymentValidationException(String code, String message, Map details) {
super(message);
this.code = code;
this.details = Collections.unmodifiableMap(details);
this.httpStatus = 422;
}
}
如何让 Controller 层自动转换异常为标准 JSON 响应
每个接口都写 try-catch 太重复,Spring 的 @ControllerAdvice + @ExceptionHandler 是标准解法,但要注意细节。
关键点:
- 异常处理器必须声明具体类型,别用
Exception.class捕获所有——否则会吞掉空指针、数组越界等真正需要告警的错误 - 响应体结构要统一,建议封装为
Result>或ErrorResponse,包含code、message、timestamp、path、details - 对
details字段做白名单过滤,避免把敏感字段(如用户密码、密钥)直接透出 - 记录日志时,用
logger.warn("Business exception: {}", ex.getMessage(), ex),保留堆栈但不打印全量details
什么时候该用 checked 异常而不是 runtime 异常
Java 的 checked 异常本质是编译器强制的契约。只有当调用方「必须」处理该异常才能继续执行时才用,比如 IO、DB 连接失败——你没法绕过重连或降级。
业务异常几乎从不用 checked,因为:
- 下单失败,调用方不会重试三次再抛异常;它会直接返回错误给前端
- 加了
throws XxxException后,所有中间层都要声明或捕获,污染 API 签名 - Feign、Dubbo 等 RPC 框架对 checked 异常序列化支持差,容易变成
UndeclaredThrowableException
唯一例外:领域模型内部强校验,比如 Money.of(BigDecimal amount) 要求金额 ≥ 0,此时可定义 IllegalAmountException extends IllegalArgumentException,属于构造约束而非业务流程分支。
最易被忽略的一点:异常类本身要放在 common 或 domain 模块,不能放在 web 层;否则 service 模块引用了 controller 相关包,模块边界就破了。










