自定义异常应在调用方需差异化处理或监控需区分业务错误时使用,继承RuntimeException为主,配合@ControllerAdvice和@ResponseStatus实现统一响应。

Java里不是“必须”用自定义异常,而是当内置异常(如 IllegalArgumentException、RuntimeException)无法准确表达业务语义或无法支撑错误处理流程时,才需要自定义。
什么时候该写自己的异常类
核心判断标准是:调用方是否需要根据异常类型做差异化处理,或者日志/监控是否需要区分业务错误维度。
- 用户余额不足和订单已取消,都可能抛
RuntimeException,但下游系统要分别触发短信提醒或重试逻辑——这时就得用InsufficientBalanceException和OrderCancelledException - 接口返回码要映射到具体业务错误(如 40001 表示“优惠券已过期”,40002 表示“库存不足”),靠
message字符串解析不可靠,必须靠异常类型识别 - 框架(如 Spring)的
@ExceptionHandler按异常类型注册处理器,没有自定义类就只能写一堆if (e.getMessage().contains("xxx"))
继承 RuntimeException 还是 Exception
绝大多数业务异常应继承 RuntimeException,即 unchecked 异常;只有极少数需强制调用方处理的场景才用 Exception。
- HTTP 接口层的参数校验失败、权限拒绝、资源不存在——这些是“预期内”的业务失败,不是程序 bug,不应强迫上层
try-catch,用RuntimeException子类更合理 - 如果某个异常代表“必须恢复的外部依赖故障”(如调用银行支付网关超时且不允许降级),才考虑 checked 异常,否则会污染大量无关代码的
throws声明 - Spring 默认把所有
RuntimeException转成 500,若想统一返回 4xx,得配合全局异常处理器 + 状态码注解(如@ResponseStatus(HttpStatus.BAD_REQUEST))
自定义异常里该放什么字段
除了继承标准构造函数,建议只加一个 errorCode 字段,其他都通过构造函数传参控制,避免膨胀。
立即学习“Java免费学习笔记(深入)”;
- 不要在异常类里塞
timestamp、requestId、traceId——这些属于日志上下文,应在捕获异常时由日志框架(如 Logback MDC)注入 - 不要重写
getMessage()拼接业务数据,会导致序列化/远程传输时丢失结构;用toString()或额外提供getDetail()方法更安全 - 推荐模板写法:
public class CouponExpiredException extends RuntimeException {
private final String errorCode = "COUPON_EXPIRED";
public CouponExpiredException(String couponId) {
super("Coupon " + couponId + " is expired");
}
}
Spring 中怎么让自定义异常生效
光定义类没用,得让框架识别并正确响应。关键在两处:全局处理器 + HTTP 状态码对齐。
- 用
@ControllerAdvice+@ExceptionHandler捕获自定义异常,避免每个 Controller 都写重复逻辑 - 别依赖
message内容做判断,Spring 支持直接按类型匹配:@ExceptionHandler(CouponExpiredException.class) - 如果多个异常要返回相同状态码(如都是 400),可统一继承一个基类并用
@ResponseStatus注解在基类上;若需不同状态码,就在各子类上单独标注 - 注意:Spring Boot 2.3+ 默认禁用错误页面(
BasicErrorController),自定义异常若没配处理器,会直接回退到 Whitelabel 页面,容易误以为“没生效”
真正难的不是写几个 extends RuntimeException,而是整个团队对“哪些错误算业务异常、哪些算系统故障”有共识,并在分层(Controller / Service / DAO)中保持异常抛出位置的一致性——比如 Service 层绝不 throw HTTP 相关异常,Controller 才决定怎么转成响应体。










