第二章 · 错误码体系与统一返回(yudao-common 基础层)
这一章是整个平台的地基。无论你写哪个业务模块,最终返回的 JSON 一定是
CommonResult,抛出的业务异常一定是ServiceException。理解了这里的”三条主线”——错误码定义、异常体系、统一返回——你就拿到了读懂全工程的第一把钥匙。
1. 模块概述
yudao-framework/yudao-common 是零 Spring 依赖的纯基础模块(56 个 Java 文件),它提供:
- 错误码体系:
ErrorCode+GlobalErrorCodeConstants+ServiceErrorCodeRange; - 分支异常:
ServiceException(业务)与ServerException(系统); - 统一返回:
CommonResult<T>; - 基础枚举:
UserTypeEnum、CommonStatusEnum、WebFilterOrderEnum等; - 工具类:
ServletUtils、JsonUtils、CollectionUtils、DateUtils、校验注解(@InEnum、@Mobile)等。
在系统中的位置:下面被框架 Starter 依赖,上面被所有业务模块依赖,因此它必须是”无污染”的——不引入 Spring,任何模块都能安全地引用它。
graph TD
BIZ[业务模块
system / infra / bpm ...] --> FW[框架层
yudao-framework/* Starter]
FW --> COM[yudao-common
本章 · 无 Spring 依赖]
2. 命名体系与易混淆对象对比
2.1 命名规律拆解
| 前缀/命名 | 对象 | 操作 | 含义 |
|---|---|---|---|
ErrorCode |
(对象) | 构造 | 不可变的 “错误码+提示” 值对象 |
ServiceException |
(异常) | exception() |
业务逻辑中断,携带错误码与可读信息 |
ServerException |
(异常) | exception() |
服务器异常,携带全局错误码 |
CommonResult |
(返回) | success/error |
统一 API 返回包装 |
xxxExceptionUtil |
exception |
(工厂) | 便捷抛出 ServiceException 的静态工具 |
命名规律总结:异常与返回统一承载 ErrorCode(code+msg);抛出侧用 exception() 工厂、返回侧用 success()/error() 静态方法,形成”定义一处、到处复用“的错误码中心化。
2.2 易混淆对象对比:ServiceException vs ServerException
| 对比维度 | ServiceException |
ServerException |
|---|---|---|
| 语义 | 业务层可预期异常(如”库存不足””手机号已存在”) | 系统层异常(服务器故障) |
| 错误码来源 | 业务码 [1_000_000_000, +∞),来自 ServiceErrorCodeRange |
全局码,来自 GlobalErrorCodeConstants |
| 抛出方式 | ServiceExceptionUtil.exception(errorCode) |
ServiceExceptionUtil.exception0(code, msg) |
| 给用户看 | msg 可读,直接展示 | 兜底,通常归并为 500 系统异常 |
| 一句话区分 | 是”业务规则的表达”,提示语直接给用户;ServerException 是”系统故障的表达”,一般不给用户看细节。 |
同理对比 CommonResult.error(ErrorCode) 与 CommonResult.error(Integer, String)——前者面向”已定义错误码”(参数可占位插缝),后者面向”临时裸码码 + 文本”。
3. API Signatures
本模块核心 API 的完整签名与调用关系:
graph TD
E[业务代码] -->|throw ServiceExceptionUtil.exception| SE[ServiceException]
E -->|返回| CR[CommonResult.success/error]
SE -->|被 GlobalExceptionHandler 捕获| CR2[转成 CommonResult]
C[CommonResult.checkError] -->|若有错| SE2[抛 ServiceException]
EC[ErrorCode] --> CR
EC --> SE
GEC[GlobalErrorCodeConstants] --> EC
1 | // —— 值对象 —— |
各方法用途:
CommonResult.error(ErrorCode errorCode, Object... params):错误码支持{}占位符,params填充到 msg;这也是读写CommonResult最常用的一条。CommonResult.checkError():常在 RPC 边界(A 调用 B 返回CommonResult)使用——成功返回,失败直接抛ServiceException向上传播。ServiceExceptionUtil.exception(errorCode):业务模块里ServiceExceptionUtil.exception(AUTH_LOGIN_BAD_CREDENTIALS)的标准写法。
4. 数据结构深度解析
4.a 结构体存在的理由
**
ErrorCode**:把”错误码 + 提示语”绑成一个不可变对象,好处是——
- 全局错误码与业务错误码分段管理,互不冲突;
- 提示语集中维护,业务代码里不需要到处散落字符串;
- 为未来国际化(源码
TODO明示)预留了结构:对象而非裸整数,才能携带多语言。**
CommonResult<T>**:若每个接口各返回各的 JSON 形态,前端就要为每种接口写一套解析。统一为code+data+msg,前端”一个接手函数打天下”。
4.b 代码:ErrorCode 与 GlobalErrorCodeConstants
📍 源码:ErrorCode.java
📍 源码:GlobalErrorCodeConstants.java
1 | // ErrorCode 是接口常量,直接 new 成值对象 |
4.c 字段三层分析表(以 CommonResult 为例)
| 字段 | 设计动机(为什么需要) | 反事实(如果去掉会怎样) | 替代方案(还能怎么做) |
|---|---|---|---|
code |
机器可读的状态码,0 表成功 |
前端无法高效判断成败 | 用 HTTP 状态码,但语义表达能力弱(源码注释已权衡) |
data |
承载返回的业务数据 | 成功时无数据可拿 | 无 |
msg |
人类可读的提示语 | 前端只能显示”请求失败” | 由前端映射 code→文案,但重复维护 |
4.d 生命周期状态图(CommonResult 从建造到消费)
stateDiagram-v2
[*] --> 构建: Service/Controller 调用 success()/error()
构建 --> 成功: success(data) code=0
构建 --> 失败: error(code,msg) code≠0
成功 --> 序列化: 前端读取 data
失败 --> 序列化: 前端读 msg
序列化 --> [*]
失败 --> 抛异常: checkError() 跨模块边界
抛异常 --> [*]
5. 函数逐行精讲
5.a 场景卡片
**类:
CommonResult**(CommonResult.java)
- 调用时机:所有 Controller 方法返回、跨模块 RPC 方法返回。
- 典型调用者:任意
XxxController、任意XxxApiImpl。- 前置条件:无(纯静态工厂,可随时调用)。
- 目的:给前端/调用方一个统一、稳定、可判定的返回结构。
5.b 逐行注释式精讲:CommonResult.error(ErrorCode, Object...)
1 | public static <T> CommonResult<T> error(ErrorCode errorCode, Object... params) { |
5.c 场景卡片:ServiceExceptionUtil.exception
函数:
ServiceExceptionUtil.exception(ErrorCode)
- 调用时机:业务校验不通过时(如”用户名不存在”)。
- 典型调用者:
AdminAuthServiceImpl、任意业务 Service。- 前置条件:已定义好对应
ErrorCode。- 目的:抛出一个携带错误码、可被全局异常处理器统一翻译成
CommonResult的业务异常。
1 | public static ServiceException exception(ErrorCode errorCode) { |
6. 关键机制剖析:分段错误码防止冲突
机制思想:全局码 [0,999] 与业务码 [1_000_000_000, +∞) 彻底隔离,业务码内部再按”系统→模块→递增”三段切分:
1 | 1_001_000_000 ~ 1_002_000_000 infra 基础设施 |
为什么这样设计:
- 全局码贴合 HTTP 语义(401/403/500…),前端好理解;
- 业务码 10 位,把”哪个系统、哪个模块、哪条错误”编码进数字,日志里看错误码即知归属,排障极快;
- 区间预留,新模块在划定区间内自增,不会撞车。
📌
ServiceErrorCodeRange源码注释用一张 ASCII 表完整说明四段切分规则,是理解”错误码为什么能跨几十个模块不冲突”的最佳入口。
7. 设计决策分析
- 为什么
CommonResult不用 HTTP 状态码承载业务成败? 权衡:HTTP 状态码本就表达传输语义,将其复用为业务成败会语义混乱(如业务”数据不存在”该用 404 还是 200?)。平台选择”HTTP 始终 200 +code字段表达业务结果”,前端只认code。 - 为什么错误码要用对象而非整数? 为国际化与提示语集中管理预留空间;
params占位符机制让同一错误码在不同场景展示不同明细。 - 为什么把
success定为code=0而非200? 项目历史沿革(源码注释明说”一直使用 0 作为成功”)+ 统一,避免与 HTTP 200 混淆。
8. 学习检查点
📝 本章小结
- 返回 JSON 恒为
code + data + msg,0成功、其余失败。 - 业务异常抛
ServiceException,系统异常抛ServerException,全局异常处理器统一翻译。 - 全局错误码
[0,999]贴合 HTTP;业务错误码[1_000_000_000,+∞)分段切分、零冲突。 - 跨模块边界用
CommonResult.checkError()把失败一键转成异常。
🤔 思考题
为什么
ErrorCode的字段用private final?改动它会造成什么影响?(提示:省流与不可变性)参考答案
因为是值对象且被全局常量集中初始化,
final保证不可变——错误码一旦确定不应被业务代码篡改,避免”某处把msg改了”导致提示语失控。若改成可变,还无法安全地把同一ErrorCode常量在多处共享(不可变才可随意引用)。你能举出一个场景,让
CommonResult.checkError()的价值最大?(提示:想想 A 模块调 B 模块接口)参考答案
跨模块 RPC 边界:comm 平台
framework调用 system 的OAuth2TokenApi,返回CommonResult<OAuth2AccessTokenDO>;调用方只需getCheckedData()——成功直接拿 data,失败自动抛ServiceException传给上层全局处理器,把”判定+传递”两件啰嗦的事压缩成一行,且错误码/提示无缝流转。