非受检异常不应靠捕获兜底,而应前置防御暴露问题;仅三类可控异常可精准捕获:输入校验失败型、本地状态异常型、解析越界型;捕获后须轻量处理、禁新建对象、限日志采样、不替代修复。

非受检异常(RuntimeException及其子类)不是为“运行时恢复”而设计的,捕获它通常不是第一选择,而是信号——代码存在可预防的逻辑缺陷。真正有效的策略是:**不依赖捕获来兜底,而靠前置防御暴露问题;仅在特定场景下精准捕获,用于降级或可观测性保障。**
哪些非受检异常值得捕获?
不是所有 RuntimeException 都该进 catch 块。只关注三类明确可控、局部可绕过的异常:
-
输入校验失败型:如
IllegalArgumentException、IllegalStateException。例如设备 ID 格式非法、状态机未就绪却调用操作——此时可直接返回默认值或原始输入,不查表、不告警。 -
本地状态异常型:如
NullPointerException(缓存未初始化)、ConcurrentModificationException(字典结构被并发修改)。说明当前内存态不可靠,应切换至预置静态兜底数据(如内置高频映射表)。 -
解析越界型:如
ArrayIndexOutOfBoundsException、StringIndexOutOfBoundsException。常见于协议码位解析错位,可降级为返回通用标识(如"unknown"),避免中断整个查询流程。
捕获后怎么做才合理?
一旦决定捕获,动作必须轻量、无副作用、不引入新风险:
- 复用静态对象,禁止在 catch 中新建集合、拼接字符串或触发远程调用;
- 日志仅限 DEBUG 级别采样(如每千次记 1 条),避免刷爆边缘设备存储;
- 不自动上报监控或触发熔断,除非同类型异常持续高频(如 5 分钟内超 5%);
- 不替代修复——捕获只是临时屏障,对应代码仍需补上参数校验、空值检查或状态 guard clause。
更推荐的替代方案:预防优于捕获
多数非受检异常本不该发生。与其写 try-catch,不如把检查做在前面:
- 用
Objects.requireNonNull()主动拦截 null,抛出带上下文的 NPE; - 方法入口用
Assert.isTrue()或自定义校验工具,抛IllegalArgumentException并附错误字段名; - 数组/集合访问前加
if (i >= 0 && i ,比靠异常兜底更高效、堆栈更清晰; - 配合 IDE 的 @NotNull/@Nullable 注解,让空指针在编码阶段就被标记出来。
业务异常怎么封装才对?
用户行为导致的“拒绝”不是 bug,但也不该用受检异常增加调用负担。正确做法是:
- 定义自定义非受检异常,如
InsufficientBalanceException、InvalidOrderStateException; - 命名带语义(含
Illegal、Invalid、Missing等前缀),继承RuntimeException; - 构造时传入业务错误码、消息和可选 cause,便于全局处理器统一转成标准响应;
- 在 @RestControllerAdvice 中集中处理,记录 ERROR 日志并返回 HTTP 400 或业务错误码,不打断事务(Spring 默认回滚非受检异常,无需额外配置)。











