Skip to content

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) {
    // 创建用户...
}

异常创建成本很高,用来控制流程是性能大忌。

总结

异常处理的精髓就三句话:

  1. 精准捕获:什么异常就抓什么,别 catch (Exception e)
  2. 保留根因:包装异常时把 cause 传进去
  3. 别吞异常:要么处理,要么抛出去,别假装没看见

好的异常处理,是让未来的自己少熬几个夜。

基于 VitePress 构建