不能只用 IllegalArgumentException 或 RuntimeException,因其缺乏业务语义,导致日志难区分、调用方无法精准处理、监控无法聚合、前端无法差异化提示;自定义异常需含 errorCode、traceId(推荐)、业务字段(如orderId),并设置serialVersionUID以保障跨JVM序列化。

为什么不能只用 IllegalArgumentException 或 RuntimeException?
因为它们不带业务语义。比如用户注册时手机号已存在,抛出 IllegalArgumentException("phone exists"),和参数为空、格式错误混在一起,日志里根本分不清是校验失败还是业务冲突。调用方也无法针对性重试或降级——所有异常都 catch RuntimeException,等于没区分。
- 同一个异常类型可能在 10 个模块里被抛出,堆栈看不出问题归属
- 监控系统无法按异常类型聚合告警(比如想单独看“库存不足”异常飙升)
-
前端需要不同错误码提示(400-参数错 / 409-资源冲突),通用异常没法携带
errorCode
继承 Exception 还是 RuntimeException?关键看是否强制处理
选错父类会导致编译报错或逻辑失控。比如订单创建失败必须通知用户并回滚事务,就该用受检异常(Exception 子类),强迫调用方显式处理;而参数校验失败属于编程错误,用 RuntimeException 子类更合理,避免满屏 try-catch。
- 用
Exception:外部依赖异常(如支付回调超时)、必须恢复的业务中断(如库存扣减失败) - 用
RuntimeException:输入校验、状态非法(如“订单已发货却要取消”)、内部逻辑断言 - 别让 Controller 层 catch 受检异常再包装成运行时异常——这会绕过编译检查,失去设计初衷
自定义异常里至少要加什么字段?
光有 message 不够。真实线上问题中,你永远需要快速定位“谁、在什么场景、因什么数据触发了这个异常”。所以建议标配三样:
-
errorCode:整型错误码,用于前端展示或网关路由(如USER_NOT_FOUND = 1001) -
traceId(可选但强烈推荐):关联全链路日志,避免翻几十个服务日志找上下文 - 必要业务字段:比如
orderId、skuId,直接打在异常对象里,比拼接字符串安全且易提取
示例:new OrderAlreadyShippedException("Order already shipped", 2003, "ORD-20260204-XXXX", "SKU-888")
立即学习“Java免费学习笔记(深入)”;
序列化 ID(serialVersionUID)不是摆设
如果异常要跨 JVM 传递(比如 Dubbo/RPC 调用、消息队列投递异常对象),缺少 serialVersionUID 会导致反序列化失败,报 InvalidClassException。哪怕现在不用,也建议加上:
private static final long serialVersionUID = 1L; —— 这行代码成本为零,但能避免未来升级时莫名其妙的序列化故障。
真正复杂的是异常传播链的设计:比如 DAO 层抛 DatabaseAccessException,Service 层不该直接 throw 上去,而应包装成 InventoryUpdateFailedException 并保留原始异常(super(message, cause))。否则你永远不知道到底是 SQL 写错了,还是连接池耗尽。










