应继承RuntimeException而非Exception以避免强制处理;构造函数需提供无参、String message、String message+Throwable cause三种;抛出时应包装底层异常并保留cause;命名须带业务前缀,重写toString()便于监控识别。

继承 Exception 还是 RuntimeException?
关键看是否需要强制调用方处理。如果异常代表“程序本不该发生但可能因外部因素触发”(比如读取配置文件失败),选 Exception;如果属于“调用方逻辑错误导致”(比如传入负数给年龄字段),用 RuntimeException 子类更合适——它不强制 try-catch 或 throws,避免污染正常流程。
常见误操作:把业务校验异常(如 InvalidOrderStatusException)设计成受检异常,结果每个 service 方法都堆满 throws,反而掩盖真正需要关注的错误。
构造函数怎么写才实用?
至少提供三个构造方法:无参、带 String message、带 String message 和 Throwable cause。这样能兼容日志记录、链路追踪和嵌套异常包装。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 不要在构造函数里做耗时操作(比如查数据库、远程调用)
- message 字符串避免拼接敏感信息(如密码、token),可统一走脱敏工具
- 如果需携带额外上下文(如订单 ID、用户 ID),用字段 + getter,而不是塞进 message
示例:
public class InsufficientBalanceException extends RuntimeException {
private final String orderId;
public InsufficientBalanceException(String orderId, String message) {
super(message);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
抛出时用 throw 还是封装再抛?
直接 throw 原始自定义异常即可,但要注意两点:
- 别把底层异常(如
SQLException)原样往上抛——暴露技术细节,且违反分层隔离原则 - 需要用
cause参数包装时,确保原始异常确实有助于排查(比如连接超时、唯一约束冲突),否则只是增加日志噪音 - Spring 项目中,若在 controller 层统一用
@ExceptionHandler处理,建议所有自定义异常都继承同一个基类(如BaseBusinessException),方便集中拦截
日志和监控里怎么识别自定义异常?
异常类名本身是最重要的识别依据,所以命名要准确、带业务域前缀,比如 PaymentTimeoutException 比 BusinessException 有用得多。
容易被忽略的点:
- 没重写
toString()或getLocalizedMessage(),导致 ELK 里只看到类名,看不到关键字段值 - 监控系统(如 SkyWalking)默认只采样异常类名,不自动提取自定义字段,需手动打点或配置 extractor
- 测试时只验证了
instanceof,没覆盖getCause()链路是否完整,上线后丢失根因










