自定义异常类名必须以exception结尾,且需提供4个标准构造器,显式声明serialversionuid,并根据业务重要性决定是否在方法签名中throws。

自定义异常类名必须以 Exception 结尾
Java 的异常体系依赖命名约定来传递语义,IDE、静态检查工具(如 SonarQube)和团队协作都默认这个规则。不以 Exception 结尾的类,即使继承了 Exception 或 RuntimeException,也会被当成普通类处理——比如 Spring 的 @ExceptionHandler 无法自动匹配,Mockito 模拟时也容易漏掉。
- 正确命名:
OrderNotFoundException、PaymentValidationException - 错误命名:
InvalidOrder、OrderError(哪怕继承了RuntimeException) - 检查手段:在 IDE 中 Ctrl+Click 进入异常使用处,看是否能跳转到你定义的类;或用
mvn compile后检查编译警告(部分插件会报“non-exception class used in throws”)
构造器必须至少保留 Throwable 的所有标准重载
不重载全构造器,会导致调用方传入 message + cause 时抛 NoSuchMethodError——尤其在框架内部(如 Spring MVC 参数解析失败、MyBatis 执行 SQL 出错后包装异常)经常隐式调用 new XxxException(String, Throwable)。
- 必须提供的构造器(4 个):
public MyException()public MyException(String message)public MyException(Throwable cause)public MyException(String message, Throwable cause) - 额外建议:如果异常需携带业务字段(如订单号),可加一个带参数的构造器,但不要删减上面四个
- 常见坑:只写了
MyException(String),上线后某次数据库连接超时被包装成MyException("DB timeout", e),直接 ClassLoad 失败
serialVersionUID 不是可选项,而是兼容性开关
只要异常类可能被序列化(网络传输、日志落盘、分布式链路追踪中跨 JVM 抛出),就必须显式声明 serialVersionUID。JVM 默认生成的值基于类结构哈希,字段增删、方法重载、甚至 JDK 版本升级都可能导致哈希变化,反序列化时抛 InvalidClassException。
- 写法统一用
private static final long serialVersionUID = 1L;(不用随机数,方便版本控制) - 何时必须改:增加非 transient 字段、修改已有字段类型、删除字段——改完要同步更新
serialVersionUID值(比如改成2L) - 验证方式:用
ObjectOutputStream序列化一次旧版对象,再用新版类反序列化,看是否报错
RuntimeException 子类要不要写 throws 声明
要不要在方法签名里写 throws MyBusinessException,取决于它是否属于「调用方必须决策」的异常。Spring 官方文档明确说:只有 checked exception 才强制要求声明;但实际协作中,不声明会让调用方误以为“这个异常不会发生”,结果线上突然炸了。
立即学习“Java免费学习笔记(深入)”;
- 推荐做法:哪怕继承
RuntimeException,也在 Javadoc 里写明@throws MyBusinessException,并在关键路径(如支付、库存扣减)的方法签名中显式throws - 反例:
public void deductStock(Order order) { ... }看似干净,但一旦StockInsufficientException抛出,上游完全没做兜底 - 权衡点:高频、纯校验类异常(如
ParamValidationException)可不声明;涉及状态变更、外部依赖失败的,必须声明或文档强约束
真正麻烦的不是写几个构造器,而是当异常从 Dubbo 接口飞到另一个服务、又被 Sentry 截获、再经 ELK 聚合分析时,类名不规范、serialVersionUID 缺失、或者 cause 链在中间某层被吃掉——这时候你翻三天堆栈,才发现最初那个 OrderNotFoundExc(少了个 e)才是根因。










