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: