try-with-resources 这样坑过我

语言: CN / TW / HK

小伙伴们好呀,昨天 摸鱼 复盘以前做的项目(大概有一年了),看到这个   try-catch ,又想起自己之前掉坑的这个经历 ,弄了个小 demo 给大家感受下~  :smile:

问题1

一个简单的下载文件的例子。

这里会出现什么情况呢?

 @GetMapping("/download")
public void downloadFile(HttpServletResponse response) throws Exception {
String resourcePath = "/java4ye.txt";
URL resource = DemoApplication.class.getResource(resourcePath);
String path = resource.getPath().replace("%20", " ");
try( ServletOutputStream outputStream = response.getOutputStream();
FileInputStream fileInputStream = new FileInputStream(path)) {


byte[] bytes = new byte[8192];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
while ((len = fileInputStream.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}

String fileName = "java4ye.txt";
// response.setHeader("content-type", "application/octet-stream;charset=UTF-8");
// response.setContentType("application/octet-stream");
// response.setHeader("Access-Control-Expose-Headers", "File-Name");
// response.setHeader("File-Name", fileName);
// 异常
int i = 1/0;

response.setHeader("Content-Disposition", "attachment;filename=" + fileName);

outputStream.write(baos.toByteArray());

} catch (Exception e) {
throw new DownloadException(e);
}

}

看完后你觉得选啥呢?

  1. 异常被全局异常处理器捕获并返回给前端。

  2. 前端收不到 response 的错误信息。

答案当然是 2 啦,哈哈 正常的话就不会写出来了 :stuck_out_tongue_closed_eyes:

bug 回忆

当时和前端联调时,我发现这个异常信息前端都没有给出相应的提示,还以为是前端的问题,哈哈哈 毕竟我这代码看着也没毛病呀。:smile:

而且项目是前后端分离的,response 的 content-type 和 header 中都做了处理,前端用了 axios 去拦截这些响应,貌似还有一个 responseType: blob 这样的东东。然后刚好那会前端也不熟悉这个东西,他也以为是他前端出了问题,但是debug 的时候,看到这个 post 请求的 response 怎么是空的呢,通过 chrome 浏览器发现的。

这个时候我还很纳闷,问他说,难道你这个 前端拦截 处理掉了,不然怎么看不到:joy:(我真坑 ,现在真想给自己两巴掌醒醒:joy: 这尽说胡话:joy:)

后来我也觉得不对劲,就仔细去看自己的代码了,还叫了另一个同事一起看 :pig:  一起猜测(中途又坑了前端一把 罪过啊……:joy:)

一两个钟过去后,我终于开窍了,想到会不会是这个 流先被关闭了 ,才导致这场闹剧的:scream: (心里估摸着 八九不离十)

于是我便尝试性地修改下代码,拆开 try-with-resources ,改成常规的   try-catch ,并在  finally 中重写了这个流的关闭逻辑,当程序正常时,才正常关闭流,否则不关闭。

结果很顺利地就解决了这个问题…… :sweat_smile:

当时也是觉得自己特蠢,第一时间居然没想到这个 流被关闭 的问题,还傻乎乎地怀疑这个浏览器,前端的一些写法是不是有问题,很尴尬:sweat_smile: 这么坑,,只想赶紧找个洞钻进去。。

再次看到这个代码,觉得里面应该还有东西可以细挖出来的,于是便有了这文~ :pig2:(公开处刑,引以为戒)

问题2

你有看过 try-with-resources 和   try-catch 编译后和反编译出来的代码吗? 有对比过他们的不同吗~

整体
细节

这里 4ye 给出了上面 try-with-resources 模块反编译后的代码,可以发现反编译后代码中是没有出现 finally 块的。

如果从上图看的话, try-with-resources 的作用就是下面两点了

  1. catch Exception 时,先关闭流,再抛出异常

  2. 添加正常关闭流的代码

细心的小伙伴是不是还发现了这一行代码呢 :smile:

var15.addSuppressed(var12);

这样就挖到 Throwable 来了:pig2:

image-20220413230827492

这个方法的作用请看 :point_down:

链接:https://blog.csdn.net/qiyan2012/article/details/116173807

大概意思就是 把异常挂到最外层的异常中去 :+1: ,不过从方法的注释上可以知道,这个一般都是   try-with-resources 偷偷帮我们做的。

到这里还不能结束 ,请接着看 :smile:

问题3

这个异常还没 debug 呢,别走呀,验证一下上面 流的关闭 逻辑:pig2:

在 OutputStream的 close 方法中打个断点,最后会来到 Tomcat 的  CoyoteOutputStream 中,可以看到此时的标志位 closed 和 doFlush 都是 false。

执行完 close 方法关闭后,这个 initial 从 true 变为 false ,而 closed 也变为 true。

同时,这个 堆内内存缓冲区 HeapByteBuffer 中还没来得及写入新的数据 ,就直接被关闭了,里面的内容还是我上一次访问留下的。:pig2:

关闭流后,才去捕获这个异常,这和我们反编译后看到的代码逻辑是一致的

下面步骤有点长,就简单概括下关键点~ :point_down:

流关闭后,这部分代码还是照常执行的。

  1. 抛出的异常被  SpringMVC 框架的  AbstractHandlerMethodExceptionResolver 捕获,并执行  doResolveHandlerMethodException 去处理

  2. 利用  jackson 的  UTF8JsonGenerator 去进行序列化,并用  NonClosingOutputStream 对  OutputStream 进行包装。

  3. 数据写入缓冲区 (关键步骤 如下图:point_down:)

可以看到流关闭后,这里 closed 也变成 true,所以自定义的信息也写不到这个缓冲区。

后面的其他 flush 操作也刷不出任何东西了。

例子的话就放到 GitHub 上了……  直接和下期要写的例子一起放上去了:pig2:

https://github.com/Java4ye/springboot-demo-4ye

总结

看完之后,你知道了 4ye 曾经犯过的一个很低级的错误:joy: (这次脸都不要了,硬是挖了点其他内容一起写出来 :pig:)

  1. 注意流关闭的问题

  2. 谨慎使用  try-with-resources ,要考虑出异常时,这个流可不可以关闭。

  3. 同时也知道了   try-with-resources 的一些技术细节,不会生成 finally 模块(我之前的误区:pig2:),而是会在异常捕获中帮我们关闭流,同时附加关闭过程的异常到最外层的异常,而且在程序的结尾增加关闭流的代码。

  4. 流关闭后,数据再也写不到缓冲区中,同时 nio 的 堆内内存缓存区 HeapByteBuffer 中的数据仍然是旧的。后面不管怎么 flush 都无法给到有效反馈信息给前端。

最后

喜欢   的话可以  点赞 & 关注 并 星标 下公众号  Java4ye 支持下  4ye 呀:stuck_out_tongue_closed_eyes:,这样就可以  第一时间收到更文消息  啦:pig:

我是4ye 咱们下期 应该 …… 很快再见!! :laughing: