第 17 章:自定义异常与最佳实践
学习目标
- 学会自定义业务异常
- 掌握统一错误码设计
- 理解企业级异常使用规范
一、为什么自定义异常?
Java 自带的异常太通用(NullPointerException、IllegalArgumentException),业务上没法区分"用户不存在"还是"密码错误"。
企业规范:业务异常继承 RuntimeException(非受检,免得满屏 throws)。
二、最简单的自定义异常
java
public class BusinessException extends RuntimeException {
private final int code; // 错误码
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
// 使用
throw new BusinessException(20001, "用户不存在");三、企业级:统一错误码枚举 + 业务异常
第 1 步:错误码枚举
java
public enum ErrorCode {
SUCCESS(0, "成功"),
PARAM_INVALID(10001, "参数校验失败"),
UNAUTHORIZED(10401, "未登录"),
FORBIDDEN(10403, "无权限"),
USER_NOT_FOUND(20001, "用户不存在"),
PASSWORD_ERROR(20003, "密码错误"),
ORDER_NOT_FOUND(30001, "订单不存在"),
SYSTEM_ERROR(50000, "系统繁忙");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
}命名规范:模块(2 位) + 类型(1 位) + 序号(2 位)。例如
20001= 用户模块(20)+ 业务错误(0)+ 第 1 个(01)。
第 2 步:业务异常类
java
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String detail) {
super(errorCode.getMessage() + ": " + detail); // 补充上下文
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}第 3 步:业务代码抛异常
java
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
}
public void login(String username, String password) {
User user = findByUsername(username);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND); // 抛业务异常
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BusinessException(ErrorCode.PASSWORD_ERROR);
}
// ...
}四、异常使用规范
| 场景 | 做法 |
|---|---|
| 参数校验失败 | 抛 BusinessException(PARAM_INVALID) |
| 资源不存在 | 抛 BusinessException(XXX_NOT_FOUND) |
| 系统异常(数据库/网络) | 让它自然抛出,日志记录,别用业务异常包 |
| 异常吞掉 | ❌ 绝对禁止 |
| catch 后只 log 不处理 | ❌ 也要禁止 |
java
// ❌ 反例:吞掉异常
try {
doSomething();
} catch (Exception e) {
e.printStackTrace(); // 只打印就完事
}
// ✅ 正例:包装后抛出
try {
doSomething();
} catch (IOException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "文件读取失败");
// 上层会拿到完整 cause 链,便于排查
}⚠️ 坑 1:catch 后只 print 不抛,等于把炸弹埋了——上层不知道出错了,数据可能不一致。
⚠️ 坑 2:业务异常继承 RuntimeException(非受检),不要继承 Exception(受检),否则所有方法都得 throws,代码会很难看。
五、本章小结
| 要点 | 关键 |
|---|---|
| 业务异常 | 继承 RuntimeException,带 code 字段 |
| 错误码枚举 | 集中管理所有错误,IDE 可一键查看 |
| 何时抛 | 参数校验、状态不符、资源不存在 |
| 何时不抛 | 系统异常(数据库/网络)不要包装 |
| catch 后必须处理 | 重新抛出 / 包装抛出 / 业务兜底 |
动手练习
- 错误码枚举:写
ErrorCode枚举,至少 5 个错误 - 业务异常类:写
BusinessException继承 RuntimeException - 服务层异常:写
UserService.login(),密码错抛BusinessException(PASSWORD_ERROR)
下一章:第 18 章:泛型 →