作者:京東零售 柯賢銘
問題回溯
2023年Q2某日運營反饋一個問題,商品系統商家中心某批次工具模板無法下載,導致功能無法使用(因為模板是動態變化的)
商家中心報錯(JSON串):
{"code":-1,"msg":"失敗"}
負責的同事看到失敗後立即與我展開討論(因為不是關鍵業務,所以不需要回滾,修復即可),我們發現新功能模板下載的程式碼與之前的程式碼有所不同,恰好之前的功能又可以正常執行,所以同事對現有程式碼進行改造然後預釋出測試完成後再次上線。
其他業務程式碼:
/**
* 模板下載
*/
@RequestMapping("/doBatchWareSetAd")
public void doBatchWareSetAd(@RequestParam MultipartFile file, HttpServletResponse response) {
wareBatchBusiness.doBatchWareSetAd(file, response, getLongOrgCode(), getCurrentUserPin(), getCurrentUserId());
}
問題業務程式碼:
/**
* 模板下載
*/
@RequestMapping("/doBatchWareSetAdDemo")
@ResponseBody
public Map<String, Object> doBatchWareSetAd(@RequestParam MultipartFile file, HttpServletResponse response) {
return wareBatchBusiness.doBatchWareSetAd(file, response, getLongOrgCode(), getCurrentUserPin(), getCurrentUserId());
}
上線的結果是;仍然無法使用。
其實也正常:因為兩種程式碼在預釋出都可以正常執行,線上上出錯只可能是因為其他原因,只不過我們不瞭解底層原理,害怕它 "可能" 有問題罷了,最終查詢得到的結論是許可權系統管理員線上上環境沒有給我們配置相應的檔案,導致請求為空,導致請求失敗。
探索 @ResponseBody 與主動寫入流的關係
我們都知道 @ResponseBody
註解可以幫助我們把返回物件轉化為JSON,方便展示和互動。
那它到底是如何工作的呢,請看下面的講解:
程式碼案例1:
@RequestMapping("/test1")
@ResponseBody
public Map<String, String> test1(HttpServletResponse response) {
Map<String, String> map = new HashMap<>();
map.put("1", "1");
return map;
}
// 響應
JSON報文
跟程式碼發現其核心處理類為:RequestResponseBodyMethodProcessor.java
方法:org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue 會處理其相關返回值。
真正的核心處理方法:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters
關鍵DEBUG記錄
如圖所示:
後續內容可以想象,肯定還有地方去把流按照指定的HEADER
寫入,因為和本文無關所以不深究。
再來看程式碼案例2:
@RequestMapping("/test2")
@ResponseBody
public Map<String, String> test2(HttpServletResponse response) throws IOException {
Map<String, String> map = new HashMap<>();
map.put("1", "1");
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", String.format(
"attachment; filename=%s_%s.xls", "Demo", System.currentTimeMillis()));
OutputStream out = response.getOutputStream();
out.flush();
out.close();
return map;
}
// 響應
提示下載檔案
關鍵DEBUG原始碼截圖
:
可以發現Spring對這種方式操作檔案流視作異常情況,然後丟擲,在後續邏輯中完成整個請求,簡單來說就是 @ResponseBody
註解沒起到任何作用。
因此答案呼之欲出:當時功能不可用的罪魁禍首就是相關人員沒有配置引數導致,與寫法沒有任何關係。
結論與啟發
結論:
啟發:
聊了這麼多,那我們這種類似場景的程式碼應該怎麼寫?
既然主動寫入流會解除@ResponseBody的作用,反之又能發揮它的作用,那我們最佳方案是不是如下所示?
@RequestMapping("/test1")
@ResponseBody
public Map<String, String> test1(HttpServletResponse response) {
Map<String, String> map = new HashMap();
if (獲取不到檔案配置 == true) {
return map.put("msg", "獲取不到檔案配置");
}
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", String.format(
"attachment; filename=%s_%s.xls", "Demo", System.currentTimeMillis()));
OutputStream out = response.getOutputStream();
out.flush();
out.close();
return map;
}
如此一來,當發生預期之外的情況,我們有非常明顯的報錯提示,當正常時又可以完美實現功能,妙哉(我覺得)~