在任何编程语言中,异常机制的本质只有一句话:
异常 = 跨函数边界的错误传播机制
它解决的根本问题是:
当程序执行路径上出现“非常规情况”时,如何优雅地打断正常控制流,并把错误信息传递给合适的处理者。
在异常机制出现之前,程序处理错误主要依赖:
这些方式的本质问题是:
错误处理逻辑会污染正常业务逻辑
异常机制的价值在于:
| 方式 | 关注点 |
|---|---|
| 返回值 | 调用方必须显式判断 |
| 异常 | 将“错误处理”与“业务逻辑”解耦 |
异常让程序结构从:
业务逻辑 + 大量 if 判断
变为:
业务逻辑主路径
+ 独立的异常处理路径
异常机制在本质上提供了三个能力:
这三点共同决定了:
异常不是“错误本身”,而是“错误处理的组织机制”。
在 Java 中,异常体系是一棵类型树:
Object
└── Throwable
├── Error
└── Exception
├── Checked Exception
└── RuntimeException
| 类型 | 本质 |
|---|---|
| Error | JVM 层面无法恢复的问题 |
| Exception | 程序逻辑层面可处理的问题 |
Java 将异常分为两类:
| 维度 | Checked Exception | Runtime Exception |
|---|---|---|
| 编译器要求 | 必须处理 | 可不处理 |
| 设计意图 | 强制调用方感知 | 表示程序缺陷 |
| 可恢复性 | 更偏可恢复 | 更偏不可恢复 |
核心区别不是“能否处理”,而是:
是否需要通过类型系统强制上层关注
Java 的异常模型背后有两个核心设计思想:
但实践中也暴露出问题:
因此在工程实践中:
RuntimeException 往往成为主流选择
为了避免“分类混乱”,需要把异常放在不同维度下理解。
这是:
Java 语言机制层面的划分
| 来源 | 示例 |
|---|---|
| 业务异常 | 未授权、余额不足 |
| 系统异常 | 空指针、数组越界 |
| 基础设施异常 | 网络超时、数据库异常 |
| 类型 | 策略 |
|---|---|
| 可恢复 | 重试、降级 |
| 不可恢复 | 快速失败 |
| 范围 | 方式 |
|---|---|
| 模块内部 | 直接处理 |
| 服务边界 | 转换为错误码 |
| 跨系统 | 封装为 Result |
异常机制的最大问题是:
异常处理是复杂度的放大器
不合理的异常设计,会让系统变得:
对外接口的异常必须稳定:
接口的异常变化 = 接口契约变化
一个稳定系统中的异常结构应为:
Controller
↓
Service -> BusinessException
↓
DAO -> InfrastructureException
不同层次之间应进行异常转换:
| 层次 | 策略 |
|---|---|
| DAO | 转为数据访问异常 |
| Service | 转为业务异常 |
| API | 转为错误码 |
在跨系统调用中:
❗ 不应该直接抛异常
而应该使用:
Result<T> {
code
message
data
}
原因:
try {
// 正常路径
} catch (SpecificException e) {
// 异常路径
} finally {
// 资源清理
}
其本质是:
把控制流一分为二:
- 正常路径
- 异常路径
finally 的语义是:
资源一致性保证,而非业务逻辑承载
因此:
| 行为 | 开销 |
|---|---|
| try-catch 本身 | 很小 |
| 真正抛异常 | 很大 |
因为抛异常时 JVM 需要:
这是一个“重型操作”。
| 类型 | 处理方式 |
|---|---|
| 业务异常 | 明确返回给用户 |
| 可恢复异常 | 重试/降级 |
| 不可恢复异常 | 快速失败 |
推荐统一异常网关:
GlobalExceptionHandler
↓
日志记录
↓
统一响应
异常处理必须配套:
以下都是常见的异常反模式:
可以用一句话总结:
异常设计 = 复杂度管理
一个优秀的异常设计应该做到: