
关于业务异常应该继承RuntimeException还是Exception,这个看似“习惯”的问题,实则直接关系到Spring事务的默认行为与工程设计的简洁性。
结论先行:业务异常必须继承RuntimeException。这不是编码风格之争,而是关乎事务一致性与代码可维护性的工程原则。
一、为什么业务异常不该继承 Exception
如果你像下面这样定义业务异常:
public class BizException extends Exception {
}
看起来似乎合情合理,但它会立刻给你的项目带来两个棘手的后果。
1. 事务默认不会回滚
Spring 对声明式事务有一个默认规则:
RuntimeException 及其子类 → 自动回滚
Exception 及其子类(受检异常) → 不回滚
这意味着,当你的 Service 方法如下时:
@Transactional
public void create() throws BizException {
// ... 一些数据库操作
throw new BizException("库存不足");
}
已经执行的数据操作将不会回滚。 为了解决这个问题,你不得不做出选择:
- 在每个
@Transactional注解中手动指定 rollbackFor = BizException.class
- 或者在代码中到处编写 try-catch 块来手动回滚
无论哪种方式,都直接增加了工程的复杂度和维护成本。
2. 调用链被迫“显式处理”
受检异常要求方法签名必须显式声明throws。于是,异常传递变成了这样:
- Service 层抛出
BizException
- Controller 层必须捕获或继续声明
throws
- 调用链上的每一层都不得不处理这个语法负担
最终,原本用于表达业务规则的异常,却沦为了污染方法签名、增加模板代码的“语法噪音”。

二、继承 RuntimeException 带来的工程收益
正确的定义方式应该如下:
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}
这样做的好处是直接且显著的:
- 事务默认回滚:符合 Spring 的默认约定,无需额外配置。
- 不污染方法签名:方法无需声明
throws,调用链简洁。
- 可随时抛出:在业务逻辑的任何地方,一旦发现状态不合法,即可直接抛出。
再配合一个全局异常处理器(例如使用 Spring MVC 的 @ExceptionHandler),收益会更大:
@ExceptionHandler(BizException.class)
public Result<?> handle(BizException e) {
return Result.fail(e.getMessage());
}
通过这种异常处理机制,业务异常的语义、事务回滚逻辑和HTTP响应格式被彻底解耦。业务代码只需关注核心逻辑和异常抛出,其余工作由框架统一接管。

三、什么时候才考虑使用受检异常
那么,受检异常 (Exception) 就毫无用处了吗?并非如此。它适用于那些调用方“必须”且有能力处理的异常场景,例如:
- IO 操作(文件不存在、读写权限问题)
- 网络通信(连接超时、协议错误)
- 外部系统调用(第三方API返回约定外的错误)
然而,关键在于,这些通常不属于“业务异常”的范畴。
业务异常的本质,是用于表达业务流程中的非法状态或规则冲突,例如:
- 参数校验不合法
- 业务状态不允许执行当前操作
- 领域规则不满足(如库存不足、余额不够)
对于这类异常,它们应该具备以下特点:
- 可以随时抛出,不受方法签名约束
- 能够自动触发事务回滚,保证数据一致性
- 由框架统一处理并转换为用户友好的提示
一句话总结:业务异常是一种流程控制手段,而非对调用方的强制约束。 将其设计为非受检异常(RuntimeException),正是为了匹配这一核心定位。
希望这次的梳理能帮你理清思路。关于Java工程实践中的更多设计模式与最佳实践,欢迎在云栈社区与更多开发者交流探讨。
