应为每类业务失败定义语义化异常类,如insufficientbalanceexception,继承runtimeexception,强制传入关键字段,提供getter而非重写getmessage(),关联错误码枚举,日志用占位符记录异常对象。

Java异常该不该带业务上下文
不该只抛 RuntimeException 或空泛的 IllegalArgumentException,除非你确定调用方能从堆栈里反推“用户没填手机号”还是“订单已过期”。语义化异常的本质,是把「哪里错了」和「为什么错」塞进异常类型本身,而不是靠日志拼字符串。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 为每类业务失败定义专属异常类,比如
InsufficientBalanceException、PhoneNumberNotVerifiedException,继承自RuntimeException(非检查异常更轻量) - 构造函数强制传入关键业务字段:比如
new InsufficientBalanceException(orderId, requiredAmount, availableAmount) - 避免在异常消息里拼接敏感信息(如用户身份证号),但可在字段中保留——日志脱敏由统一拦截器处理,不是异常类的责任
getMessage() 和 toString() 别手动拼接
很多人重写 getMessage() 返回类似 "余额不足:订单" + orderId + "需" + amount,这会让下游无法结构化提取字段。一旦要加监控告警或做错误码映射,就只能写正则去 parse 字符串。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 不重写
getMessage(),让它默认返回类名 + 字段 toString(JDK 14+ record 自动支持,老版本用 Lombok@Data) - 提供 getter 方法暴露结构化数据:
getOrderId()、getRequiredAmount(),让上层按需组装提示或落库 - 如果必须有可读消息(比如给前端展示),单独加一个
getFriendlyMessage()方法,不覆盖父类行为
Spring 中 @ControllerAdvice 捕获时别吞掉原始异常类型
常见错误是全局异常处理器里统一转成 ErrorResponse,但所有异常都塞进同一个 errorCode: "UNKNOWN_ERROR",前端根本分不清是参数错、权限错还是系统错。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 按异常类型分组处理:
@ExceptionHandler(InsufficientBalanceException.class)单独映射errorCode: "BALANCE_INSUFFICIENT" - 避免用
instanceof在一个 handler 里 if-else 判断十多种异常——类型多了就失控 - 把错误码定义为枚举,每个业务异常类关联一个
ErrorCode枚举实例,保证编译期校验和文档一致性
日志记录时别只打 e.printStackTrace()
堆栈里看不到 orderId 或 userId,运维查问题得翻三四个日志文件再人工关联。语义化异常的价值,一大半体现在日志可追溯性上。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 用 SLF4J 的占位符记录:
log.warn("转账失败,订单{}余额不足", ex.getOrderId(), ex)—— 异常对象放最后,SLF4J 自动附加堆栈 - 在异常类里重写
toString(),只包含业务关键字段(不包含大对象或集合),避免日志刷屏 - 禁止在 catch 块里 new 一个新异常再 throw(如
throw new RuntimeException("xxx", e)),会丢失原始类型;要用throw ex或封装为带 cause 的自定义异常
最难的不是定义一堆异常类,而是让团队所有人一致地用 getter 取值、不拼字符串、不在日志里丢关键 ID。一旦松动一环,语义就退化回“请查看堆栈”。










