为什么需要自定义异常
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 {
public BusinessException(String message) { super(message); }
public BusinessException(String message, Throwable cause) { super(message, cause); }
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
|
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; } }
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
| public User findUser(Long id) { User user = userDao.findById(id); if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND, "ID=" + id); } return user; }
@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()); }
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); }
|
坑2:异常类太多,每个业务场景一个异常类
不需要 UserNotFoundException、OrderNotFoundException、ProductNotFoundException 各一个
一个 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