try-with-resources 中的一個坑,注意避讓
小伙伴們好呀,昨天復(fù)盤以前做的項(xiàng)目(大概有一年了),看到這個 try-catch ,又想起自己之前掉坑的這個經(jīng)歷 ,弄了個小 demo 給大家感受下~ ??
問題1
一個簡單的下載文件的例子。
這里會出現(xiàn)什么情況呢?

@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 的錯誤信息。

答案當(dāng)然是 2 啦,哈哈 正常的話就不會寫出來了 ??

bug 回憶
當(dāng)時和前端聯(lián)調(diào)時,我發(fā)現(xiàn)這個異常信息前端都沒有給出相應(yīng)的提示,還以為是前端的問題,哈哈哈 畢竟我這代碼看著也沒毛病呀。??
而且項(xiàng)目是前后端分離的,response 的 content-type 和 header 中都做了處理,前端用了 axios 去攔截這些響應(yīng),貌似還有一個 responseType: blob 這樣的東東。然后剛好那會前端也不熟悉這個東西,他也以為是他前端出了問題,但是debug 的時候,看到這個 post 請求的 response 怎么是空的呢,通過 chrome 瀏覽器發(fā)現(xiàn)的。
這個時候我還很納悶,問他說,難道你這個 前端攔截 處理掉了,不然怎么看不到??(我真坑??,現(xiàn)在真想給自己兩巴掌醒醒?? 這盡說胡話??)
后來我也覺得不對勁,就仔細(xì)去看自己的代碼了,還叫了另一個同事一起看 ?? 一起猜測(中途又坑了前端一把 罪過啊……??)

一兩個鐘過去后,我終于開竅了,想到會不會是這個 流先被關(guān)閉了 ,才導(dǎo)致這場鬧劇的?? (心里估摸著 八九不離十)
于是我便嘗試性地修改下代碼,拆開 try-with-resources ,改成常規(guī)的 try-catch ,并在 finally 中重寫了這個流的關(guān)閉邏輯,當(dāng)程序正常時,才正常關(guān)閉流,否則不關(guān)閉。
結(jié)果很順利地就解決了這個問題…… ??
當(dāng)時也是覺得自己特蠢,第一時間居然沒想到這個流被關(guān)閉的問題,還傻乎乎地懷疑這個瀏覽器,前端的一些寫法是不是有問題,很尷尬?? 這么坑,,只想趕緊找個洞鉆進(jìn)去。。

再次看到這個代碼,覺得里面應(yīng)該還有東西可以細(xì)挖出來的,于是便有了這文~ ??(公開處刑,引以為戒)

問題2
你有看過 try-with-resources 和 try-catch 編譯后和反編譯出來的代碼嗎? 有對比過他們的不同嗎~


這里給出了上面 try-with-resources 模塊反編譯后的代碼,可以發(fā)現(xiàn)反編譯后代碼中是沒有出現(xiàn) finally 塊的。
如果從上圖看的話, try-with-resources 的作用就是下面兩點(diǎn)了
catch Exception 時,先關(guān)閉流,再拋出異常 添加正常關(guān)閉流的代碼
細(xì)心的小伙伴是不是還發(fā)現(xiàn)了這一行代碼呢 ??
var15.addSuppressed(var12);
這樣就挖到 Throwable 來了??

這個方法的作用請看 ??
鏈接:https://blog.csdn.net/qiyan2012/article/details/116173807

大概意思就是把異常掛到最外層的異常中去 ?? ,不過從方法的注釋上可以知道,這個一般都是 try-with-resources 偷偷幫我們做的。

到這里還不能結(jié)束 ,請接著看 ??
問題3
這個異常還沒 debug 呢,別走呀,驗(yàn)證一下上面 流的關(guān)閉 邏輯??
在 OutputStream的 close 方法中打個斷點(diǎn),最后會來到 Tomcat 的 CoyoteOutputStream 中,可以看到此時的標(biāo)志位 closed 和 doFlush 都是 false。

執(zhí)行完 close 方法關(guān)閉后,這個 initial 從 true 變?yōu)?false ,而 closed 也變?yōu)?true。
同時,這個 堆內(nèi)內(nèi)存緩沖區(qū) HeapByteBuffer 中還沒來得及寫入新的數(shù)據(jù),就直接被關(guān)閉了,里面的內(nèi)容還是我上一次訪問留下的。??

關(guān)閉流后,才去捕獲這個異常,這和我們反編譯后看到的代碼邏輯是一致的

下面步驟有點(diǎn)長,就簡單概括下關(guān)鍵點(diǎn)~ ??
流關(guān)閉后,這部分代碼還是照常執(zhí)行的。
拋出的異常被 SpringMVC 框架的 AbstractHandlerMethodExceptionResolver 捕獲,并執(zhí)行 doResolveHandlerMethodException 去處理 利用 jackson 的 UTF8JsonGenerator 去進(jìn)行序列化,并用 NonClosingOutputStream 對 OutputStream 進(jìn)行包裝。 數(shù)據(jù)寫入緩沖區(qū) (關(guān)鍵步驟 如下圖??)

可以看到流關(guān)閉后,這里 closed 也變成 true,所以自定義的信息也寫不到這個緩沖區(qū)。
后面的其他 flush 操作也刷不出任何東西了。

例子的話就放到 GitHub 上了…… 直接和下期要寫的例子一起放上去了??
https://github.com/Java4ye/springboot-demo-4ye

總結(jié)
看完之后,你知道了我曾經(jīng)犯過的一個很低級的錯誤?? (這次臉都不要了,硬是挖了點(diǎn)其他內(nèi)容一起寫出來 ??)
注意流關(guān)閉的問題 謹(jǐn)慎使用 try-with-resources ,要考慮出異常時,這個流可不可以關(guān)閉。 同時也知道了 try-with-resources 的一些技術(shù)細(xì)節(jié),不會生成 finally 模塊(我之前的誤區(qū)??),而是會在異常捕獲中幫我們關(guān)閉流,同時附加關(guān)閉過程的異常到最外層的異常,而且在程序的結(jié)尾增加關(guān)閉流的代碼。 流關(guān)閉后,數(shù)據(jù)再也寫不到緩沖區(qū)中,同時 nio 的 堆內(nèi)內(nèi)存緩存區(qū) HeapByteBuffer 中的數(shù)據(jù)仍然是舊的。后面不管怎么 flush 都無法給到有效反饋信息給前端。

往期推薦

33歲程序員的年中總結(jié)

面渣逆襲:MySQL六十六問!建議收藏

實(shí)戰(zhàn):10 種實(shí)現(xiàn)延遲任務(wù)的方法,附代碼!

