刚工作那会儿,我写的catch块只有一行:e.printStackTrace()。后来线上出问题,日志里啥也看不出来,才知道异常处理这事儿没那么简单。
坑一:catch住不处理
try {
orderService.create(order);
} catch (Exception e) {
// 啥也不写
}
这是最致命的写法。异常被吞掉,程序继续往下跑,数据可能已经处于不一致状态,而你完全不知道。
正确的做法至少要做三件事之一:记录日志、向上抛出、或者做降级处理。
坑二:catch Exception一把梭
try {
// 业务逻辑
} catch (Exception e) {
log.error("出错了", e);
}
Exception太宽了,你根本不知道发生了什么。业务异常应该被区分对待——参数校验失败、库存不足,这些是预期内的,不需要报警;而NPE、数据库连接失败,这些才是真要命的。
try {
orderService.create(order);
} catch (BizException e) {
// 业务异常,返回友好提示
return Result.fail(e.getMessage());
} catch (Exception e) {
// 系统异常,记录完整堆栈并报警
log.error("创建订单失败, order={}", order, e);
return Result.fail("系统繁忙,请稍后重试");
}
坑三:finally里return
public int getValue() {
try {
return 1;
} finally {
return 2; // 覆盖了try里的return
}
}
// 返回2,不是1
finally里的return会覆盖try里的返回值,而且会吞掉try中抛出的异常。这属于"语法允许但绝对不要写"的范畴。
坑四:在finally里关闭资源却不判空
Connection conn = null;
try {
conn = getConnection();
// ...
} finally {
conn.close(); // conn可能为null,抛NPE
}
现在都用try-with-resources了,这个问题自然解决:
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 自动关闭,异常也能正确传播
}
坑五:自定义异常丢了cause
// 错误写法
try {
parseConfig();
} catch (IOException e) {
throw new ConfigException("配置加载失败");
}
// 正确写法:保留原始异常
try {
parseConfig();
} catch (IOException e) {
throw new ConfigException("配置加载失败", e);
}
不传cause,堆栈就断在这里了,排查问题时只能看到"配置加载失败",看不到真正的IOException在哪一行。
我的几条实践原则
| 原则 | 说明 |
|---|---|
| 不要吞异常 | 至少记日志 |
| 不要catch Exception | 精确捕获,分别处理 |
| 不要用异常控制流程 | 能用if判断就别抛异常 |
| 异常信息要带上下文 | 把关键参数打进日志 |
| 自定义异常保留cause | 别断掉堆栈链 |
异常处理写得好不好,平时看不出来,一到线上排障就高下立判。共勉。