错误码必须绑定在异常实例上,正确做法是构造时固化code和message并提供getter;统一继承BaseException避免instanceof分发;错误码采用六位数字格式如201001;异常需内置不可变context透传排障信息。

错误码必须绑定在异常实例上,不能只靠枚举静态值
很多人写 BusinessException 时,只传一个 ErrorCode 枚举进去,但没把它的 code() 和 message() 绑定到异常对象自身。结果日志里只能看到类名,看不到具体错误码;前端收到的响应也得靠反射或额外字段拼接——这破坏了异常的自解释性。
正确做法是让异常构造时就把错误码的字符串值和消息固化下来:
- 构造函数中调用
errorCode.code()和errorCode.message(),传给父类super(message),同时存为this.code和this.message字段 - 暴露
getErrorCode()、getErrorMessage()方法,供全局处理器直接取用,不依赖反射或类型判断 - 避免在
@ExceptionHandler里再查一遍枚举——那会引入重复逻辑和潜在 NPE
@ControllerAdvice 中别用 instanceof 做异常类型分发
常见错误是这样写处理器:
@ExceptionHandler
public ResponseEntity<result> handle(Exception e) {
if (e instanceof BusinessException) {
return Result.fail(((BusinessException) e).getCode(), e.getMessage());
} else if (e instanceof SystemException) {
return Result.fail(500, "系统繁忙");
}
// ...一堆 else if
}</result>
问题很明显:新增一种异常就得改这里,违反开闭原则;而且一旦子类继承链变深(比如 PaymentException extends BusinessException),instanceof BusinessException 会误捕获不该由它处理的子类。
更稳的方式是:
- 所有业务异常统一继承抽象基类
BaseException,且该类提供getErrorCode()和getErrorLevel() - 用
@ExceptionHandler(BaseException.class)拦住全部业务异常,用@ExceptionHandler(Throwable.class)拦住未预期异常 - 让
BaseException的子类自己决定是否覆盖getHttpStatusCode(),而不是在处理器里硬编码状态码
错误码格式要支持模块隔离与机器可读
纯数字如 1001 或纯字符串如 "USER_NOT_FOUND" 都有缺陷:前者难溯源模块,后者无法做范围判断(比如“用户模块所有错误码都在 1000–1999”)。
推荐用「项目+模块+序号」六位数字格式,例如 201001 表示「项目 20 → 用户模块 10 → 错误 001」:
- 定义为
enum,每个枚举项带projectCode()、moduleCode()、errorCode()三个方法,方便聚合统计 - 在
BaseException构造时校验:如果传入的枚举值不是IErrorCode实现类,抛IllegalArgumentException,防止误传 - 日志打印时自动附加
[ERR:201001]前缀,运维 grep 时能直接定位模块
别忽略异常上下文(context)的透传能力
很多团队只返回错误码和一句话提示,但真实排障需要更多现场信息:用户 ID、订单号、请求 traceId、甚至参数快照。这些不能塞进 message 字符串里(破坏结构化),也不能全丢进日志(前端拿不到)。
解决方案是在异常基类中内置一个 Map 字段:
- 提供
addContext(String key, Object value)方法,允许在抛异常前注入关键现场数据 - 全局处理器序列化这个 map 到响应体的
data.context字段(注意过滤敏感键如 password) - 前端遇到特定错误码(如支付失败)时,可从
context.orderNo直接取单号重试,不用再查日志
最易被忽略的是:context 字段必须是不可变副本(new HashMap(original)),否则多线程下可能被后续代码篡改。










