---
title: "Java 清理失败盖住了原始错误：用 suppressed 找回故障链"
date: 2026-11-13
slug: java-try-with-resources-suppressed
canonical: https://kernelranch.com/blog/2026/11/java-try-with-resources-suppressed
markdown_url: https://kernelranch.com/blog/2026/11/java-try-with-resources-suppressed.md
tags: ["java", "exceptions", "resources"]
---导入失败后，日志只有关闭文件的异常，最早的解析错误丢了。手写 finally 中抛出的异常可能覆盖正在传播的异常。try-with-resources 会保留主体异常，并通过 getSuppressed 提供关闭时的异常。

## 1. 同时注入解析和关闭故障

保存为 Lab.java，使用 JDK 17 运行 java -ea Lab.java。

```java
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

class Lab {
    record Resource(String name, List<String> closed) implements AutoCloseable {
        public void close() throws IOException {
            closed.add(name);
            throw new IOException("close " + name);
        }
    }

    static void manual(List<String> closed) throws IOException {
        var input = new Resource("input", closed);
        try {
            throw new IOException("parse failed");
        } finally {
            input.close();
        }
    }

    public static void main(String[] args) {
        var closed = new ArrayList<String>();
        try {
            manual(closed);
            throw new AssertionError("missing exception");
        } catch (IOException error) {
            assert error.getMessage().equals("close input");
            System.out.println("finally primary: " + error.getMessage());
        }

        closed.clear();
        try (var input = new Resource("input", closed);
             var output = new Resource("output", closed)) {
            throw new IOException("parse failed");
        } catch (IOException error) {
            assert error.getMessage().equals("parse failed");
            assert closed.equals(List.of("output", "input"));
            var suppressed = error.getSuppressed();
            assert suppressed.length == 2;
            assert suppressed[0].getMessage().equals("close output");
            assert suppressed[1].getMessage().equals("close input");
            System.out.println("resource primary: " + error.getMessage());
            System.out.println("close order: " + closed);
            for (var failure : suppressed) {
                System.out.println("suppressed: " + failure.getMessage());
            }
        }
    }
}
```

本机实测输出如下。

```text
finally primary: close input
resource primary: parse failed
close order: [output, input]
suppressed: close output
suppressed: close input
```

## 2. 日志和事务各管什么

两个资源按声明的逆序关闭。关闭也可能是业务操作的一部分，例如输出流在 close 时刷新最后一批字节。主体正常结束时，首个关闭异常会成为主异常，因此成功计数应放在 try 块退出之后。

| 现场现象 | 检查位置 |
|---|---|
| 最早异常消失 | finally 中的关闭或清理是否抛出新异常 |
| 关闭错误没有入日志 | 日志调用是否传入完整 Throwable |
| 写入看似成功但文件损坏 | 输出流关闭成功前是否已发布文件 |

捕获异常后若要增加上下文，保留原异常作为 cause，这样它的 suppressed 信息依然可以查看。外部事务是否回滚由事务边界决定，关闭动作本身按资源实现处理。

## 小结

先读主异常定位首个失败，再沿 suppressed 检查清理阶段。把资源块完整退出作为记录成功的条件。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| Java 17 语言规范 §14.20.3 | https://docs.oracle.com/javase/specs/jls/se17/html/jls-14.html#jls-14.20.3 | 关闭顺序、主异常与 suppressed |

## 核验说明

- OpenJDK 17.0.19 启用断言运行。关闭故障由测试资源注入。
