try-with-resources 中的一个坑,注意避让
小伙伴们好呀,昨天复盘以前做的项目(大概有一年了),看到这个 try-catch ,又想起自己之前掉坑的这个经历 ,弄了个小 demo 给大家感受下~ 😄
问题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);
}
}
看完后你觉得选啥呢?
异常被全局异常处理器捕获并返回给前端。 前端收不到 response 的错误信息。
答案当然是 2 啦,哈哈 正常的话就不会写出来了 😝
bug 回忆
当时和前端联调时,我发现这个异常信息前端都没有给出相应的提示,还以为是前端的问题,哈哈哈 毕竟我这代码看着也没毛病呀。😄
而且项目是前后端分离的,response 的 content-type 和 header 中都做了处理,前端用了 axios 去拦截这些响应,貌似还有一个 responseType: blob 这样的东东。然后刚好那会前端也不熟悉这个东西,他也以为是他前端出了问题,但是debug 的时候,看到这个 post 请求的 response 怎么是空的呢,通过 chrome 浏览器发现的。
这个时候我还很纳闷,问他说,难道你这个 前端拦截 处理掉了,不然怎么看不到😂(我真坑🕳,现在真想给自己两巴掌醒醒😂 这尽说胡话😂)
后来我也觉得不对劲,就仔细去看自己的代码了,还叫了另一个同事一起看 🐷 一起猜测(中途又坑了前端一把 罪过啊……😂)
一两个钟过去后,我终于开窍了,想到会不会是这个 流先被关闭了 ,才导致这场闹剧的😱 (心里估摸着 八九不离十)
于是我便尝试性地修改下代码,拆开 try-with-resources ,改成常规的 try-catch ,并在 finally 中重写了这个流的关闭逻辑,当程序正常时,才正常关闭流,否则不关闭。
结果很顺利地就解决了这个问题…… 😅
当时也是觉得自己特蠢,第一时间居然没想到这个流被关闭的问题,还傻乎乎地怀疑这个浏览器,前端的一些写法是不是有问题,很尴尬😅 这么坑,,只想赶紧找个洞钻进去。。
再次看到这个代码,觉得里面应该还有东西可以细挖出来的,于是便有了这文~ 🐖(公开处刑,引以为戒)
问题2
你有看过 try-with-resources 和 try-catch 编译后和反编译出来的代码吗? 有对比过他们的不同吗~
这里给出了上面 try-with-resources 模块反编译后的代码,可以发现反编译后代码中是没有出现 finally 块的。
如果从上图看的话, try-with-resources 的作用就是下面两点了
catch Exception 时,先关闭流,再抛出异常 添加正常关闭流的代码
细心的小伙伴是不是还发现了这一行代码呢 😄
var15.addSuppressed(var12);
这样就挖到 Throwable 来了🐖
这个方法的作用请看 👇
链接:https://blog.csdn.net/qiyan2012/article/details/116173807
大概意思就是把异常挂到最外层的异常中去 👍 ,不过从方法的注释上可以知道,这个一般都是 try-with-resources 偷偷帮我们做的。
到这里还不能结束 ,请接着看 😄
问题3
这个异常还没 debug 呢,别走呀,验证一下上面 流的关闭 逻辑🐖
在 OutputStream的 close 方法中打个断点,最后会来到 Tomcat 的 CoyoteOutputStream 中,可以看到此时的标志位 closed 和 doFlush 都是 false。
执行完 close 方法关闭后,这个 initial 从 true 变为 false ,而 closed 也变为 true。
同时,这个 堆内内存缓冲区 HeapByteBuffer 中还没来得及写入新的数据,就直接被关闭了,里面的内容还是我上一次访问留下的。🐖
关闭流后,才去捕获这个异常,这和我们反编译后看到的代码逻辑是一致的
下面步骤有点长,就简单概括下关键点~ 👇
流关闭后,这部分代码还是照常执行的。
抛出的异常被 SpringMVC 框架的 AbstractHandlerMethodExceptionResolver 捕获,并执行 doResolveHandlerMethodException 去处理 利用 jackson 的 UTF8JsonGenerator 去进行序列化,并用 NonClosingOutputStream 对 OutputStream 进行包装。 数据写入缓冲区 (关键步骤 如下图👇)
可以看到流关闭后,这里 closed 也变成 true,所以自定义的信息也写不到这个缓冲区。
后面的其他 flush 操作也刷不出任何东西了。
例子的话就放到 GitHub 上了…… 直接和下期要写的例子一起放上去了🐖
https://github.com/Java4ye/springboot-demo-4ye
总结
看完之后,你知道了我曾经犯过的一个很低级的错误😂 (这次脸都不要了,硬是挖了点其他内容一起写出来 🐷)
注意流关闭的问题 谨慎使用 try-with-resources ,要考虑出异常时,这个流可不可以关闭。 同时也知道了 try-with-resources 的一些技术细节,不会生成 finally 模块(我之前的误区🐖),而是会在异常捕获中帮我们关闭流,同时附加关闭过程的异常到最外层的异常,而且在程序的结尾增加关闭流的代码。 流关闭后,数据再也写不到缓冲区中,同时 nio 的 堆内内存缓存区 HeapByteBuffer 中的数据仍然是旧的。后面不管怎么 flush 都无法给到有效反馈信息给前端。
往期推荐
33岁程序员的年中总结
面渣逆袭:MySQL六十六问!建议收藏
实战:10 种实现延迟任务的方法,附代码!