背景
今天,調整了一箇舊項目的報表下載功能,原來文件是存儲在服務器本地的,下載直接從本機獲取就可以了,現在要改成從 FTP 服務器獲取文件再返回給前臺。
理論上,對代碼稍微調整就可以了,實際上卻踩了一個小坑,本文將整理 Java Web 應用文件下載的流程及注意點。
文件下載流程
文件下載是一個老生常談的功能了,基本原理是直接向響應流寫數據,並設置響應類型爲二進制流格式:
- 設置響應編碼 ;
- 設置響應文件類型 octet-stream ;
- 設置響應頭域附件名稱;
- 想 ServletResponse 的 OutputStream 流 write 數據。
第二、三、四步對應的代碼爲:
下載操作源碼
常見的文件下載代碼爲:
@ResponseBody
@RequestMapping(value = "/download")
public void download(HttpServletRequest request, HttpServletResponse response,String reportId) {
// TODO 根據 reportId 查詢報表對應的文件名稱
String fileName = "xxx日報表文件.xlsx";
//設置響應類型和附件頭域
response.setCharacterEncoding("utf-8");
response.setContentType("application/octet-stream");
response.setHeader("content-disposition", "attachment;filename="+ fileName);
//讀取報表內容寫入響應流對象
InputStream inputStream = null;
try {
OutputStream output = response.getOutputStream();
//檢查文件是否存在
if(!isFileExist(fileName)){
logger.warn("文件/目錄 {} 不存在", pathName);
response.getWriter().println("報表文件不存在!");
return;
}
inputStream = new FileInputStream(new File(pathName));
int len = -1;
byte[] bytes = new byte[2048];
// 向 Response 的響應流寫入二進制數據
while ((len = inputStream.read(bytes)) != -1) {
output .write(bytes, 0, len);
}
output.flush();
} catch (Exception e) {
logger.error("下載文件異常",e);
try {
response.getWriter().println("下載文件異常!");
} catch (IOException ex) {
ex.printStackTrace();
}
}finally{
if(output != null){
try {
output.close();
} catch (IOException e) {
logger.error("下載文件關閉輸出流異常",e);
}
}
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
logger.error("下載文件關閉輸入流異常",e);
}
}
}
}
敲重點
,運行下載操作後,xlsx 類型的報表文件現在下載欄中,查看網絡請求的響應頭爲:
響應頭域設置位置
設置響應類型和頭域信息必須在 write
寫入之前,否則附件就是不可讀的。調整代碼順序,先寫後設置響應頭:
執行下載操作,發現 xlsx 類型的報表以 .zip 壓縮包格式被下載,且內容不可讀。
查看網絡響應的頭,可以看出,之後設置的頭域沒有生效:
編程啓示錄
爲什麼設置順序不同,下載附件顯示就不一樣呢?
反覆驗證了十幾次,發現 response.setHeader
在 write
操作之後時,頭域設置是無效的。推測這是由 http 通信協議包組裝順序決定的,因爲 http 響應頭域信息是在 body 之前組裝的
。
最後,再總結下文件下載的要點:
- 下載路徑必須在後臺設置,不能直接接收前臺的下載路徑,否則就有
../..
路徑的任意文件下載風險;如果要接收帶路徑的fileName
參數,必須校驗fileName
不能包含../
等特殊路徑; - 必須注意響應頭域設置和流寫入操作的順序,
寫設頭域後寫
,否則附件不可用; - 從 FTP 下載也是一樣,將
ServletResponse
的OutputStream
對象傳給FTPClient
的retrieveFile(filename, outputStream)
直接下載到輸出流中。