Java 异常处理:从踩坑到精通
凌晨 2 点,你被生产告警叫醒。日志里赫然写着:
Exception in thread "main" java.lang.NullPointerException
at com.example.UserService.getUser(UserService.java:23)你翻遍代码,发现有人在 catch 块里写了个 // ignore,然后把异常吞了。
这不是故事,这是 Java 异常处理中最常见的三大原罪。
异常的三层世界
Java 的异常体系像一座冰山:
Throwable
├── Error ← JVM 的锅,别伸手
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── IOException ← 受检异常,必须处理
├── RuntimeException ← 非受检异常,运行时才暴露
└── ...| 层次 | 处理策略 | 例子 |
|---|---|---|
| Error | 别管,让程序自生自灭 | OOM、StackOverflow |
| Checked Exception | 必须捕获或声明 | IOException、SQLException |
| Unchecked Exception | 应该预防而非捕获 | NPE、IllegalArgumentException |
记住这个原则:Error 不用管,Checked 要处理,Unchecked 要预防。
捕获异常的艺术
精准捕获,别一把抓
java
// ❌ 见过太多这样的代码
try {
doSomething();
} catch (Exception e) {
// 什么异常都能进来,鬼知道发生了什么
}
// ✅ 推荐:捕获具体异常
try (FileInputStream fis = new FileInputStream(path)) {
fis.read();
} catch (FileNotFoundException e) {
log.warn("配置文件不存在,使用默认配置", e);
} catch (IOException e) {
log.error("读取配置文件失败: {}", path, e);
throw new ConfigLoadException("无法加载配置", e);
}异常链:留住根因
异常被包装后,原始堆栈不能丢:
java
public void process() {
try {
doSomething();
} catch (IOException e) {
// ❌ 直接抛新异常,根因丢失
throw new BusinessException("处理失败");
// ✅ 保留原始异常,堆栈链完整
throw new BusinessException("处理失败", e);
}
}这样做的好处是:日志里会显示完整的异常链 BusinessException ← IOException ← FileNotFoundException,根因一目了然。
不要忽略异常
这是最容易被忽略的:
java
// ❌ 灾难级:吞掉异常
try {
connection.close();
} catch (IOException e) {
// 什么都没做,资源泄漏了都不知道
}
// ❌ 次等糟糕:假装处理
try {
file.close();
} catch (IOException e) {
log.debug("关闭文件失败"); // debug 级别,生产根本看不到
}
// ✅ 最低限度:记录日志
try {
file.close();
} catch (IOException e) {
log.error("关闭文件失败: {}", file.getPath(), e);
}
// ✅ 最佳实践:try-with-resources
try (FileInputStream fis = new FileInputStream(file)) {
// 自动关闭,无需手动处理
}自定义异常:正确打开方式
大多数项目的自定义异常长这样:
java
// ❌ 自以为是的异常
public class MyException extends Exception {}
// ✅ 有信息量的异常
public class BusinessException extends RuntimeException {
private final String code;
private final Map<String, Object> context;
public BusinessException(String message) {
super(message);
this.code = "BUSINESS_ERROR";
this.context = Collections.emptyMap();
}
public BusinessException(String code, String message) {
super(message);
this.code = code;
this.context = Collections.emptyMap();
}
public BusinessException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.context = Collections.emptyMap();
}
public String getCode() {
return code;
}
public Map<String, Object> getContext() {
return context;
}
}带错误码的异常在分布式系统中是标配,方便前端做分支处理。
常见陷阱
1. 在 finally 里抛异常
java
// ❌ finally 里的异常会覆盖 try 里的异常
try {
doSomething();
} finally {
close(); // 如果这里抛异常,doSomething 的异常就丢了
}
// ✅ 使用 try-with-resources,或在 finally 里只做日志
try {
doSomething();
} finally {
try {
close();
} catch (Exception e) {
log.warn("关闭资源失败", e); // 吞掉次要异常
}
}2. 捕获异常后继续执行
java
// ❌ 捕获后假装没事
try {
sendEmail();
} catch (Exception e) {
log.error("发送邮件失败", e);
}
processOrder(); // 继续执行?订单发出去没?这是分布式系统中事务不一致的常见原因。要么往外抛,要么做好补偿。
3. 用异常做流程控制
java
// ❌ 别用异常来控制业务流
try {
user = userMap.get(username);
user.getId();
} catch (NullPointerException e) {
// 创建用户...
}
// ✅ 用 Optional 或 null 检查
User user = userMap.get(username);
if (user == null) {
// 创建用户...
}异常创建成本很高,用来控制流程是性能大忌。
总结
异常处理的精髓就三句话:
- 精准捕获:什么异常就抓什么,别
catch (Exception e) - 保留根因:包装异常时把 cause 传进去
- 别吞异常:要么处理,要么抛出去,别假装没看见
好的异常处理,是让未来的自己少熬几个夜。
