欢迎你来读这篇博客,这篇博客主要是关于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 异常处理和资源管理的核心问题。
如果你能真正理解 Closer 和 try-with-resources,你写 IO、数据库、网络、文件导入导出时会稳很多。
正文 chapter 1:为什么资源关闭很重要 资源关闭不是“代码洁癖”,而是系统稳定性问题。
例如读取文件:
1 2 3 InputStream inputStream = new FileInputStream (file);byte [] bytes = inputStream.readAllBytes();
如果不关闭:
文件句柄可能一直占用。
在开发环境可能没感觉。
但在线上服务中,如果每个请求都打开文件、网络连接或数据库连接而不关闭,服务会慢慢积累资源泄漏。
最终可能出现:
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" ); } 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" ); } finally { if (outputStream != null ) { outputStream.close(); } if (inputStream != null ) { inputStream.close(); } }
如果关闭时也可能抛异常,就更麻烦。
如果 outputStream.close() 抛异常,后面的 inputStream.close() 可能就不执行了。
于是你还要写嵌套 try-catch。
代码很快变得又长又丑。
chapter 3:多资源关闭的典型问题 多资源关闭有几个难点。
1. 关闭顺序 通常应该后打开的先关闭。
例如:
1 InputStream -> BufferedInputStream
使用时先打开底层流,再包装缓冲流。
关闭时应该先关闭外层流。
也就是:
2. 关闭异常不能阻止其他资源关闭 如果关闭第一个资源失败,也应该继续关闭其他资源。
否则可能造成更多泄漏。
3. 主异常不能被关闭异常覆盖 如果业务代码抛出异常:
同时关闭资源也抛异常:
我们更关心主异常。
关闭异常应该作为 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(); }
这段代码有几个关键点:
创建 Closer;
使用 closer.register() 注册资源;
catch 中使用 closer.rethrow(e) 传播异常;
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) 做两件事:
把资源加入 Closer 内部维护的栈;
返回这个资源本身。
所以可以这样写:
1 InputStream inputStream = closer.register(new FileInputStream (sourcePath));
不用写成:
1 2 InputStream inputStream = new FileInputStream (sourcePath); closer.register(inputStream);
当然两种都可以。
register 返回资源本身,是为了让代码更紧凑。
注册后的资源会在:
中统一关闭。
chapter 7:Closer 支持什么资源 Closer 主要支持:
例如:
InputStream
OutputStream
Reader
Writer
RandomAccessFile
ZipInputStream
Socket
Java 7 之后更通用的是:
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));
关闭时先关闭:
再关闭:
这符合包装流的关闭习惯。
chapter 9:为什么关闭顺序要反过来 资源经常存在依赖关系。
例如:
1 2 FileInputStream fileInputStream = new FileInputStream (file);BufferedInputStream bufferedInputStream = new BufferedInputStream (fileInputStream);
BufferedInputStream 包装了 FileInputStream。
使用时是:
关闭时应该:
因为外层资源关闭时可能还需要 flush、读取或处理底层资源。
如果先关闭底层资源,外层资源关闭可能失败。
所以栈式关闭是合理的。
chapter 10:Closer 的异常传播 Closer 的经典写法中,catch 部分是:
1 2 3 catch (Throwable e) { throw closer.rethrow(e); }
这看起来有点奇怪。
为什么不是:
原因是:
Closer 需要记录主异常,以便 close 阶段如果也发生异常,可以把关闭异常作为 suppressed exception 处理。
closer.rethrow(e) 会把当前异常记录到 Closer 内部。
然后在 finally 中:
如果关闭资源又发生异常,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() 中关闭资源也抛异常时,关闭异常可能覆盖主异常。
最终你看到的异常可能是:
而真正业务失败原因:
被隐藏了。
这会导致排查问题非常痛苦。
所以使用 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 2 主异常:文件读取失败 suppressed:关闭输出流失败
这样你既知道主错误,也没有丢失关闭错误。
Closer 和 try-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(); } } }
理想情况下,主异常是:
关闭异常会作为 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 ()) { }
关闭顺序是:
也就是:
后创建的资源先关闭。
这和 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 支持:
比 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. 保存主异常 当调用:
时,Closer 会记录这个异常。
类似:
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 文件。
流程:
打开文件输入流;
包装成 Reader;
包装成 BufferedReader;
按行读取;
统计有效行数;
关闭资源。
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. 捕获异常后吞掉
这会让问题消失在日志之外。
问题没消失,只是你看不见了。
6. 依赖 finalize 不要依赖对象回收时关闭资源。
finalize 已经过时,也不可靠。
7. 不 flush 就关闭不确定目标 虽然很多 close 会自动 flush,但重要输出建议明确理解 flush/close 语义。
chapter 30:资源关闭最佳实践 1. 新代码优先 try-with-resources 1 2 3 try (InputStream in = new FileInputStream (file)) { }
2. 多资源按依赖顺序声明 声明顺序:
关闭顺序会自动反过来。
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. 大文件使用流式处理 不要:
直接读大文件。
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)); } catch (Throwable e) { throw closer.rethrow(e); } finally { closer.close(); }
try-with-resources 标准写法 1 2 3 try (InputStream in = new FileInputStream (file)) { }
多资源 try-with-resources 1 2 3 4 try (InputStream in = new FileInputStream (source); OutputStream out = new FileOutputStream (target)) { }
Guava ByteStreams copy 1 ByteStreams.copy(inputStream, outputStream);
Guava Source/Sink copy
chapter 40:一句话总结 这一章的核心是:
资源关闭的本质是确保资源在任何情况下都能被释放,同时不能让关闭异常覆盖真正的业务异常。
Closer 解决的是 Java 7 之前多资源关闭和异常传播困难的问题。
它支持:
多资源注册;
反向关闭;
异常传播;
suppressed exception;
避免关闭异常覆盖主异常。
Java 7 之后,try-with-resources 成为更推荐的标准做法。
它原生支持:
自动关闭;
多资源反向关闭;
suppressed exception;
AutoCloseable。
所以新项目中:
老项目中:
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 .
启示录 富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。