主動寫入流對@ResponseBody註解的影響

京东云开发者發表於2024-10-29

作者:京東零售 柯賢銘

問題回溯

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 註解沒起到任何作用。

因此答案呼之欲出:當時功能不可用的罪魁禍首就是相關人員沒有配置引數導致,與寫法沒有任何關係。

結論與啟發

結論:

1.我們要相信自己的程式碼,至少是要相信已經經過測試的程式碼。
2.在委託他人或者自己配置環境引數,如許可權、ZK等每次都保證預釋出和線上同時配置,避免遺漏的情況。

啟發:

聊了這麼多,那我們這種類似場景的程式碼應該怎麼寫?

既然主動寫入流會解除@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;
}

如此一來,當發生預期之外的情況,我們有非常明顯的報錯提示,當正常時又可以完美實現功能,妙哉(我覺得)~

相關文章