Skip to content

异常处理全生命周期规范指南

前置检查清单

开始异常处理前,请确认以下步骤:

  • [ ] 已确定业务模块名称(如 OrderUser
  • [ ] 已梳理该模块需要定义的业务异常场景
  • [ ] 已检查 src/main/resources/i18n/messages.propertiessrc/main/resources/i18n/messages_en.properties 都同时存在(如不存在则创建)
  • [ ] 已确认每次新增/修改错误码枚举值时,同步更新上述两个 properties 文件

Step 1:异常类型选择决策

异常类型速查

异常类型父类日志行为适用场景
BusinessExceptionAbstractRuntimeException记录 error 日志一般业务异常:参数校验失败、业务规则违反、数据不存在
BusinessHintExceptionAbstractRuntimeException不记录日志业务提示:需要给用户友好提示但不需要记录错误日志的场景
SystemExceptionAbstractRuntimeException记录 error 日志系统级异常:第三方服务调用失败、系统内部错误
AuthExceptionRuntimeException记录日志认证/鉴权失败(纯标记异常,由安全框架统一处理)

框架全局异常处理器(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 接口。

命名规则XxxOfBusinessExceptionCodeEnumXxxExceptionCodeEnum
包路径建议xxx.constantsxxx.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:异常抛出规范

抛出方式选择

场景异常类型推荐方式示例
常规业务异常BusinessExceptionBusinessException.error(enum)throw BusinessException.error(ERR_DATA_NOT_FOUND)
需要 i18n 占位符替换BusinessExceptionBusinessException.error(enum, args...)throw BusinessException.error(ERR_REQUIRED, "userName")
需要包装底层异常(传递 cause)BusinessExceptionnew BusinessException(enum, msg, cause)new BusinessException(ERR_RPC, e.getMessage(), e)
用户提示场景(不记录日志)BusinessHintExceptionBusinessHintException.error(enum)throw BusinessHintException.error(ERR_NO_PERMISSION)
系统级异常SystemExceptionSystemException.error(enum)throw SystemException.error(ERR_RPC_FAIL)
认证/鉴权失败AuthExceptionnew 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 决策)核心要点参考文档
参数校验失败BusinessExceptiondto 判空、字段判空后抛出 BusinessException.error(enum)exception-throwing.md
数据不存在BusinessExceptionMapper 查询结果判空后抛出 BusinessException.error(enum)exception-throwing.md
业务规则校验BusinessException状态/权限等校验,配合 i18n 占位符传参exception-throwing.md
用户提示场景BusinessHintException使用 BusinessHintException.error(enum)不记录 error 日志exception-throwing.md
包装第三方异常BusinessExceptionnew 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 和 msgexception-catching.md
异常类型转换BusinessExceptionBusinessHintException,避免公开接口记录过多错误日志exception-catching.md
捕获后包装为外部接口格式catch 后构建 ExternalApiDTO,传递 code 和 msgexception-catching.md
跨服务调用异常处理Provider 端直接抛异常;Consumer 端让全局处理器接管exception-catching.md

上述场景的完整代码示例详见 exception-catching.md


全局处理机制说明

业务层不需要手动编写 @ControllerAdvicetry-catch 来转换响应格式。框架自动完成以下工作:

  1. 捕获异常并解析枚举 code() 作为响应码
  2. 通过 @AppI18n 切面自动翻译消息
  3. 根据异常类型决定是否记录 error 日志(BusinessHintException 不记录)
  4. 包装为统一的 BaseResponse 返回给前端

常见反模式速查

反模式问题说明正确做法
所有异常都用 BusinessException日志膨胀,用户看到不必要的错误用户提示场景用 BusinessHintException
错误码用魔法字符串难以维护,容易冲突定义枚举实现 IBusinessExceptionCodeConfiguration
捕获后打印日志再 throw导致重复日志直接抛出,让全局处理器统一记录
使用 e.printStackTrace()不规范的日志输出使用 log.error("msg", e) 或交由全局处理器
异常消息硬编码中文/英文不支持多语言通过 i18n 资源文件管理消息
只创建中文资源文件英文环境体验差同时提供 messages.propertiesmessages_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.mdValidationUtils Bean 校验与属性校验

Power By 数字海南