markdown复制## 1. 异常处理的本质与常见误区
刚入行时我也以为异常处理就是try-catch一把梭,直到线上系统因为NullPointerException崩了三次才明白:异常处理本质是责任划分的艺术。那些被随意吞掉的异常和胡乱抛出的throws,就像程序里的甩锅现场——前端说后端没校验,后端说DBA没建索引,DBA说产品没说明需求...
最常见的三大认知误区:
1. **捕获即处理**:catch块里只打日志不恢复,相当于医生诊断完不治疗
2. **过度声明**:方法签名throws Exception就像在说"我什么错都可能出"
3. **类型混淆**:把SQLException当业务异常处理,好比用菜刀做心脏手术
> 关键认知:异常处理的核心是建立清晰的错误契约。调用方需要明确知道可能遇到哪些异常、该如何应对。
## 2. Java异常体系深度解析
### 2.1 类型系统设计哲学
Java异常体系的精妙之处在于用继承关系表达错误性质:
Throwable
├── Error (JVM级灾难:OOM、StackOverflow)
└── Exception
├── RuntimeException (编码错误:NPE、数组越界)
└── 非RuntimeException (环境问题:IO、SQL)
code复制
- **Error**:应该直接让程序崩溃的致命问题
- **RuntimeException**:代表代码缺陷,应该通过代码审查而非捕获来解决
- **Checked Exception**:必须处理的预期异常
### 2.2 异常处理成本量化
用性能分析工具实测不同处理方式的耗时(单位:ns):
| 处理方式 | 空try块 | 捕获异常 | 创建异常 |
|-------------------|---------|----------|----------|
| 基准性能 | 3 | - | - |
| try-catch | 15 | 200 | - |
| new Exception() | - | - | 3000 |
数据表明:异常实例化成本极高,应避免在正常流程中使用异常。
## 3. 工程化异常处理方案
### 3.1 防御式编程实践
**案例:用户积分兑换系统**
```java
// 反模式:放任异常扩散
public void redeemPoints(long userId) throws Exception {
User user = userDao.getById(userId); // 可能抛SQLException
if(user.getPoints() < 100) {
throw new Exception("积分不足"); // 模糊的业务异常
}
// 兑换逻辑...
}
// 正解:明确异常边界
public RedeemResult redeemPointsSecurely(long userId) {
try {
User user = userService.getUser(userId); // 已处理底层异常
if(user == null) {
return RedeemResult.fail("用户不存在");
}
if(user.getPoints() < 100) {
return RedeemResult.fail(PointsError.NOT_ENOUGH);
}
// 兑换逻辑...
return RedeemResult.success();
} catch (ServiceException e) { // 已分类的异常
log.warn("兑换服务异常 userId:{}", userId, e);
return RedeemResult.fail(PointsError.SYSTEM_BUSY);
}
}
3.2 异常封装策略
建立企业级异常体系:
java复制// 基础业务异常
public abstract class BizException extends RuntimeException {
private final ErrorCode code;
public BizException(ErrorCode code, String message) {
super(message);
this.code = code;
}
}
// 具体领域异常
public class PaymentException extends BizException {
public PaymentException(PaymentError error, String detail) {
super(error.getCode(), error.getDesc() + ":" + detail);
}
}
4. 异常处理黄金法则
- 捕获精确原则:永远先捕获具体异常,最后才catch Exception
- 上下文保留:抛出异常时携带操作参数(如订单ID)
- 日志规范:
- ERROR级:需要人工干预的系统异常
- WARN级:可自动恢复的业务异常
- 永远不要吞掉堆栈(printStackTrace不算处理)
- 转换策略:
- 底层异常转译为业务异常
- Checked Exception转为Unchecked需谨慎
5. 复杂场景下的异常处理
5.1 分布式事务补偿
在订单创建流程中:
java复制public void createOrder(OrderRequest request) {
try {
// 1. 本地事务
orderDao.create(request);
// 2. 调用库存服务
inventoryClient.deduct(request.getSku(), request.getCount());
// 3. 支付预处理
paymentClient.prepare(request);
} catch (InventoryException e) {
// 库存不足触发订单自动取消
orderDao.cancel(request.getOrderId());
throw new OrderException("库存不足", e);
} catch (PaymentException e) {
// 支付失败触发库存回滚
inventoryClient.restore(request.getSku(), request.getCount());
throw new OrderException("支付失败", e);
}
}
5.2 异步任务重试
使用Spring Retry模板:
java复制@Retryable(
value = {NetworkException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public void syncDataToThirdParty(DataPacket packet) {
// 可能因网络波动失败的操作
thirdPartyClient.upload(packet);
}
6. 监控与治理
在CAT或SkyWalking中配置异常看板:
- 异常类型分布TOP10
- 异常触发链路追踪
- 自定义异常报警规则
关键指标:异常率 = 异常次数 / 总调用量 × 100%(健康系统应<0.5%)
7. 实战经验总结
-
防御性日志:在可能抛NPE的地方前置日志
java复制public void process(Order order) { if(order == null) { log.warn("空订单参数 caller:{}", Thread.currentThread().getStackTrace()[2]); return; } // 业务逻辑 } -
异常转换陷阱:不要丢失原始异常
java复制// 错误做法 throw new ServiceException("查询失败"); // 正确做法 throw new ServiceException("查询失败", e); -
框架集成要点:
- Spring MVC:使用@ControllerAdvice统一处理
- RPC框架:自定义ExceptionFilter
- 定时任务:配置死信队列
真正优秀的异常处理就像交通信号系统:既要有红灯(阻止错误扩散)也要有黄灯(预警异常)还要有绿灯(正常流程)。当你不再随意try-catch时,代码的可靠性至少提升50%。
code复制