主题
异常处理全生命周期规范指南
前置检查清单
开始异常处理前,请确认以下步骤:
- [ ] 已确定业务模块名称(如
Order、User) - [ ] 已梳理该模块需要定义的业务异常场景
- [ ] 已检查
src/main/resources/i18n/messages.properties和src/main/resources/i18n/messages_en.properties都同时存在(如不存在则创建) - [ ] 已确认每次新增/修改错误码枚举值时,同步更新上述两个 properties 文件
Step 1:异常类型选择决策
异常类型速查
| 异常类型 | 父类 | 日志行为 | 适用场景 |
|---|---|---|---|
BusinessException | AbstractRuntimeException | 记录 error 日志 | 一般业务异常:参数校验失败、业务规则违反、数据不存在 |
BusinessHintException | AbstractRuntimeException | 不记录日志 | 业务提示:需要给用户友好提示但不需要记录错误日志的场景 |
SystemException | AbstractRuntimeException | 记录 error 日志 | 系统级异常:第三方服务调用失败、系统内部错误 |
AuthException | RuntimeException | 记录日志 | 认证/鉴权失败(纯标记异常,由安全框架统一处理) |
框架全局异常处理器(
HttpHandlerExceptionResolverConfiguration)会自动捕获这些异常并转换为统一的BaseResponse响应。
决策流程
按以下顺序逐步判断,匹配到任一条件即停止:
判断点 1:是否需要给用户提示且不需要记录错误日志?
- 如果是 → 选择
BusinessHintException - 如果否 → 继续判断点 2
判断点 2:是否涉及认证或鉴权失败?
- 如果是 → 选择
AuthException - 如果否 → 继续判断点 3
判断点 3:是否为系统内部错误或第三方服务调用失败?
- 如果是 → 选择
SystemException - 如果否 → 继续判断点 4
判断点 4:其余所有业务逻辑异常场景
- 选择
BusinessException(参数校验、业务规则、数据不存在等)
场景对照表
| 场景描述 | 推荐异常类型 | 原因 |
|---|---|---|
| 参数格式不合法、必填字段缺失 | BusinessException | 业务规则校验,需要记录日志 |
| 根据 ID 查询不到数据 | BusinessException | 数据不存在属于业务异常 |
| 给用户展示友好提示(如"额度不足")且不记录 error 日志 | BusinessHintException | 仅需提示,不需要记录错误日志 |
| 调用第三方 API 超时或失败 | SystemException | 系统级问题,非业务逻辑 |
| Token 过期、无权限访问 | AuthException | 认证/鉴权相关,由安全框架处理 |
| Dubbo/Feign 远程调用失败 | SystemException | 属于外部服务调用失败 |
Step 2:错误码枚举与 i18n 配置
创建错误码枚举
枚举类需实现 IBusinessExceptionCodeConfiguration 接口。
命名规则:XxxOfBusinessExceptionCodeEnum 或 XxxExceptionCodeEnum
包路径建议:xxx.constants 或 xxx.enums
完整枚举模板代码与 i18n 资源文件定义详见 error-code-definition.md。
接口方法说明
| 方法 | 用途 | 是否必须实现 |
|---|---|---|
code() | 返回业务异常状态码,同时作为 i18n key | 是 |
defaultMsg() | 返回默认错误描述,作为 i18n 找不到 key 时的 fallback | 否(默认返回 code()) |
belongHttpStatus() | 指定 HTTP 状态码 | 否(默认 HttpStatus.BAD_REQUEST) |
创建 i18n 资源文件
每个异常枚举需配套创建以下两个文件:
- 中文默认资源文件:
src/main/resources/i18n/messages.properties - 英文资源文件:
src/main/resources/i18n/messages_en.properties
键名为枚举 code() 返回值,值为对应语言的错误描述。同时提供中英文可确保英文环境不 fallback 到中文。
完整 properties 示例详见 error-code-definition.md。
占位符使用规范
支持 java.text.MessageFormat 格式({0}、{1} 等)。
定义与使用必须成对出现:在枚举中定义占位符后,抛出时务必通过 args 传入对应数量的实际值,否则占位符无法被替换。
java
// Step 2:枚举定义占位符
ERR_SIZE_LIMIT("字段 {0} 长度必须在 {1} 到 {2} 之间")
// Step 3:抛出时传入对应数量的 args
throw BusinessException.error(
XxxOfBusinessExceptionCodeEnum.ERR_SIZE_LIMIT,
"description", 10, 200
);
// 中文结果: "字段 description 长度必须在 10 到 200 之间"
// 英文结果: "Field description length must be between 10 and 200"Step 3:异常抛出规范
抛出方式选择
| 场景 | 异常类型 | 推荐方式 | 示例 |
|---|---|---|---|
| 常规业务异常 | BusinessException | BusinessException.error(enum) | throw BusinessException.error(ERR_DATA_NOT_FOUND) |
| 需要 i18n 占位符替换 | BusinessException | BusinessException.error(enum, args...) | throw BusinessException.error(ERR_REQUIRED, "userName") |
| 需要包装底层异常(传递 cause) | BusinessException | new BusinessException(enum, msg, cause) | new BusinessException(ERR_RPC, e.getMessage(), e) |
| 用户提示场景(不记录日志) | BusinessHintException | BusinessHintException.error(enum) | throw BusinessHintException.error(ERR_NO_PERMISSION) |
| 系统级异常 | SystemException | SystemException.error(enum) | throw SystemException.error(ERR_RPC_FAIL) |
| 认证/鉴权失败 | AuthException | new AuthException(msg) | throw new AuthException("Token 已过期") |
标准抛出模板
模板 A:使用枚举默认消息(配合 i18n)
java
throw BusinessException.error(XxxOfBusinessExceptionCodeEnum.ERR_DATA_NOT_FOUND);模板 B:带参数 args(i18n 占位符替换)
java
throw BusinessException.error(
XxxOfBusinessExceptionCodeEnum.ERR_REQUIRED_FIELD_MISSING,
"userName"
);模板 C:多参数占位符替换
java
throw BusinessException.error(
XxxOfBusinessExceptionCodeEnum.ERR_SIZE_LIMIT,
"description", 10, 200
);模板 D:包装底层异常(需要传递 cause 时)
使用构造函数直接实例化并传递原始异常:
java
try {
thirdPartyApi.call();
} catch (Exception e) {
throw new BusinessException(
XxxOfBusinessExceptionCodeEnum.ERR_RPC,
e.getMessage(),
e
);
}注:当需要传递
cause时,必须使用new构造函数而非静态工厂方法error(),以确保原始堆栈信息不丢失。
常见场景速查
| 场景 | 异常类型(Step 1 决策) | 核心要点 | 参考文档 |
|---|---|---|---|
| 参数校验失败 | BusinessException | dto 判空、字段判空后抛出 BusinessException.error(enum) | exception-throwing.md |
| 数据不存在 | BusinessException | Mapper 查询结果判空后抛出 BusinessException.error(enum) | exception-throwing.md |
| 业务规则校验 | BusinessException | 状态/权限等校验,配合 i18n 占位符传参 | exception-throwing.md |
| 用户提示场景 | BusinessHintException | 使用 BusinessHintException.error(enum),不记录 error 日志 | exception-throwing.md |
| 包装第三方异常 | BusinessException | new BusinessException(enum, msg, cause) 保留原始堆栈 | exception-throwing.md |
| 参数批量校验 | BusinessException | 使用 ValidationUtils.validateBean() 或 validateProperty() | validation-utils.md |
上述场景的完整代码示例详见对应参考文档。
远程调用注意事项
在 Dubbo/Feign 等跨服务调用场景中:
- Provider 端:按 Step 1 决策结果直接抛出对应异常类型即可(业务校验失败用
BusinessException,远程调用失败用SystemException),无需额外处理 - Consumer 端:通常不需要
try-catch,让全局处理器统一接管 - 如需降级,捕获后按 Step 1 重新决策目标异常类型,并带上原始 cause
Step 4:异常捕获与处理
捕获原则
- [ ] 捕获异常前,确认是否有明确的降级逻辑或转换需求
- [ ] 如果没有明确处理逻辑,直接抛出让全局处理器接管
- [ ]
BusinessHintException的处理方法中不记录 error 日志 - [ ] 捕获后如需重新抛出,带上原始 cause
- [ ] 捕获后如需转换异常类型,回退到 Step 1 决策流程重新判断目标类型
常见场景速查
| 场景 | 核心要点 | 参考文档 |
|---|---|---|
| 异常降级为外部接口格式 | catch 后返回标准 DTO,提取枚举 code 和 msg | exception-catching.md |
| 异常类型转换 | BusinessException → BusinessHintException,避免公开接口记录过多错误日志 | exception-catching.md |
| 捕获后包装为外部接口格式 | catch 后构建 ExternalApiDTO,传递 code 和 msg | exception-catching.md |
| 跨服务调用异常处理 | Provider 端直接抛异常;Consumer 端让全局处理器接管 | exception-catching.md |
上述场景的完整代码示例详见 exception-catching.md。
全局处理机制说明
业务层不需要手动编写 @ControllerAdvice 或 try-catch 来转换响应格式。框架自动完成以下工作:
- 捕获异常并解析枚举
code()作为响应码 - 通过
@AppI18n切面自动翻译消息 - 根据异常类型决定是否记录 error 日志(
BusinessHintException不记录) - 包装为统一的
BaseResponse返回给前端
常见反模式速查
| 反模式 | 问题说明 | 正确做法 |
|---|---|---|
所有异常都用 BusinessException | 日志膨胀,用户看到不必要的错误 | 用户提示场景用 BusinessHintException |
| 错误码用魔法字符串 | 难以维护,容易冲突 | 定义枚举实现 IBusinessExceptionCodeConfiguration |
| 捕获后打印日志再 throw | 导致重复日志 | 直接抛出,让全局处理器统一记录 |
使用 e.printStackTrace() | 不规范的日志输出 | 使用 log.error("msg", e) 或交由全局处理器 |
| 异常消息硬编码中文/英文 | 不支持多语言 | 通过 i18n 资源文件管理消息 |
| 只创建中文资源文件 | 英文环境体验差 | 同时提供 messages.properties 和 messages_en.properties |
| args 参数用字符串拼接 | 破坏 i18n 占位符替换 | 将动态值通过 args 传入,资源文件中使用占位符 |
Do's and Don'ts
| Do(推荐做法) | Don't(避免做法) |
|---|---|
每个异常枚举配套创建 messages.properties(中文)和 messages_en.properties(英文) | 只创建中文资源文件,导致英文环境 fallback 到中文 |
动态值通过 args 传入,在资源文件中使用 {0}、{1} 占位符 | 使用字符串拼接传入异常消息,破坏 i18n 占位符替换机制 |
枚举的 defaultMsg() 作为 fallback,实际翻译以资源文件为准 | 将异常消息硬编码在代码中 |
使用 java.text.MessageFormat 格式的占位符 {0}、{1} | 使用 Slf4j 的 {} 占位符格式 |
使用 BusinessException.error(enum) 静态工厂方法 | 直接 new BusinessException() 而不传枚举 |
需要包装 cause 时使用 new BusinessException(enum, msg, cause) | 吞掉原始异常,不传递 cause |
BusinessHintException 用于只需提示用户的场景 | 所有场景都使用 BusinessException,导致日志膨胀 |
| 有明确降级逻辑时才捕获异常 | 无意义地捕获后仅打印日志再重新抛出 |
| 直接抛出异常,让全局处理器统一记录日志 | 捕获后打印日志再 throw,导致重复日志 |
| 降级时返回标准 DTO 而非抛出异常 | 静默吞掉异常,不做任何处理 |
参考文档
| 文档 | 内容 |
|---|---|
| error-code-definition.md | 完整枚举模板与 i18n 配置示例 |
| exception-throwing.md | 完整抛出场景示例(参数校验、数据不存在、业务规则、第三方包装、多参数占位符) |
| exception-catching.md | 完整捕获场景示例(降级、类型转换、外部接口包装) |
| validation-utils.md | ValidationUtils Bean 校验与属性校验 |
