try-with-resources 中的一个坑,注意避让

共 5621字,需浏览 12分钟

 ·

2022-06-25 11:14

小伙伴们好呀,昨天复盘以前做的项目(大概有一年了),看到这个  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);
        }

    }

看完后你觉得选啥呢?

  1. 异常被全局异常处理器捕获并返回给前端。
  2. 前端收不到 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  的作用就是下面两点了

  1. catch Exception 时,先关闭流,再抛出异常
  2. 添加正常关闭流的代码

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

var15.addSuppressed(var12);

这样就挖到  Throwable 来了🐖

image-20220413230827492

这个方法的作用请看 👇

链接: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 中还没来得及写入新的数据,就直接被关闭了,里面的内容还是我上一次访问留下的。🐖

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

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

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

  1. 抛出的异常被 SpringMVC 框架的 AbstractHandlerMethodExceptionResolver 捕获,并执行 doResolveHandlerMethodException 去处理
  2. 利用 jackson 的 UTF8JsonGenerator 去进行序列化,并用 NonClosingOutputStream  对 OutputStream 进行包装。
  3. 数据写入缓冲区 (关键步骤 如下图👇)

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

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

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

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

总结

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

  1. 注意流关闭的问题
  2. 谨慎使用  try-with-resources ,要考虑出异常时,这个流可不可以关闭。
  3. 同时也知道了  try-with-resources 的一些技术细节,不会生成 finally 模块(我之前的误区🐖),而是会在异常捕获中帮我们关闭流,同时附加关闭过程的异常到最外层的异常,而且在程序的结尾增加关闭流的代码。
  4. 流关闭后,数据再也写不到缓冲区中,同时 nio 的 堆内内存缓存区 HeapByteBuffer 中的数据仍然是旧的。后面不管怎么 flush 都无法给到有效反馈信息给前端。

往期推荐

33岁程序员的年中总结


面渣逆袭:MySQL六十六问!建议收藏


实战:10 种实现延迟任务的方法,附代码!


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报