自定义异常

为什么需要自定义异常

Java内置的异常就那么几种,不够用来表达业务含义

比如”用户余额不足”、”订单已过期”、”验证码错误”——你用 RuntimeException 当然能跑,但看到异常类名你根本不知道出了啥事

自定义异常就是给你的业务问题起个专属名字,一看就知道怎么回事

就像医院里”感冒”和”肺炎”都是病,但你总不能都叫”生病了”吧

怎么写自定义异常

两种选择

继承 Exception → 受检异常(编译器强制处理)

继承 RuntimeException → 非受检异常(编译器不管)

基本模板

1
2
3
4
5
6
7
8
9
10
11
12
13
// 受检异常版本
public class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}

// 非受检异常版本(推荐)
public class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String message) {
super(message);
}
}

完整版模板(企业级写法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BusinessException extends RuntimeException {

// 构造方法1:只传消息
public BusinessException(String message) {
super(message);
}

// 构造方法2:传消息 + 原始异常(保留异常链)
public BusinessException(String message, Throwable cause) {
super(message, cause);
}

// 构造方法3:只传原始异常
public BusinessException(Throwable cause) {
super(cause);
}
}

一般至少提供两个构造方法:一个只传message,一个传message+cause

实际建议:继承RuntimeException

为什么推荐非受检异常?

受检异常会”污染”方法签名,每个调用者都得写 throws 或 try-catch,代码很烦琐

Spring框架的异常几乎全是RuntimeException

现代Java开发的趋势是用非受检异常表达业务错误

什么时候用受检异常?

当你强制要求调用者处理这个异常时(非常少见)

比如银行转账,你希望调用者必须处理转账失败的情况

选择 适用场景 代表
继承 RuntimeException 业务异常、参数校验失败 Spring的各种异常
继承 Exception 强制调用者必须处理的情况 Java IO相关异常

带错误码的自定义异常(企业级)

实际项目中,异常不光要有描述信息,还要有错误码,方便前后端联调和排查问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 业务异常 —— 带错误码
* 配合统一返回结果 Result 使用
*/
public class BusinessException extends RuntimeException {

private final int code; // 错误码

public BusinessException(int code, String message) {
super(message);
this.code = code;
}

public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}

public int getCode() {
return code;
}
}

配合错误码枚举使用(更规范)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 错误码枚举
public enum ErrorCode {
USER_NOT_FOUND(1001, "用户不存在"),
BALANCE_NOT_ENOUGH(1002, "余额不足"),
ORDER_EXPIRED(1003, "订单已过期"),
PARAM_INVALID(1004, "参数校验失败"),
SYSTEM_ERROR(9999, "系统内部错误");

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; }
}

// 改进后的BusinessException
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; }
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Service层抛异常
public User findUser(Long id) {
User user = userDao.findById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND, "ID=" + id);
}
return user;
}

// 全局异常处理器(Spring项目)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusiness(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
}

完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Test
public void testCustomException() {
System.out.println("=== 自定义异常演示 ===");

// 场景:用户下单,余额不足
try {
placeOrder("lucy", 100.0, 9999.0);
} catch (BusinessException e) {
System.out.println("错误码:" + e.getCode());
System.out.println("错误信息:" + e.getMessage());
// 输出:
// 错误码:1002
// 错误信息:余额不足:当前余额100.0,需要9999.0
}

// 场景:查找用户,用户不存在
try {
findUser(null);
} catch (BusinessException e) {
System.out.println("错误码:" + e.getCode());
System.out.println("错误信息:" + e.getMessage());
}
}

public void placeOrder(String userId, double balance, double amount) {
if (amount > balance) {
throw new BusinessException(1002,
"余额不足:当前余额" + balance + ",需要" + amount);
}
System.out.println("下单成功!");
}

public User findUser(String id) {
if (id == null || id.isEmpty()) {
throw new BusinessException(1001, "用户ID不能为空");
}
// 模拟查询...
return null;
}

常见坑

坑1:自定义异常没有提供带cause的构造方法

当你在catch块里包装异常时,必须把原始异常传进去,不然堆栈信息就断了

1
2
3
4
5
6
7
8
9
10
// ❌ 原始异常丢了
catch (SQLException e) {
throw new BusinessException(9999, "数据库操作失败");
}

// ✅ 保留原始异常
catch (SQLException e) {
throw new BusinessException(9999, "数据库操作失败", e);
}
// 所以你的自定义异常一定要有 (String message, Throwable cause) 构造方法

坑2:异常类太多,每个业务场景一个异常类

不需要 UserNotFoundExceptionOrderNotFoundExceptionProductNotFoundException 各一个

一个 BusinessException + 错误码枚举就够了,用错误码区分不同业务

坑3:在异常的message里放敏感信息

异常信息可能会返回给前端或写进日志,别把密码、身份证号这些放进去

坑4:忘记serialVersionUID

异常类本质是可序列化的(继承了Throwable,Throwable实现了 序列化与Serializable

虽然不加也能跑,但规范的做法是加上:private static final long serialVersionUID = 1L;

练习题

题1:写一个自定义异常 AgeOutOfRangeException,表示年龄超出合理范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AgeOutOfRangeException extends RuntimeException {
public AgeOutOfRangeException(String message) {
super(message);
}
public AgeOutOfRangeException(String message, Throwable cause) {
super(message, cause);
}
}

// 使用
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new AgeOutOfRangeException("年龄必须在0-150之间,当前值:" + age);
}
}

题2:设计一个带错误码的异常体系,包含以下场景

用户名已存在(2001)

密码强度不够(2002)

登录失败次数过多(2003)

提示:用ErrorCode枚举 + BusinessException

题3:受检异常和非受检异常的自定义异常分别怎么写?各自适用什么场景?

受检:extends Exception,适合强制调用者处理的场景

非受检:extends RuntimeException,适合业务异常、参数校验等场景

相关笔记

异常体系 | try-catch-finally | throw与throws | 序列化与Serializable


上一章 目录 下一章
throw与throws java基础 序列化与Serializable