业务异常用自定义runtimeexception主动抛、不拦截,系统异常在依赖边界捕获并包装为统一类型上抛,error只记录不处理;三者语义归属与恢复能力不同,需分层精准应对。

区分捕获业务异常与系统异常,核心不在“怎么抓”,而在“谁该抛、谁该拦、谁根本不该拦”。关键看异常的语义归属和恢复能力:业务异常是流程中可预期的分支结果;系统异常是运行环境失稳或外部依赖失效的信号;Error 则是 JVM 已无法保障正常执行的灾难。
业务异常:用自定义 RuntimeException 主动抛,不拦截原始调用
业务异常不是错误,而是业务规则的自然反馈。比如“手机号已注册”“优惠券不可叠加”“库存不足但允许凑单”。这类情况应:
- 定义明确语义的运行时异常类,如 PhoneAlreadyRegisteredException、CouponNotApplicableException,类名带业务关键词,继承 RuntimeException
- 构造时携带业务码(如 ERR_PHONE_DUPLICATE)、关键参数(phone=138****1234)和建议动作("请登录查看已有账号")
- 在 Service 层校验失败时直接 throw,不 try-catch 包裹,也不向上 throws 声明
- 由全局异常处理器(@ControllerAdvice)统一捕获,返回 HTTP 200 + JSON 中的 code/warning/message 字段,前端决定是否提示
系统异常:在依赖边界捕获原始 Exception,分层包装再上抛
数据库连不上、HTTP 调用超时、文件写入失败……这些属于系统异常,本质是外部协作环节出问题。它们属于 Exception 体系,但不能让 Controller 直接面对 SQLException 或 TimeoutException:
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
- 在 DAO 层或 Feign Client 层捕获原始异常(如 SQLException、ConnectException),记录完整 ERROR 日志(含 MDC 上下文、重试次数、下游服务名)
- 立即包装为统一的系统异常类型,如 SystemUnavailableException(继承 RuntimeException),避免上层堆砌各种 catch
- 配置熔断器(如 Sentinel)对同一异常类型单位时间频次做自动降级,降级后返回缓存值或默认值,并打 WARN 日志
- 全局异常处理器捕获 SystemUnavailableException,返回 HTTP 503 或自定义错误码,附带可操作提示(如“当前服务繁忙,请稍后再试”)
Error:不捕获、不处理、只记录并退出
OutOfMemoryError、StackOverflowError、NoClassDefFoundError 这些不是异常,是 JVM 状态崩溃的证据。它们和 Exception 平级,catch(Exception e) 根本捕不到:
- 禁止写 catch(Error e) 并尝试“恢复逻辑”——此时日志可能写不出,线程可能卡死
- 唯一合理做法是在 JVM 启动参数中加入 -XX:+HeapDumpOnOutOfMemoryError 和 -XX:ErrorFile,确保崩溃时留痕
- 监控系统对 java.lang.Error 类型日志自动触发 P0 告警(电话+短信),并强制实例下线重启
- 代码里出现 new MyBusinessError extends Error 是严重反模式,必须从代码规范中禁用
补充:别混淆“日志级别”和“异常类型”
不是打了 WARN 日志就是业务警告,也不是 ERROR 日志就代表系统故障。日志级别只是输出标记,真正决定处理策略的是异常类型设计:
- 业务警告类异常(如 UserBalanceWarningException)日志打 WARN,但响应是 HTTP 200
- 系统异常(如 SystemUnavailableException)日志打 ERROR,响应是 HTTP 503
- Error 类型日志必须打 ERROR,且必须触发告警和自愈流程
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










