Java自定业务异常CustomException怎么编写与规范应用
自定义业务异常应继承Exception以强制调用方处理,类名以Exception结尾并包含message和cause构造器,仅在业务规则被违反时抛出。统一在Service层抛出,Controller层捕获并转译为标准响应,配合全局异常处理器收敛处理,避免技术异常滥用。
自定义业务异常的核心价值,不在于“能不能抛出一个异常”,而在于让异常真正承载业务语义,从而把业务错误和系统错误彻底区分开。与其简单包一层 RuntimeException,不如让异常自己说明白:为什么抛、谁来管、怎么恢复。

下面直接拆解四个关键环节,把规范写法和实际套路说清楚。
一、CustomException 的标准编写方式
为什么要求必须继承 Exception 而非 RuntimeException?就是为了强制调用方不能装看不见——技术上需要显式 try-catch 或 throws 声明,业务上则体现异常的严肃性。几个基本规范:
- 类名以 Exception 结尾,比如 OrderNotPayedException,一看就知道是什么问题。
- 至少提供两个构造器:一个只带 message,另一个带 message 和 cause,给上层足够的诊断信息。
- 可以附带业务字段(如 errorCode、httpStatus),但别过度设计——字段越多,维护成本越高,够用就好。
示例:
public class InsufficientBalanceException extends Exception {
private final String accountId;
private final BigDecimal requiredAmount;
public InsufficientBalanceException(String accountId, BigDecimal requiredAmount) {
super("账户余额不足:accountId=" + accountId + ", required=" + requiredAmount);
this.accountId = accountId;
this.requiredAmount = requiredAmount;
}
public InsufficientBalanceException(String accountId, BigDecimal requiredAmount, Throwable cause) {
super("账户余额不足:accountId=" + accountId + ", required=" + requiredAmount, cause);
this.accountId = accountId;
this.requiredAmount = requiredAmount;
}
// getter 省略
}
二、只在业务规则被违反时抛出
这一点特别容易跑偏。CustomException 不是日志的替代品,更不是流程控制的工具。它只应该在“输入合法、参数正确,但业务本身无法继续”时才登场:
- ✅ 正确用法:下单时库存为0、支付订单重复提交、会员等级不满足折扣条件——这些都是业务规则决定的,必须用自定义异常说清楚。
- ❌ 错误用法:数据库连接失败(应该抛 SQLException 或 DataAccessException)、JSON 解析失败(应该抛 JsonProcessingException)、NPE(应该修代码,别抛异常糊弄)。
还有一个常见雷区:有人习惯用 CustomException 包裹技术异常再往上抛——这本质上是在掩盖问题根源,让运维和排查变得一团乱。技术异常就该保持技术异常的类型,业务异常才用自定义类,各司其职。
三、统一在 Service 层抛出,Controller 层捕获并转译
分层的重要职责之一是“谁发现问题谁抛出,谁接触外部谁处理”。具体来说:
- Service 方法内部检测到业务规则不满足 → 直接 new 并 throw CustomException。
- Controller 捕获到这个异常 → 转为标准化的响应体(比如统一 Result
结构),同时设置对应的 HTTP 状态码(如 400 Bad Request)和业务错误码。 - 切忌在 DAO 层或工具类里抛 CustomException,也不要在 Controller 里 new 一个再 throw——前者破坏了抽象层次,后者把校验和转译混在一起,后面很难复用。
这样设计的好处很明显:Service 可以被不同调用方(RPC、MQ消费者、定时任务)共用,每个调用方收到的都是明确的异常,由各自的上层决定怎么响应。前端也能根据 errorCode 做差异化提示,而不是统一弹个“系统繁忙”。
四、配合全局异常处理器做收敛处理
如果每个 Controller 都得写 try-catch 处理自定义异常,那代码会变得又臭又长。用 @ControllerAdvice + @ExceptionHandler 统一收割所有 CustomException 子类才是正解:
- 从异常对象中提取 errorCode、message、httpStatus,组装成统一的响应体返回。
- 日志记录时带上 traceId、errorCode 和关键参数,但不需要打完整堆栈——业务异常本身不需要 full stack,打了反而增加噪音。
- 对不同子类可以定制响应逻辑,比如 OrderTimeoutException 可以额外返回倒计时信息给前端。
这个机制既保证了代码整洁,又防止了业务异常意外穿透到前端直接显示堆栈——那种场面,用户看了只会一脸懵。
