try-with-resources 這樣坑過我
小夥伴們好呀,昨天 摸魚 覆盤以前做的專案(大概有一年了),看到這個 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);
}
}
看完後你覺得選啥呢?
-
異常被全域性異常處理器捕獲並返回給前端。
-
前端收不到 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 的作用就是下面兩點了
-
catch Exception 時,先關閉流,再丟擲異常
-
新增正常關閉流的程式碼
細心的小夥伴是不是還發現了這一行程式碼呢 :smile:
var15.addSuppressed(var12);
這樣就挖到 Throwable 來了:pig2:
這個方法的作用請看 :point_down:
連結:http://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:
流關閉後,這部分程式碼還是照常執行的。
-
丟擲的異常被 SpringMVC 框架的 AbstractHandlerMethodExceptionResolver 捕獲,並執行 doResolveHandlerMethodException 去處理
-
利用 jackson 的 UTF8JsonGenerator 去進行序列化,並用 NonClosingOutputStream 對 OutputStream 進行包裝。
-
資料寫入緩衝區 (關鍵步驟 如下圖:point_down:)
可以看到流關閉後,這裡 closed 也變成 true,所以自定義的資訊也寫不到這個緩衝區。
後面的其他 flush 操作也刷不出任何東西了。
例子的話就放到 GitHub 上了…… 直接和下期要寫的例子一起放上去了:pig2:
http://github.com/Java4ye/springboot-demo-4ye
總結
看完之後,你知道了 4ye 曾經犯過的一個很低階的錯誤:joy: (這次臉都不要了,硬是挖了點其他內容一起寫出來 :pig:)
-
注意流關閉的問題
-
謹慎使用 try-with-resources ,要考慮出異常時,這個流可不可以關閉。
-
同時也知道了 try-with-resources 的一些技術細節,不會生成 finally 模組(我之前的誤區:pig2:),而是會在異常捕獲中幫我們關閉流,同時附加關閉過程的異常到最外層的異常,而且在程式的結尾增加關閉流的程式碼。
-
流關閉後,資料再也寫不到緩衝區中,同時 nio 的 堆內記憶體快取區 HeapByteBuffer 中的資料仍然是舊的。後面不管怎麼 flush 都無法給到有效反饋資訊給前端。
最後
喜歡 的話可以 點贊 & 關注 並 星標 下公眾號 Java4ye 支援下 4ye 呀:stuck_out_tongue_closed_eyes:,這樣就可以 第一時間收到更文訊息 啦:pig:
我是4ye 咱們下期 應該 …… 很快再見!! :laughing: