Guava:资源关闭与异常处理

欢迎你来读这篇博客,这篇博客主要是关于Guava 资源关闭与异常处理
其中包括 Closer 的使用方式、多资源关闭、异常传播、suppressed exception、与 Java 7 try-with-resources 的对比、为什么 Java 7 之后 Closer 使用减少,以及资源关闭的最佳实践。

序言

在 Java 后端开发中,资源关闭是一个非常基础但非常容易出问题的主题。

你可能每天都在使用这些资源:

  • InputStream
  • OutputStream
  • Reader
  • Writer
  • Socket
  • Channel
  • Connection
  • ResultSet
  • PreparedStatement
  • ZipInputStream
  • BufferedReader

这些对象大多都需要关闭。

如果不关闭,可能会导致:

  • 文件句柄泄漏;
  • 连接泄漏;
  • 内存占用增加;
  • 临时文件无法删除;
  • 数据没有 flush;
  • Socket 长时间占用;
  • 数据库连接池耗尽;
  • 线上服务慢慢变成“资源黑洞”。

在 Java 7 之前,资源关闭通常依赖 try-finally

代码会非常啰嗦,尤其是多个资源一起使用时。

Guava 提供了一个工具类:

1
com.google.common.io.Closer

它的作用是:

统一注册多个可关闭资源,在 finally 中按顺序关闭,并正确处理关闭过程中的异常。

这一章对应原课程:

1
14 Guava 之 Closer 使用和原理剖析

本章重点包括:

1
2
第 6 章:资源关闭与异常处理
6.1 Closer 使用和原理剖析

核心内容:

  • Closer 的使用方式;
  • 多资源关闭;
  • 异常传播;
  • suppressed exception;
  • try-with-resources 对比;
  • 为什么 Closer 在 Java 7 之后使用减少;
  • 资源关闭的最佳实践。

这章看起来只是一个工具类,但背后其实是 Java 异常处理和资源管理的核心问题。

如果你能真正理解 Closertry-with-resources,你写 IO、数据库、网络、文件导入导出时会稳很多。

正文

chapter 1:为什么资源关闭很重要

资源关闭不是“代码洁癖”,而是系统稳定性问题。

例如读取文件:

1
2
3
InputStream inputStream = new FileInputStream(file);

byte[] bytes = inputStream.readAllBytes();

如果不关闭:

1
inputStream.close();

文件句柄可能一直占用。

在开发环境可能没感觉。

但在线上服务中,如果每个请求都打开文件、网络连接或数据库连接而不关闭,服务会慢慢积累资源泄漏。

最终可能出现:

1
2
3
4
Too many open files
Connection pool exhausted
No space left on device
File is locked

所以,谁打开资源,谁就应该负责关闭。

chapter 2:Java 7 之前的资源关闭写法

Java 7 之前,没有 try-with-resources

典型写法是:

1
2
3
4
5
6
7
8
9
10
11
InputStream inputStream = null;

try {
inputStream = new FileInputStream("data.txt");

// 使用 inputStream
} finally {
if (inputStream != null) {
inputStream.close();
}
}

如果只有一个资源,还能接受。

如果有两个资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
InputStream inputStream = null;
OutputStream outputStream = null;

try {
inputStream = new FileInputStream("source.txt");
outputStream = new FileOutputStream("target.txt");

// copy
} finally {
if (outputStream != null) {
outputStream.close();
}

if (inputStream != null) {
inputStream.close();
}
}

如果关闭时也可能抛异常,就更麻烦。

如果 outputStream.close() 抛异常,后面的 inputStream.close() 可能就不执行了。

于是你还要写嵌套 try-catch。

代码很快变得又长又丑。

chapter 3:多资源关闭的典型问题

多资源关闭有几个难点。

1. 关闭顺序

通常应该后打开的先关闭。

例如:

1
InputStream -> BufferedInputStream

使用时先打开底层流,再包装缓冲流。

关闭时应该先关闭外层流。

也就是:

1
后注册,先关闭

2. 关闭异常不能阻止其他资源关闭

如果关闭第一个资源失败,也应该继续关闭其他资源。

否则可能造成更多泄漏。

3. 主异常不能被关闭异常覆盖

如果业务代码抛出异常:

1
读取文件失败

同时关闭资源也抛异常:

1
关闭流失败

我们更关心主异常。

关闭异常应该作为 suppressed exception 附加到主异常上,而不是覆盖主异常。

4. 代码不能太啰嗦

手写多层 try-finally 很容易出错,也不易维护。

Closer 就是为了解决这些问题。

chapter 4:Closer 是什么

Closer 是 Guava 提供的资源关闭工具类。

包名:

1
com.google.common.io.Closer

它的核心作用是:

注册多个 Closeable 资源,在 finally 中统一关闭,并正确处理异常传播和 suppressed exception。

典型用法:

1
2
3
4
5
6
7
8
9
10
11
12
Closer closer = Closer.create();

try {
InputStream in = closer.register(new FileInputStream("source.txt"));
OutputStream out = closer.register(new FileOutputStream("target.txt"));

// 使用资源
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}

这段代码有几个关键点:

  1. 创建 Closer
  2. 使用 closer.register() 注册资源;
  3. catch 中使用 closer.rethrow(e) 传播异常;
  4. finally 中调用 closer.close() 关闭所有资源。

chapter 5:Closer 的基础使用示例

下面写一个文件复制示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import com.google.common.io.Closer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class CloserCopyDemo {

public void copy(String sourcePath, String targetPath) throws Exception {
Closer closer = Closer.create();

try {
InputStream inputStream = closer.register(new FileInputStream(sourcePath));
OutputStream outputStream = closer.register(new FileOutputStream(targetPath));

byte[] buffer = new byte[8192];

int length;

while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
}

这里的资源注册顺序是:

1
2
inputStream
outputStream

关闭顺序是反过来:

1
2
outputStream
inputStream

这符合常见资源关闭习惯。

chapter 6:Closer.register 的作用

closer.register(resource) 做两件事:

  1. 把资源加入 Closer 内部维护的栈;
  2. 返回这个资源本身。

所以可以这样写:

1
InputStream inputStream = closer.register(new FileInputStream(sourcePath));

不用写成:

1
2
InputStream inputStream = new FileInputStream(sourcePath);
closer.register(inputStream);

当然两种都可以。

register 返回资源本身,是为了让代码更紧凑。

注册后的资源会在:

1
closer.close()

中统一关闭。

chapter 7:Closer 支持什么资源

Closer 主要支持:

1
java.io.Closeable

例如:

  • InputStream
  • OutputStream
  • Reader
  • Writer
  • RandomAccessFile
  • ZipInputStream
  • Socket

Java 7 之后更通用的是:

1
AutoCloseable

try-with-resources 支持 AutoCloseable

而 Guava Closer 主要围绕 Closeable 设计。

这也是 Java 7 之后 Closer 使用减少的原因之一。

因为 JDK 原生 try-with-resources 更标准、更通用。

chapter 8:Closer 的关闭顺序

Closer 内部通常会用类似栈的结构保存资源。

注册顺序:

1
2
3
closer.register(resource1);
closer.register(resource2);
closer.register(resource3);

关闭顺序:

1
2
3
resource3
resource2
resource1

也就是:

后注册的资源先关闭。

这和嵌套资源的关闭需求一致。

例如:

1
2
InputStream fileInputStream = closer.register(new FileInputStream(file));
BufferedInputStream bufferedInputStream = closer.register(new BufferedInputStream(fileInputStream));

关闭时先关闭:

1
BufferedInputStream

再关闭:

1
FileInputStream

这符合包装流的关闭习惯。

chapter 9:为什么关闭顺序要反过来

资源经常存在依赖关系。

例如:

1
2
FileInputStream fileInputStream = new FileInputStream(file);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

BufferedInputStream 包装了 FileInputStream

使用时是:

1
底层资源 -> 外层包装资源

关闭时应该:

1
外层包装资源 -> 底层资源

因为外层资源关闭时可能还需要 flush、读取或处理底层资源。

如果先关闭底层资源,外层资源关闭可能失败。

所以栈式关闭是合理的。

chapter 10:Closer 的异常传播

Closer 的经典写法中,catch 部分是:

1
2
3
catch (Throwable e) {
throw closer.rethrow(e);
}

这看起来有点奇怪。

为什么不是:

1
throw e;

原因是:

Closer 需要记录主异常,以便 close 阶段如果也发生异常,可以把关闭异常作为 suppressed exception 处理。

closer.rethrow(e) 会把当前异常记录到 Closer 内部。

然后在 finally 中:

1
closer.close();

如果关闭资源又发生异常,Closer 会知道已经有一个主异常存在。

它就可以选择:

  • 保留主异常;
  • 把关闭异常附加为 suppressed exception;
  • 不让关闭异常覆盖主异常。

这就是 closer.rethrow(e) 的核心意义。

chapter 11:如果不使用 closer.rethrow 会怎样

如果写成:

1
2
3
4
5
6
7
try {
// 使用资源
} catch (Throwable e) {
throw e;
} finally {
closer.close();
}

问题是:

Closer 不知道 try 块中已经发生了主异常。

closer.close() 中关闭资源也抛异常时,关闭异常可能覆盖主异常。

最终你看到的异常可能是:

1
close failed

而真正业务失败原因:

1
read failed

被隐藏了。

这会导致排查问题非常痛苦。

所以使用 Closer 时,要遵循它的标准写法:

1
2
3
4
5
catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}

chapter 12:suppressed exception 是什么

suppressed exception 是 Java 7 引入的异常机制。

它用于表达:

一个主异常发生后,在资源关闭过程中又发生了其他异常,这些关闭异常不应该覆盖主异常,而应该附加到主异常上。

例如:

1
2
3
主异常:业务处理失败
suppressed:关闭资源 A 失败
suppressed:关闭资源 B 失败

在 Java 中,Throwable 有方法:

1
2
addSuppressed(Throwable exception)
getSuppressed()

示例:

1
2
3
4
5
6
7
8
9
10
11
public class SuppressedExceptionDemo {

public static void main(String[] args) {
RuntimeException main = new RuntimeException("main exception");

main.addSuppressed(new RuntimeException("close resource 1 failed"));
main.addSuppressed(new RuntimeException("close resource 2 failed"));

main.printStackTrace();
}
}

输出中会看到:

1
2
Suppressed: java.lang.RuntimeException: close resource 1 failed
Suppressed: java.lang.RuntimeException: close resource 2 failed

这就是 suppressed exception。

chapter 13:为什么 suppressed exception 很重要

假设业务执行失败:

1
文件复制过程中读取失败

然后关闭流也失败:

1
关闭输出流失败

如果关闭异常覆盖主异常,你会看到:

1
关闭输出流失败

但真正的问题是读取失败。

这会误导排查方向。

正确做法是:

1
2
主异常:文件读取失败
suppressed:关闭输出流失败

这样你既知道主错误,也没有丢失关闭错误。

Closertry-with-resources 都是为了解决这个问题。

chapter 14:Closer 多资源异常示例

我们写一个会在关闭时抛异常的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.Closeable;
import java.io.IOException;

public class BrokenCloseable implements Closeable {

private final String name;

public BrokenCloseable(String name) {
this.name = name;
}

public void work() {
System.out.println(name + " working");
}

@Override
public void close() throws IOException {
throw new IOException(name + " close failed");
}
}

使用 Closer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.io.Closer;

public class CloserSuppressedDemo {

public static void main(String[] args) throws Exception {
Closer closer = Closer.create();

try {
BrokenCloseable resource1 = closer.register(new BrokenCloseable("resource1"));
BrokenCloseable resource2 = closer.register(new BrokenCloseable("resource2"));

resource1.work();
resource2.work();

throw new RuntimeException("main work failed");
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
}

理想情况下,主异常是:

1
main work failed

关闭异常会作为 suppressed exception 附加上去。

chapter 15:Closer 与 try-with-resources 对比

Java 7 引入了 try-with-resources

写法:

1
2
3
4
5
6
7
8
9
10
11
try (InputStream inputStream = new FileInputStream(sourcePath);
OutputStream outputStream = new FileOutputStream(targetPath)) {

byte[] buffer = new byte[8192];

int length;

while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
}

这比 Closer 更简洁。

它也会自动:

  • 关闭多个资源;
  • 按反向顺序关闭;
  • 保留主异常;
  • 把关闭异常加入 suppressed exception。

所以 Java 7 之后,很多场景不再需要 Closer。

chapter 16:try-with-resources 的关闭顺序

例如:

1
2
3
4
5
6
try (ResourceA a = new ResourceA();
ResourceB b = new ResourceB();
ResourceC c = new ResourceC()) {

// use resources
}

关闭顺序是:

1
2
3
c
b
a

也就是:

后创建的资源先关闭。

这和 Closer 的关闭顺序一致。

chapter 17:try-with-resources 的 suppressed exception

try-with-resources 内置支持 suppressed exception。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TryWithResourcesSuppressedDemo {

public static void main(String[] args) {
try (BrokenAutoCloseable resource = new BrokenAutoCloseable()) {
throw new RuntimeException("main failed");
} catch (Exception e) {
e.printStackTrace();
}
}

static class BrokenAutoCloseable implements AutoCloseable {

@Override
public void close() throws Exception {
throw new Exception("close failed");
}
}
}

输出会包含:

1
2
RuntimeException: main failed
Suppressed: Exception: close failed

这说明 try-with-resources 已经原生解决了 Closer 要解决的核心问题。

chapter 18:Closer 和 try-with-resources 对比表

对比项 Guava Closer try-with-resources
引入时间 Java 7 之前很有价值 Java 7 引入
是否 JDK 原生
支持资源类型 主要是 Closeable AutoCloseable
多资源关闭 支持 支持
反向关闭顺序 支持 支持
suppressed exception 支持 支持
写法简洁性 较啰嗦 更简洁
推荐程度 老项目维护要会 新项目优先推荐

一句话总结:

Java 7 之前 Closer 很重要,Java 7 之后 try-with-resources 是首选。

chapter 19:为什么 Java 7 之后 Closer 使用减少

原因主要有几个。

1. try-with-resources 成为标准语法

JDK 原生支持,不需要第三方库。

2. 写法更简洁

Closer 写法:

1
2
3
4
5
6
7
8
9
Closer closer = Closer.create();

try {
InputStream in = closer.register(...);
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}

try-with-resources 写法:

1
2
try (InputStream in = new FileInputStream(file)) {
}

明显后者更简洁。

3. 支持 AutoCloseable

try-with-resources 支持:

1
AutoCloseable

Closeable 更通用。

4. suppressed exception 原生支持

Java 7 异常机制本身已经支持 suppressed exception。

5. IDE 和静态检查更友好

现代 IDE 会自动提示资源可以用 try-with-resources。

chapter 20:Closer 现在还值得学吗

值得。

原因有三个。

1. 老项目里可能还在用

很多 Java 6/7 时代的项目仍然有 Guava Closer。

你需要能看懂。

2. 它帮助理解资源关闭原理

Closer 手动展示了:

  • 注册资源;
  • 反向关闭;
  • 主异常记录;
  • 关闭异常 suppress;
  • 异常重新抛出。

这些都是 try-with-resources 背后的机制。

3. 它体现了良好的工具类设计

Closer 把多资源关闭的复杂逻辑封装到一个类里。

这对设计自己的资源管理工具也有参考价值。

所以:

新代码不一定要用 Closer,但一定要理解 Closer。

chapter 21:Closer 源码设计思想

Closer 的源码思想可以简化理解为几部分。

1. 内部维护资源栈

类似:

1
Deque<Closeable> stack = new ArrayDeque<>();

注册资源时:

1
stack.addFirst(closeable);

关闭时从栈顶开始关闭。

2. 保存主异常

当调用:

1
closer.rethrow(e)

时,Closer 会记录这个异常。

类似:

1
this.thrown = e;

3. close 时逐个关闭资源

类似:

1
2
3
4
5
6
7
8
9
while (!stack.isEmpty()) {
Closeable closeable = stack.removeFirst();

try {
closeable.close();
} catch (Throwable closeException) {
// 处理关闭异常
}
}

4. 如果已有主异常,关闭异常作为 suppressed

如果 try 块已经抛出异常,关闭异常不能覆盖它。

于是关闭异常会被 suppress。

5. 如果没有主异常,关闭异常会抛出

如果业务逻辑没有异常,但关闭失败了,那么关闭异常就是主异常。

这就是资源关闭异常处理的核心逻辑。

chapter 22:Closer 简化版源码模拟

下面写一个简化版 SimpleCloser,帮助理解原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;

public class SimpleCloser implements Closeable {

private final Deque<Closeable> stack = new ArrayDeque<>();

private Throwable thrown;

public <T extends Closeable> T register(T closeable) {
if (closeable != null) {
stack.addFirst(closeable);
}

return closeable;
}

public RuntimeException rethrow(Throwable throwable) throws IOException {
this.thrown = throwable;

if (throwable instanceof IOException ioException) {
throw ioException;
}

if (throwable instanceof RuntimeException runtimeException) {
throw runtimeException;
}

if (throwable instanceof Error error) {
throw error;
}

throw new RuntimeException(throwable);
}

@Override
public void close() throws IOException {
Throwable throwable = thrown;

while (!stack.isEmpty()) {
Closeable closeable = stack.removeFirst();

try {
closeable.close();
} catch (Throwable closeException) {
if (throwable == null) {
throwable = closeException;
} else {
throwable.addSuppressed(closeException);
}
}
}

if (thrown == null && throwable != null) {
if (throwable instanceof IOException ioException) {
throw ioException;
}

if (throwable instanceof RuntimeException runtimeException) {
throw runtimeException;
}

if (throwable instanceof Error error) {
throw error;
}

throw new RuntimeException(throwable);
}
}
}

这不是 Guava 的真实源码,只是为了理解核心思想。

真正的 Guava Closer 会有更细致的异常传播和 suppress 处理。

chapter 23:使用 SimpleCloser 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SimpleCloserDemo {

public static void main(String[] args) throws Exception {
SimpleCloser closer = new SimpleCloser();

try {
BrokenCloseable resource1 = closer.register(new BrokenCloseable("resource1"));
BrokenCloseable resource2 = closer.register(new BrokenCloseable("resource2"));

resource1.work();
resource2.work();

throw new RuntimeException("main failed");
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
}

这个例子可以帮助你理解:

  • 为什么要 register;
  • 为什么要 rethrow;
  • 为什么 finally 要 close;
  • 为什么异常不能简单覆盖。

chapter 24:Closer 实战:安全复制文件

虽然新代码更推荐 try-with-resources,但我们先用 Closer 写一个完整文件复制工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.google.common.io.Closer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class CloserFileCopyService {

public void copy(File source, File target) throws Exception {
if (source == null || !source.isFile()) {
throw new IllegalArgumentException("source must be a file");
}

if (target == null) {
throw new IllegalArgumentException("target can not be null");
}

File parent = target.getParentFile();

if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IllegalStateException("create parent dir failed: " + parent.getAbsolutePath());
}

Closer closer = Closer.create();

try {
InputStream inputStream = closer.register(new FileInputStream(source));
OutputStream outputStream = closer.register(new FileOutputStream(target));

byte[] buffer = new byte[8192];

int length;

while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
}

这个示例适合理解 Closer,但如果是 Java 7+ 新代码,可以改成 try-with-resources。

chapter 25:try-with-resources 版本文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class TryWithResourcesFileCopyService {

public void copy(File source, File target) throws Exception {
if (source == null || !source.isFile()) {
throw new IllegalArgumentException("source must be a file");
}

if (target == null) {
throw new IllegalArgumentException("target can not be null");
}

File parent = target.getParentFile();

if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IllegalStateException("create parent dir failed: " + parent.getAbsolutePath());
}

try (InputStream inputStream = new FileInputStream(source);
OutputStream outputStream = new FileOutputStream(target)) {

byte[] buffer = new byte[8192];

int length;

while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
}
}
}

对比之后可以看到:

try-with-resources 更短、更清晰、更标准。

所以新项目优先用它。

chapter 26:Closer 实战:文件导入场景

假设我们要导入 CSV 文件。

流程:

  1. 打开文件输入流;
  2. 包装成 Reader;
  3. 包装成 BufferedReader;
  4. 按行读取;
  5. 统计有效行数;
  6. 关闭资源。

Closer 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.google.common.io.Closer;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class CsvImportWithCloser {

public int importCsv(File file) throws Exception {
Closer closer = Closer.create();

try {
FileInputStream inputStream = closer.register(new FileInputStream(file));

InputStreamReader reader = closer.register(
new InputStreamReader(inputStream, StandardCharsets.UTF_8)
);

BufferedReader bufferedReader = closer.register(new BufferedReader(reader));

int count = 0;

String line;

while ((line = bufferedReader.readLine()) != null) {
if (!line.isBlank()) {
count++;
}
}

return count;
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}
}
}

注意注册顺序:

1
2
3
FileInputStream
InputStreamReader
BufferedReader

关闭顺序:

1
2
3
BufferedReader
InputStreamReader
FileInputStream

chapter 27:try-with-resources 版本 CSV 导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class CsvImportWithTryResources {

public int importCsv(File file) throws Exception {
try (FileInputStream inputStream = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(reader)) {

int count = 0;

String line;

while ((line = bufferedReader.readLine()) != null) {
if (!line.isBlank()) {
count++;
}
}

return count;
}
}
}

这个版本更符合现代 Java 写法。

如果资源都能在 try 头部创建,优先使用这种写法。

chapter 28:什么时候 Closer 仍然有参考价值

虽然新代码优先 try-with-resources,但 Closer 在某些场景仍有参考意义。

1. 资源不是一次性在 try 头部创建

例如资源在多个分支里动态创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
Closer closer = Closer.create();

try {
if (needFile) {
closer.register(new FileInputStream(file));
}

if (needSocket) {
closer.register(socket);
}
} finally {
closer.close();
}

try-with-resources 更适合资源在开头就明确列出。

2. Java 6 老项目

Java 6 没有 try-with-resources。

Closer 是很好的替代方案。

3. 想统一管理动态资源

比如根据配置动态打开多个文件、多个流、多个连接。

4. 学习资源关闭机制

Closer 的设计能帮助你理解 suppressed exception 和反向关闭顺序。

但在现代业务代码里,只要能用 try-with-resources,就优先用它。

chapter 29:资源关闭中的常见错误

1. 忘记关闭资源

1
InputStream in = new FileInputStream(file);

没有 close。

这是最基础也最危险的错误。

2. close 写在 try 末尾

错误:

1
2
3
4
5
InputStream in = new FileInputStream(file);

doSomething(in);

in.close();

如果 doSomething 抛异常,close 不会执行。

3. finally 中 close 覆盖主异常

手写 finally 时,如果 close 抛异常,可能覆盖 try 中异常。

4. 多资源关闭不完整

关闭第一个资源失败后,后面的资源没有关闭。

5. 捕获异常后吞掉

1
2
catch (Exception e) {
}

这会让问题消失在日志之外。

问题没消失,只是你看不见了。

6. 依赖 finalize

不要依赖对象回收时关闭资源。

finalize 已经过时,也不可靠。

7. 不 flush 就关闭不确定目标

虽然很多 close 会自动 flush,但重要输出建议明确理解 flush/close 语义。

chapter 30:资源关闭最佳实践

1. 新代码优先 try-with-resources

1
2
3
try (InputStream in = new FileInputStream(file)) {
// use in
}

2. 多资源按依赖顺序声明

声明顺序:

1
底层资源 -> 外层包装资源

关闭顺序会自动反过来。

3. 字符流必须指定编码

1
new InputStreamReader(inputStream, StandardCharsets.UTF_8)

不要使用默认编码。

4. 谁打开,谁关闭

方法如果接收外部传入的流,通常不要随便关闭,除非方法契约明确说明。

例如:

1
public void process(InputStream inputStream)

一般不应该关闭调用方传进来的流。

但:

1
public void processFile(File file)

方法内部打开的流,方法内部要关闭。

5. 不要吞掉关闭异常

至少记录日志或作为 suppressed exception 保留。

6. 使用工具类减少手写关闭逻辑

现代 Java 用 try-with-resources。

老项目可以用 Closer。

7. 大文件使用流式处理

不要:

1
readAllBytes()

直接读大文件。

8. finally 中清理临时文件

除了关闭流,还要清理临时文件、临时目录。

9. 关闭顺序要考虑包装关系

外层先关闭,底层后关闭。

10. 注意 Spring 管理的资源

数据库连接、事务资源很多时候由 Spring 管理。

不要手动关闭 Spring 管理的资源,除非你明确知道自己在做什么。

chapter 31:Spring Boot 中的资源管理

在 Spring Boot 项目中,很多资源由框架管理。

例如:

  • 数据库连接由连接池管理;
  • 事务由 Spring 管理;
  • HTTP 连接可能由客户端连接池管理;
  • 文件上传临时资源由容器管理;
  • Bean 生命周期由 Spring 管理。

但你自己打开的资源仍然要自己关闭。

例如:

1
2
3
try (InputStream inputStream = multipartFile.getInputStream()) {
// 读取上传内容
}

或者:

1
2
3
try (Stream<Path> stream = java.nio.file.Files.walk(root)) {
// 遍历文件
}

注意 JDK 的 Files.walk 返回的是 Stream<Path>,它也需要关闭。

chapter 32:Files.walk 也要关闭

很多人会忽略这个。

错误写法:

1
2
3
java.nio.file.Files.walk(root)
.filter(java.nio.file.Files::isRegularFile)
.forEach(System.out::println);

推荐:

1
2
3
4
try (Stream<Path> stream = java.nio.file.Files.walk(root)) {
stream.filter(java.nio.file.Files::isRegularFile)
.forEach(System.out::println);
}

因为 Files.walk 底层可能持有目录句柄。

它返回的 Stream 实现了 AutoCloseable

所以要用 try-with-resources。

chapter 33:资源关闭和异常日志

资源关闭失败时,不要简单忽略。

至少要在合适层级记录日志。

例如:

1
2
3
4
5
try {
resource.close();
} catch (IOException e) {
log.warn("close resource failed", e);
}

但如果使用 try-with-resources,关闭异常会自动处理为 suppressed exception。

你可以在统一异常日志中看到 suppressed 信息。

日志系统通常会打印 suppressed exception。

所以不要手写一堆会覆盖主异常的 close 逻辑。

chapter 34:资源关闭和事务边界

文件 IO 和数据库事务混用时要特别小心。

例如:

1
2
3
4
5
6
@Transactional
public void importFile(File file) {
// 读取文件
// 写数据库
// 移动文件到归档目录
}

如果数据库回滚了,但文件已经移动了,会出现不一致。

资源关闭只解决“资源释放”问题,不解决“业务一致性”问题。

真实项目中要考虑:

  • 文件处理成功后再移动;
  • 数据库提交后再归档;
  • 失败文件移动到 error 目录;
  • 记录导入任务状态;
  • 支持重试;
  • 幂等处理;
  • 临时文件清理。

Closer 和 try-with-resources 只是基础能力。

业务一致性还要单独设计。

chapter 35:完整案例:安全文件复制工具

下面写一个现代推荐版本,使用 try-with-resources。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class SafeFileCopyService {

public void copy(File source, File target) throws Exception {
validate(source, target);

createParentDirs(target);

try (InputStream inputStream = new FileInputStream(source);
OutputStream outputStream = new FileOutputStream(target)) {

copy(inputStream, outputStream);
}
}

private void validate(File source, File target) {
if (source == null || !source.isFile()) {
throw new IllegalArgumentException("source must be a file");
}

if (target == null) {
throw new IllegalArgumentException("target can not be null");
}

if (source.equals(target)) {
throw new IllegalArgumentException("source and target can not be same");
}
}

private void createParentDirs(File target) {
File parent = target.getParentFile();

if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IllegalStateException("create parent dir failed: " + parent.getAbsolutePath());
}
}

private void copy(InputStream inputStream, OutputStream outputStream) throws Exception {
byte[] buffer = new byte[8192];

int length;

while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
}
}

这个版本表达很清晰:

  • 校验;
  • 创建父目录;
  • 打开资源;
  • 复制;
  • 自动关闭。

chapter 36:完整案例:使用 Guava ByteStreams 简化复制

如果项目中使用 Guava,可以用:

1
ByteStreams.copy(inputStream, outputStream)

简化复制逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.google.common.io.ByteStreams;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class GuavaByteStreamsCopyService {

public void copy(File source, File target) throws Exception {
validate(source, target);

createParentDirs(target);

try (InputStream inputStream = new FileInputStream(source);
OutputStream outputStream = new FileOutputStream(target)) {

ByteStreams.copy(inputStream, outputStream);
}
}

private void validate(File source, File target) {
if (source == null || !source.isFile()) {
throw new IllegalArgumentException("source must be a file");
}

if (target == null) {
throw new IllegalArgumentException("target can not be null");
}

if (source.equals(target)) {
throw new IllegalArgumentException("source and target can not be same");
}
}

private void createParentDirs(File target) {
File parent = target.getParentFile();

if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IllegalStateException("create parent dir failed: " + parent.getAbsolutePath());
}
}
}

这里我们没有使用 Closer。

因为 Java 7+ try-with-resources 更推荐。

但 Guava 的 ByteStreams.copy 仍然可以用于底层复制。

chapter 37:完整案例:使用 ByteSource 和 ByteSink

如果继续使用 Guava IO 抽象,可以写得更高层:

1
2
3
4
5
6
7
8
9
10
11
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;

import java.io.IOException;

public class SourceSinkCopyService {

public long copy(ByteSource source, ByteSink sink) throws IOException {
return source.copyTo(sink);
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.io.Files;

import java.io.File;

public class SourceSinkCopyDemo {

public static void main(String[] args) throws Exception {
File source = new File("data/source.txt");
File target = new File("data/target.txt");

SourceSinkCopyService service = new SourceSinkCopyService();

long copied = service.copy(
Files.asByteSource(source),
Files.asByteSink(target)
);

System.out.println("copied bytes = " + copied);
}
}

ByteSource.copyTo(ByteSink) 内部会管理打开和关闭流。

这比直接操作流更抽象。

chapter 38:Closer、try-with-resources、Source/Sink 怎么选

场景 推荐
Java 7+ 新代码管理资源 try-with-resources
Java 6 老项目 Closer
动态注册多个 Closeable Closer 可考虑
已经有 InputStream / OutputStream try-with-resources + ByteStreams
希望抽象输入输出来源 ByteSource / ByteSink
文本输入输出抽象 CharSource / CharSink
简单文件复制 JDK Files.copy 或 Guava Files.copy
Spring 管理资源 不要手动关闭框架管理资源

一句话:

能用 try-with-resources,就优先用 try-with-resources;需要更高层 IO 抽象,就用 Source/Sink;维护老代码时理解 Closer。

chapter 39:资源关闭 API 速查

Closer 标准写法

1
2
3
4
5
6
7
8
9
10
11
Closer closer = Closer.create();

try {
InputStream in = closer.register(new FileInputStream(file));

// use resource
} catch (Throwable e) {
throw closer.rethrow(e);
} finally {
closer.close();
}

try-with-resources 标准写法

1
2
3
try (InputStream in = new FileInputStream(file)) {
// use resource
}

多资源 try-with-resources

1
2
3
4
try (InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(target)) {
// copy
}

Guava ByteStreams copy

1
ByteStreams.copy(inputStream, outputStream);

Guava Source/Sink copy

1
source.copyTo(sink);

chapter 40:一句话总结

这一章的核心是:

资源关闭的本质是确保资源在任何情况下都能被释放,同时不能让关闭异常覆盖真正的业务异常。

Closer 解决的是 Java 7 之前多资源关闭和异常传播困难的问题。

它支持:

  • 多资源注册;
  • 反向关闭;
  • 异常传播;
  • suppressed exception;
  • 避免关闭异常覆盖主异常。

Java 7 之后,try-with-resources 成为更推荐的标准做法。

它原生支持:

  • 自动关闭;
  • 多资源反向关闭;
  • suppressed exception;
  • AutoCloseable

所以新项目中:

1
优先 try-with-resources。

老项目中:

1
看到 Closer 要能读懂,并知道它为什么这么写。

资源关闭不是小事。

一个系统可能不是被复杂算法拖垮的,而是被一个没关闭的流、一个没释放的连接、一个没清理的临时文件慢慢拖垮的。

好的资源管理代码应该做到:

1
2
3
4
5
谁打开,谁关闭。
资源必释放。
异常不丢失。
日志可追踪。
业务一致性单独设计。

这才是后端代码稳定性的基本功。

参考资料

  • Google Guava API: Closer.
  • Google Guava API: ByteStreams.
  • Google Guava API: ByteSource.
  • Google Guava API: ByteSink.
  • Java Documentation: try-with-resources.
  • Java Documentation: AutoCloseable.
  • Java Documentation: Closeable.
  • Java Documentation: Throwable.addSuppressed.
  • Java Documentation: Throwable.getSuppressed.
  • Java Documentation: InputStream.
  • Java Documentation: OutputStream.
  • Joshua Bloch. Effective Java.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Guava:资源关闭与异常处理
https://allendericdalexander.github.io/2026/04/01/java/utils/guava/06guava-closer-resource-exception-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议