1. 什么是抑制异常

在 Java 中,抑制异常(Suppressed Exception) 指的是:某个异常本应被抛出,但由于另一个异常的出现而被“掩盖”或“压制”,最终没有直接暴露给调用方。这种情况最常出现在 finally 块中抛出异常时。

举个典型场景:
try 块里抛了个异常 A,但随后 finally 块执行时又抛了异常 B。此时 JVM 会优先抛出 B,而 A 就被“抑制”了——如果不做特殊处理,开发者很难发现最初的异常到底是什么,这就成了一个经典踩坑点

从 Java 7 开始,Throwable 类新增了两个关键方法来解决这个问题:

  • addSuppressed(Throwable):手动添加一个被抑制的异常
  • getSuppressed():获取所有被抑制的异常数组

此外,Java 7 还引入了 try-with-resources 机制,它底层自动利用了这些方法,能更优雅地处理资源关闭时的异常叠加问题。


2. 抑制异常实战演示

2.1 经典踩坑场景:finally 中的 NullPointerException

来看一段看似正常、实则隐患重重的代码:

public static void demoSuppressedException(String filePath) throws IOException {
    FileInputStream fileIn = null;
    try {
        fileIn = new FileInputStream(filePath);
    } catch (FileNotFoundException e) {
        throw new IOException(e);
    } finally {
        fileIn.close();
    }
}

⚠️ 问题出在 finally 块中的 fileIn.close()

  • 如果文件路径无效,fileIn 初始化失败(为 null
  • 此时进入 finally,调用 null.close() 直接触发 NullPointerException
  • 原本有意义的 FileNotFoundException 被完全掩盖

测试一下就知道有多坑:

@Test(expected = NullPointerException.class)
public void givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException() throws IOException {
    demoSuppressedException("/non-existent-path/non-existent-file.txt");
}

运行结果只看到 NullPointerException,根本看不出“文件不存在”才是根因。这种日志查半天都定位不到问题,简直是线上故障排查的噩梦。


2.2 手动添加抑制异常:addSuppressed 的正确用法

我们可以通过手动管理异常,把原始异常保留下来:

public static void demoAddSuppressedException(String filePath) throws IOException {
    Throwable firstException = null;
    FileInputStream fileIn = null;
    try {
        fileIn = new FileInputStream(filePath);
    } catch (IOException e) {
        firstException = e; // 记录 try 块中的异常
    } finally {
        try {
            if (fileIn != null) {
                fileIn.close();
            }
        } catch (NullPointerException | IOException closeException) {
            if (firstException != null) {
                closeException.addSuppressed(firstException); // 把原始异常塞进去
            }
            throw closeException;
        }
        // 如果 finally 没有异常,但 try 有异常,也要抛出去
        if (firstException != null && !(firstException instanceof RuntimeException)) {
            throw (IOException) firstException;
        }
    }
}

✅ 单元测试验证效果:

@Test
public void whenFileNotExists_thenOriginalExceptionIsSuppressed() {
    try {
        demoAddSuppressedException("/non-existent-path/non-existent-file.txt");
    } catch (Exception e) {
        assertThat(e, instanceOf(NullPointerException.class));
        assertEquals(1, e.getSuppressed().length);
        assertThat(e.getSuppressed()[0], instanceOf(FileNotFoundException.class));
    }
}

现在我们不仅能捕获到 NullPointerException,还能通过 getSuppressed() 拿到真正的罪魁祸首 FileNotFoundException,排查效率直接拉满。


2.3 try-with-resources 自动处理抑制异常(推荐做法)

Java 7 引入的 try-with-resources 不仅简化了资源管理,更重要的是——它自动处理了抑制异常的逻辑,无需手动干预。

我们先定义一个会抛异常的资源类:

public class ExceptionalResource implements AutoCloseable {
    
    public void processSomething() {
        throw new IllegalArgumentException("Thrown from processSomething()");
    }

    @Override
    public void close() throws Exception {
        throw new NullPointerException("Thrown from close()");
    }
}

然后在 try-with-resources 中使用它:

public static void demoExceptionalResource() throws Exception {
    try (ExceptionalResource exceptionalResource = new ExceptionalResource()) {
        exceptionalResource.processSomething();
    }
}

✅ 测试结果如下:

@Test
public void whenResourceThrowsInTryAndClose_thenPrimaryExceptionIsPreserved() {
    try {
        demoExceptionalResource();
    } catch (Exception e) {
        // 主异常来自 try 块
        assertThat(e, instanceOf(IllegalArgumentException.class));
        assertEquals("Thrown from processSomething()", e.getMessage());
        
        // close() 抛出的异常被抑制
        assertEquals(1, e.getSuppressed().length);
        assertThat(e.getSuppressed()[0], instanceOf(NullPointerException.class));
        assertEquals("Thrown from close()", e.getSuppressed()[0].getMessage());
    }
}

📌 关键结论:

  • try-with-resources 保证:try 块中抛出的异常是主异常
  • close() 方法抛出的异常会被自动作为“抑制异常”附加到主异常上
  • ⚠️ 不用手动写 addSuppressed,JVM 自动帮你完成

这正是为什么现代 Java 开发中,凡是实现了 AutoCloseable 的资源(如 InputStream、Connection、Statement 等),都强烈建议使用 try-with-resources 而非传统 try-finally


3. 总结

要点 说明
✅ 抑制异常本质 当多个异常发生时,JVM 只抛一个,其余被“压制”
✅ 典型场景 finally 块抛异常,掩盖了 try 块的真实问题
✅ 解决方案 使用 addSuppressed()getSuppressed() 手动传递异常链
✅ 最佳实践 优先使用 try-with-resources,由 JVM 自动处理抑制异常
✅ 注意点 try-with-resources 中,close 异常会被抑制,try 中的异常才是主异常

💡 小贴士:你在写工具类或中间件时,如果需要手动管理资源并处理异常,记得模仿 try-with-resources 的行为,合理使用 addSuppressed,否则很容易让调用方陷入“只看到表层异常”的窘境。

所有示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/core-java-modules/core-java-exceptions-2


原始标题:Java Suppressed Exceptions