异常处理(错误码)
这篇解决什么
管理端接口要让前端知道:成功时 data 是什么,失败时提示什么。读完能改 CommonResult 返回、在 Service 里抛 ServiceException、给模块补错误码。
默认端口 48080,前缀 /admin-api。PC 端按 code !== 0 当成失败,见 ruoyi-office-vben/apps/web-antd/src/api/request.ts。
统一响应
业务错误很多,没法一一映射成 HTTP 状态码。状态放在响应体里:成功 code = 0 加 data,失败非 0 加 msg。
示意图:响应体只有 code / msg / data。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
code | Integer。0 成功,其它失败 | ruoyi-office/yudao-framework/yudao-common/.../pojo/CommonResult.java |
msg | 用户可读提示。成功时是 "" | 同上 |
data | 成功载荷,泛型 T | 同上 |
isSuccess() | 判断 code == 0,@JsonIgnore 不进 JSON | 同上 |
{
"code": 0,
"msg": "",
"data": { "id": 1, "username": "admin" }
}{
"code": 1002003000,
"msg": "用户账号已经存在"
}要不要再加 success 字段
JSON 里没有 success。前端用 code === 0。isSuccess() / isError() 只给 Java 用。
成功返回
Controller 返回类型写成 CommonResult<T>,调用 success(data)。data 用 VO,不要用 Map。
示意图:UserController#createUser 返回新建用户编号。
@RestController
@RequestMapping("/system/user")
public class UserController {
@PostMapping("/create")
public CommonResult<Long> createUser(@Valid @RequestBody UserSaveReqVO reqVO) {
Long id = userService.createUser(reqVO);
return success(id);
}
}路径:ruoyi-office/yudao-module-system/yudao-module-system-server/.../controller/admin/user/UserController.java。
失败不要在 Controller 里拼 CommonResult.error,让 Service 抛异常,由全局处理器翻译。
不要自动包装
有的项目用 @ControllerAdvice 把任意返回值包成统一结构。这里不这么做:方法签名必须是 CommonResult,Swagger 才对得上。
GlobalResponseBodyHandler 只在返回类型已是 CommonResult 时,把结果记到请求属性,给访问日志用,不改结构。
路径:ruoyi-office/yudao-framework/yudao-spring-boot-starter-web/.../handler/GlobalResponseBodyHandler.java。
哪些接口不包 CommonResult
文件下载、Excel 导出、第三方回调往往直接写字符串或流。这些方法不要改成 CommonResult。
异常处理
MVC 里的异常由 @RestControllerAdvice 收口。Filter 在 DispatcherServlet 之前,必须自己 try / catch,再复用同一套翻译。
示意图:Filter 与 MVC 最终都落到 CommonResult。
Spring MVC
GlobalExceptionHandler 扫描 cn.iocoder.yudao。参数校验、404、405、无权限、ServiceException 各有 @ExceptionHandler。未识别的进 defaultExceptionHandler,返回 500「系统异常」,并异步写错误日志。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
GlobalExceptionHandler | 异常 → CommonResult | ruoyi-office/yudao-framework/yudao-spring-boot-starter-web/.../handler/GlobalExceptionHandler.java |
serviceExceptionHandler | 业务码原样回前端 | 同上 |
defaultExceptionHandler | 兜底 INTERNAL_SERVER_ERROR | 同上 |
allExceptionHandler | Filter 调用的总入口 | 同上 |
@ExceptionHandler(value = ServiceException.class)
public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
return CommonResult.error(ex.getCode(), ex.getMessage());
}Filter
TokenAuthenticationFilter 校验 Token 时若抛错,不能指望 @ExceptionHandler。
示意图:catch 后走 allExceptionHandler,再 writeJSON。
} catch (Throwable ex) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
ServletUtils.writeJSON(response, result);
return;
}路径:ruoyi-office/yudao-framework/yudao-spring-boot-starter-security/.../filter/TokenAuthenticationFilter.java。ApiEncryptFilter、TenantSecurityWebFilter 同样调用 allExceptionHandler。
Filter 进不了 MVC 异常处理
Filter 里的异常必须自己捕获。漏掉 catch,前端拿到的不是 CommonResult,页面无法弹 msg。
业务异常
Service 里用户名重复、库存不足这类错误,不要 return CommonResult.error。
声明式事务按异常回滚;层层返回 CommonResult 还要不断判断 isError()。做法是抛 ServiceException。RPC 若拿到别人的 CommonResult,可调用 checkError() / getCheckedData()。
ServiceException
继承 RuntimeException,带 code 和 message。多数情况不用 try / catch,交给 GlobalExceptionHandler。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
ServiceException | 业务异常,非受检 | ruoyi-office/yudao-framework/yudao-common/.../exception/ServiceException.java |
ErrorCode | code + msg 常量对象 | ruoyi-office/yudao-framework/yudao-common/.../exception/ErrorCode.java |
ServiceExceptionUtil | 按 {} 占位符格式化后抛出 | ruoyi-office/yudao-framework/yudao-common/.../exception/util/ServiceExceptionUtil.java |
示意图:throw exception(...) → 处理器回 error(code, msg)。
if (id == null) {
throw exception(USER_USERNAME_EXISTS);
}路径:ruoyi-office/yudao-module-system/yudao-module-system-server/.../service/user/AdminUserServiceImpl.java。静态导入 ServiceExceptionUtil.exception。
不要抛裸 Exception
throw new Exception("失败") 或未捕获的 NPE 会进兜底,前端只看到「系统异常」,并记一条错误日志。业务失败一律 throw exception(错误码常量)。
ServiceExceptionUtil
提示支持 {} 占位,不要用 String.format(参数对不上会再抛异常)。
public static ServiceException exception(ErrorCode errorCode) { /* ... */ }
public static ServiceException exception(ErrorCode errorCode, Object... params) { /* ... */ }ROLE_NAME_DUPLICATE 的文案是 已经存在名为【{}】的角色,调用 exception(ROLE_NAME_DUPLICATE, name)。参数个数不对时打错误日志,尽量拼出可读句子。
错误码
ErrorCode 全局唯一,用来定位是哪个模块、哪条规则。系统码 0–999,业务码从 1_000_000_000 起。
系统错误码
和常见 HTTP 语义对齐。成功不用 200,继续用 0。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
SUCCESS | 0 成功 | ruoyi-office/yudao-framework/yudao-common/.../enums/GlobalErrorCodeConstants.java |
BAD_REQUEST | 400 参数不正确 | 同上 |
UNAUTHORIZED | 401 未登录 | 同上 |
FORBIDDEN | 403 无权限 | 同上 |
NOT_FOUND | 404 | 同上 |
METHOD_NOT_ALLOWED | 405 | 同上 |
TOO_MANY_REQUESTS | 429 | 同上 |
INTERNAL_SERVER_ERROR | 500 系统异常 | 同上 |
NOT_IMPLEMENTED | 501 未实现 / 未开 | 同上 |
REPEATED_REQUESTS | 900 重复请求 | 同上 |
DEMO_DENY | 901 演示模式禁写 | 同上 |
业务错误码
一共 10 位、四段。ServiceErrorCodeRange 只写注释、不参与运行。
示意图:1_002_003_000 = 业务 / system / 用户 / 序号 000。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 第 1 段 1 位 | 1 业务异常 | ruoyi-office/yudao-framework/yudao-common/.../enums/ServiceErrorCodeRange.java |
| 第 2 段 3 位 | 系统,如 002 system | 同上 |
| 第 3 段 3 位 | 模块,如 003 用户 | 同上 |
| 第 4 段 3 位 | 该模块内自增 | 同上 |
ServiceErrorCodeRange 注释里的区间:
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| infra | [1-001-000-000, 1-002-000-000) | ServiceErrorCodeRange |
| system | [1-002-000-000, 1-003-000-000) | 同上 |
| report | [1-003-000-000, 1-004-000-000) | 同上 |
| member | [1-004-000-000, 1-005-000-000) | 同上 |
| mp | [1-006-000-000, 1-007-000-000) | 同上 |
| pay | [1-007-000-000, 1-008-000-000) | 同上 |
| product | [1-008-000-000, 1-009-000-000) | 同上 |
| bpm | [1-009-000-000, 1-010-000-000) | 同上 |
| trade | [1-011-000-000, 1-012-000-000) | 同上 |
| promotion | [1-013-000-000, 1-014-000-000) | 同上 |
| crm | [1-020-000-000, 1-021-000-000) | 同上 |
| ai(注释) | [1-022-000-000, 1-023-000-000) | 同上 |
各模块真正使用的段号写在自己的 ErrorCodeConstants 文件头,新增时以该文件为准。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| system | 1-002-000-000 | ruoyi-office/yudao-module-system/yudao-module-system-api/.../enums/ErrorCodeConstants.java |
| infra | 1-001-000-000 | yudao-module-infra-api/.../ErrorCodeConstants.java |
| bpm | 1-009-000-000 | yudao-module-bpm-api/.../ErrorCodeConstants.java |
| oa | 1-101-000-000 | yudao-module-oa-api/.../ErrorCodeConstants.java |
| hrm | 1-050-000-000 | yudao-module-hrm-api/.../ErrorCodeConstants.java |
| contract | 1-102-000-000 | yudao-module-contract-api/.../ErrorCodeConstants.java |
| crm | 1-020-000-000 | yudao-module-crm-api/.../ErrorCodeConstants.java |
| erp | 1-030-000-000 | yudao-module-erp-api/.../ErrorCodeConstants.java |
段号可能重叠
ErrorCode 必须全局唯一。个别模块文件头都写了 1-040 或 1-050。加新码先全仓搜数字,不要只看 ServiceErrorCodeRange 注释。
模块常量
system 按子功能再切三段,例如 AUTH 1-002-000-000、菜单 1-002-001-000、用户 1-002-003-000。
ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1_002_000_000, "登录失败,账号密码不正确");
ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1_002_000_004, "验证码不正确,原因:{}");
ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在");新业务:在对应 *-api 的 ErrorCodeConstants 追加常量,Service 里 throw exception(常量)。不要在代码里写裸数字。
配置与操作
新错误在对应 *-api 的 ErrorCodeConstants 追加,Service 里 throw exception(常量)。不要写裸数字。全局处理在 Web Starter,页面无「错误码」菜单。
开启见 框架层。
