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