BusinessException必须继承RuntimeException并持有ErrorCode对象, ErrorCode提供getCode()和getMessageArgs(),国际化渲染由全局异常处理器通过MessageSource统一完成。

业务异常类必须继承 RuntimeException 且带 error 字段
Spring Boot 或主流框架的全局异常处理器(如 @ControllerAdvice)只捕获 RuntimeException 及其子类。若自定义异常继承 Exception,不加 throws 声明就无法被默认机制捕获,容易静默失败。
关键不是“能不能抛”,而是“抛了有没有人接”。所以 BusinessException 必须直接或间接继承 RuntimeException,且内部必须持有错误码对象(如 ErrorCode 接口实现类),不能只存字符串或数字。
- 错误码对象要能提供
getCode()、getMsg()、getLocaleMessage(Locale)等方法 - 避免在异常构造时就拼好最终提示语(如
"用户不存在,ID=" + id),否则无法做国际化参数替换 - 不要把
Locale或MessageSource注入到异常类里——异常是数据载体,不是执行单元
错误码对象需支持多语言模板 + 动态参数绑定
国际化提示不是靠 if-else 切语言,而是靠 MessageSource 的 getMessage(code, args, locale) 方法动态解析。因此错误码对象本身不能硬编码中文或英文文案,而应只提供 key(如 "error.user.not.found")和可选参数数组(如 new Object[]{userId})。
典型结构是:ErrorCode 接口定义 getCode() 和 getDefaultMessage(),再由具体实现类(如 UserNotFoundErrorCode)返回 key;实际渲染交给外部 MessageSource。
- 资源文件用
messages_zh_CN.properties和messages_en_US.properties,key 保持一致 - 参数位置用
{0}、{1},不要用{id}这类命名占位符(MessageSource不支持) - 如果参数含敏感信息(如手机号),应在
getMessageArgs()返回前做脱敏,而不是在资源文件里写逻辑
构造函数别传 Locale,让调用方决定语言上下文
异常类的生命周期应尽可能短,且不依赖 HTTP 请求上下文。把 Locale 塞进 BusinessException 构造函数,会导致:测试难(得 mock 请求头)、复用差(RPC 调用无请求头)、线程不安全(Locale 是可变对象)。
正确做法是只传错误码和参数,语言解析延迟到全局异常处理器中统一做——那里能拿到 HttpServletRequest,也能从 lang header 或 Accept-Language 提取 Locale,再交由 MessageSource 渲染。
- 错误码类里不要有
getZhMessage()/getEnMessage()这类方法 - 异常类的
getMessage()可以 fallback 到getDefaultMessage(),仅用于日志或调试,不用于响应体 - 若需透传语言偏好(如异步任务中),应通过上下文传递
Locale,而非塞进异常实例
避免在异常里做 MessageSource 查找
有些实现会在 BusinessException 构造时就调用 messageSource.getMessage(...),这看似“一步到位”,实则埋下三个坑:
- Spring 上下文未初始化完成时,
messageSource为 null,直接 NPE - 不同模块加载顺序不确定,可能找不到对应资源文件,降级失败
- 违反单一职责:异常类不该承担消息渲染职责,该由处理器统一处理
真正该做的,是在全局异常处理器(如 GlobalExceptionHandler)里,用 exception.getErrorCode().getCode() 拿到 key,再结合请求中的 Locale 和 exception.getMessageArgs() 去查 MessageSource。这样既解耦,又可控。
最常被忽略的一点:资源文件路径没配对、basename 写错、或者 MessageSource Bean 名不是默认的 messageSource,都会导致 NoSuchMessageException ——这个异常本身又得被兜底,否则整个链路就崩了。










