⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 www.zyiz.net/tech/detail-143543.html 「使徒行者白」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

「1、引言」

最近行者接到了一个任务,现有 redis 集群存在比较多的大对象(前人直接将对象序列化成 json 字符串后塞到 redis 中),为了节约 redis 内存,需要用 Gzip 压缩后再写入 redis。听起来没啥难度,于是三下五除二写下了如下代码:

写完之后,行者验证了一把,功能也没啥问题,于是就提交了代码。

「2、繁荣表面下的隐患」

有些老司机可能一眼就看出这段代码的问题了——输入输出流没有关闭。单论这段代码:没有合理的关闭 GZIPInputStream 流会导出内存溢出。那么如何解决呢?很简单,加个 finally 就可以了。

那么发生这种低级问题原因是什么呢?有些老司机可能觉得自己不会犯这种错误,只要自己细心就能避免。

如果大家感兴趣,可以用任意搜索引擎搜一下 GZIP 解压缩的用法。你会发现:网上大部分 GZIP 相关文章中的写法和我第一种写法如出一辙。那么问题到底出在了哪里?

也许我们应该反思下:

很多时候,我们的关注点只有功能有没有实现,却忽视了繁荣表面下的隐患。

「3、说好的奇技淫巧呢?」

那么如何尽量避免类似问题的发生呢?要是系统能自动帮我关闭输入输出流就好了!实际上自从 jdk 1.7 开始,try-with-source 语法糖就已经支持了类似功能,先看优化后的代码:

可以看到这段代码已经没有了笨重的 finally 代码了。我猜你现在可能在想:为啥这段代码的不需要我们写 finally 代码来保障输入输出流的正确关闭呢?直接看编译后的 class 文件:

不难看出编译器已经自动帮我们添加了 finally 代码段来释放输入输出流。

「4、万能的 try-with-source?」

聪明的你看到这里心里可能又在犯嘀咕了,try-with-source 看起来的确很不错, 那么它有没有啥使用场景、限制条件的呢?

你说的没错,对于软件编程来说没有银弹。如果要借助 try-with-souce 来实现资源的自动回收,在编写代码的时候,针对有资源释放的类需要实现 Closeable 接口。

再看看我上面给出示例中的 ByteArrayOutputStream 和 GZIPOutputStream 流,其实已经帮实现了 Closeable 接口,所以我们再使用的时候,我们借助 try-with-source 语法糖就能免去使用 finally 释放资源的那一堆代码了。

public class ByteArrayOutputStream extends OutputStream {}  
public abstract class OutputStream implements Closeable, Flushable {}

public class GZIPOutputStream extends DeflaterOutputStream {}
public class DeflaterOutputStream extends FilterOutputStream {}
public class FilterOutputStream extends OutputStream {}
public abstract class OutputStream implements Closeable, Flushable {}

「5、构造可自动释放资源的类」

如果你对此还有一些顾虑,不妨自己动手验证一番:

public class ImageStream implements Closeable {  
public void work() {
System.out.println("开始工作");
}
@Override
public void close() throws IOException {
System.out.println("自动释放资源");
}
}

public static void main(String\[\] args) throws IOException {
try (ImageStream is = new ImageStream()) {
is.work();
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}

这是的一个简单测试类,实现了 Closeable 方法。

运行结果如下:

开始工作

自动释放资源

「6、值得注意的地方」

try (  
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(createdFile));
OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
BufferedWriter bw = new BufferedWriter(osw)
) {
// ...
} catch (Exception ex) {
//...
}

上面是我使用 try-with-source 语法糖写的一个简易 demo,看起来很不错,短小精悍。但其这里其实隐藏着一个小陷阱。

我们现在已经知道,用 try-with-source 语法糖后,GZIOutputStream 的 close() 方法会被自动调用进行资源回收。我们来看下 GZIOutputStream 的 close() 方法:

public void close() throws IOException {  
if (!closed) {
finish();
if (usesDefaultDeflater)
def.end();
out.close();
closed = true;
}
}
public void close() throws IOException {
if (!closed) {
finish();
if (usesDefaultDeflater)
def.end();
out.close();
closed = true;
}
}

可以看到这里实际上执行了两步操作:

  1. finish( )
  2. out.close( )

不难分析,这里的 out 实际上是 GZIOutputStream 构造方法传入的 FileOutputStream 对象。那么问题就来了,各位大佬仔细看,这里的 finish 方法是可以抛出 IO 异常的,如果在执行 finish() 方法时,抛出了 IO 异常,那么下面的 out.close() 方法实际上不会被执行的。

如何解决?

单独声明每个流就可以了,如下所示:

try (  
OutputStream out = new FileOutputStream(createdFile);
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(out);
OutputStreamWriter osw = new OutputStreamWriter(gzipOutputStream);
BufferedWriter bw = new BufferedWriter(osw)
) {
// ...
} catch (Exception ex) {
//...
}

「7、尾声」

为啥写这篇文章,原因大致如下:

  1. 软件开发不能只关注功能实现,需要时刻保持一颗敬畏的心;
  2. 使用 try-with-source 语法糖确实可以帮助我们精简代码并且弱化资源释放的存在感;
  3. 软件编程没有银弹,在了解并尝试使用新技术的时候,不仅仅要看到它的优势,也要尝试去发现繁荣背后的隐患,避免踩坑啊;
文章目录
  1. 1. 「1、引言」
  2. 2. 「2、繁荣表面下的隐患」
  3. 3. 「3、说好的奇技淫巧呢?」
  4. 4. 「4、万能的 try-with-source?」
  5. 5. 「5、构造可自动释放资源的类」
  6. 6. 「6、值得注意的地方」
  7. 7. 「7、尾声」