自定义异常
概述
当标准异常(如 IllegalArgumentException、IOException)无法准确表达业务含义时,可以自定义异常类。自定义异常能传达领域语义(如「用户不存在」「订单状态非法」)、统一错误码或上下文信息,并可通过异常链保留底层原因,便于排查。本文说明如何正确设计受检/非受检自定义异常、构造方法约定以及常见实践。
何时需要自定义异常
- 业务语义明确:如
UserNotFoundException、InsufficientBalanceException,比通用的IllegalArgumentException更易读、易处理。 - 统一错误码或上下文:在异常中封装错误码、字段名、业务 ID 等,便于上层做国际化或统一响应。
- 保留异常链:包装底层异常(如 IO、SQL)时,通过 cause 传递根因,不丢失堆栈信息。
若标准异常已能表达含义(如参数不合法用 IllegalArgumentException),不必强行自定义;过度细分异常类会增加调用方的 catch 负担。
提示
自定义异常前先确认 异常体系:需要调用方必须处理的用受检异常(继承 Exception);表示编程错误或不可恢复状态、可不强制处理的用非受检异常(继承 RuntimeException)。
基本语法:继承 Exception 或 RuntimeException
- 受检异常:继承
Exception(不要带RuntimeException的父类)。方法抛出后,调用方必须 try-catch 或 throws。 - 非受检异常:继承
RuntimeException。编译器不强制处理,适合前置条件不满足、非法状态等。
不要继承 Error:Error 表示 JVM/系统级严重错误,业务异常应归入 Exception 体系。
// 受检异常:调用方必须处理(如「文件格式不支持」)
public class UnsupportedFormatException extends Exception {
public UnsupportedFormatException(String message) {
super(message);
}
}
// 非受检异常:可不强制处理(如「用户不存在」由上层统一处理)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}构造方法约定
Throwable 及其子类常用以下形式的构造方法,自定义异常应至少提供「消息」和「原因」两种,以便日志与异常链完整。
| 构造方法 | 说明 |
|---|---|
| 无参 | 使用较少,消息可由子类固定 |
(String message) | 最常用,携带错误描述 |
(String message, Throwable cause) | 包装底层异常,保留原因链 |
(Throwable cause) | 仅包装原因,消息可用 cause.getMessage() |
重写时用 super(message)、super(cause)、super(message, cause) 传给父类,不要吞掉 cause。
示例:标准自定义异常类
/**
* 业务含义:用户不存在。
* 继承 RuntimeException,调用方可不强制 try-catch,由上层或全局异常处理器统一处理。
*/
public class UserNotFoundException extends RuntimeException {
private final String userId; // 可选:携带业务上下文,便于日志与接口返回
public UserNotFoundException(String message) {
super(message);
this.userId = null;
}
public UserNotFoundException(String message, String userId) {
super(message);
this.userId = userId;
}
public UserNotFoundException(String message, Throwable cause) {
super(message, cause);
this.userId = null;
}
public String getUserId() {
return userId;
}
}使用示例
示例 1:非受检业务异常(推荐用于「查无此人」类错误)
public class UserService {
public User findById(String id) {
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException("用户不存在: " + id, id);
}
return user;
}
}
// 调用方:可不捕获,由上层或全局异常处理器统一返回 404
// 或按需捕获做分支逻辑
try {
User user = userService.findById("U001");
return user;
} catch (UserNotFoundException e) {
return ResponseEntity.status(404).body(e.getMessage());
}示例 2:受检异常(调用方必须处理)
适用于「可恢复或调用方必须知晓」的场景,如配置缺失、格式不支持等。
public class InvalidConfigException extends Exception {
public InvalidConfigException(String message) {
super(message);
}
public InvalidConfigException(String message, Throwable cause) {
super(message, cause);
}
}
// 使用:调用方必须 try-catch 或 throws
public void loadConfig(String path) throws InvalidConfigException {
try {
String content = Files.readString(Path.of(path));
parseConfig(content);
} catch (IOException e) {
throw new InvalidConfigException("无法读取配置文件: " + path, e);
}
}示例 3:异常链(保留 cause)
包装底层异常时,务必使用带 cause 的构造方法,便于排查根因。
try {
return jdbcTemplate.queryForObject(sql, rowMapper, id);
} catch (DataAccessException e) {
// 包装为业务异常,保留底层 SQL/连接异常信息
throw new OrderQueryException("查询订单失败, id=" + id, e);
}捕获方可通过 getCause() 或 printStackTrace() 看到完整链式堆栈。
进阶用法
错误码与统一响应
在异常中封装错误码,便于 REST 接口或前端统一处理。
public class BusinessException extends RuntimeException {
private final String code; // 如 "USER_NOT_FOUND", "INSUFFICIENT_BALANCE"
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
// 使用
throw new BusinessException("INSUFFICIENT_BALANCE", "余额不足,当前余额: " + balance);序列化(可选)
若异常需要跨 JVM 传递(如 RPC、分布式调用),应实现 Serializable 并显式声明 serialVersionUID,避免版本变更导致反序列化失败。
public class RemoteServiceException extends Exception implements java.io.Serializable {
private static final long serialVersionUID = 1L;
public RemoteServiceException(String message) {
super(message);
}
}说明
仅当异常实例会跨进程/网络传输时才需考虑序列化;多数 Web 应用在单机内抛出、由全局异常处理器转为 JSON 响应,不必须实现 Serializable。
注意事项
注意
受检 vs 非受检:若希望调用方必须处理(如重试、降级、提示用户),用受检异常(继承 Exception);若表示编程错误或由上层统一处理即可,用非受检异常(继承 RuntimeException)。不要为了「少写 throws」而把所有自定义异常都做成 RuntimeException,否则调用方容易忽略。
注意
务必保留异常链:包装底层异常时使用 super(message, cause),不要 new MyException(message) 而丢弃 cause,否则排查问题时难以定位根因。
不要继承 Error
业务异常一律继承 Exception 或 RuntimeException,不要继承 Error。Error 留给 JVM 与系统级错误使用。
- 命名:类名应能表达业务含义,通常以
Exception结尾,如UserNotFoundException、InvalidOrderStateException。 - 避免过度细分:同一模块内异常类不宜过多,否则调用方需要 catch 大量类型;可适当用「错误码 + 通用业务异常」替代过多子类。