故事
系統裡有個Excel報表匯出,以前是匯出xls格式,沒問題。後來改成xlsx後,開啟就報錯了。
一開始同事還以為是用的Excel工具庫不支援xlsx,但我覺得不太可能,我把不能開啟的Excel檔案拿來一分析,果然有蹊蹺。
不能開啟的xlsx檔案末尾多了一個字串{reponseCode:"000000",reponseMsg:"成功"}
,之前xls格式能開啟可能是因為xls錯誤相容性比較好。
多這個字串的原因是匯出報表虛擬碼如下
@POST
@Path("/download")
public String download(){
WorkBook wb = generateWorkBook();
wb.save(response.getOutputStream());
return {reponseCode:"000000",reponseMsg:"成功"};
}
最開始的同事之所以返回String我猜是想著出錯時返回一些錯誤提示。但其實真的出錯時直接往外拋異常即可。所以同事將程式碼改成
@POST
@Path("/download")
public void download(){
WorkBook wb = generateWorkBook();
wb.save(response.getOutputStream());
}
不要在操作outputStream時同時在方法返回字串就好。
問題
這個故事引出了本篇文章。
事實上網上大把檔案下載的示例程式碼都是這樣寫的。返回值為void,然後直接操作OutputStream。
但我認為這實在不是一個好的實踐。
我認為無論如何不應該去操作outputStream。
如果要返回檔案,就應該顯式地宣告返回值。
思考
函式的輸入輸出,非常直觀。Controller的介面本質也是一個函式。
// 例子1
public String sayHello(String uername){
return "hello " + username;
}
// 顯而易見,假設輸入"張三",那麼函式的返回是"hello 張三",
// 如果是在controller裡,那客戶端收到的報文是"hello 張三"
非常直觀的輸入、輸出對吧,
常規的rest介面不會有人想著要直接入操作outputStream,對吧?
尬來一波,假如有人非要!
如果你在controller裡看到下面的程式碼,你認為這個函式的輸出是什麼?客戶端收到的返回又是什麼?
// 例子2
pubilc String sayHello(String username){
response.getOutputStream().write("world".getBytes());
return "hello " + username;
}
// 同樣輸入"張三",雖然這個函式的返回值是 "hello 張三",
// 但是對於http請求來說,客戶端收到的返回是 "world hello 張三"
// 看看本文開頭的例子,雖然函式本身的返回值僅僅是一個JSON字串,
// 但是客戶端收到的返回卻是【excel檔案+JSON字串】。
繼續往下看,加個 response.getOutputStream().close()你認為這個函式的輸出是什麼?客戶端收到的返回又是什麼?
// 例子3
pubilc String sayHello(String username){
response.getOutputStream().write("world".getBytes());
reponse.getOutputStream().close();
return "hello " + username;
}
// 同樣輸入"張三",雖然這個函式的返回值是 "hello 張三",
// 但是對於http請求來說,客戶端收到的返回是 "world"
// 因為outputStream已經被關閉了,返回值不能再寫入到outputStream裡
// PS: springmvc表現不一樣,springmvc對request和response包了一層wrapper,
// 在springmvc下操作`outputStream.close()`實際上是執行了一個空方法,
// 所以springmvc下返回和例子2一樣
請問:這兩個例子,是不是讓你覺得很混亂,很不直觀?
再次回到本文的觀點,為什麼不要去操作outputStream?
沒有明顯的好處。
固然,常規 restful 的介面我們直接return資料比較簡單,所以不會有人自找麻煩去操作outputStream。但是對於檔案,用返回值的形式是給你帶了很大麻煩嗎?直接操作outputStream是給你帶來了很大的便利嗎?
與常規的思維邏輯相悖。
方法返回體宣告為void即是表明該方法無返回值,但是實際你又返回了一個檔案。
帶來混亂。
如上述的3個例子,當有新手開發在操作outputStream的同時還宣告瞭返回值,可能還呼叫了flush或close等方法時,會有一些意想不到的表現。
可能帶來未知的問題。
不同的框架有不同的實現,不是所有人都會去研究框架的原始碼。如上述例子3的程式碼在springmvc和jersey下表現就是不一致。
所以,為什麼就那麼執著地要去直接操作outputStream呢?
結論
無論如何不應該在Controller裡直接操作outputStream。
那以常見的檔案下載需求來說,應該怎樣寫檔案下載呢?
不推薦:直接操作outputStream,同時方法返回值宣告為void。
推薦:直接返回檔案流
// jersey的寫法
public Response downloadExcel(){
StreamingOutput stream = 二進位制檔案內容;
return Response
.ok(stream, MediaType.APPLICATION_OCTET_STREAM)
.header("content-disposition","attachment; filename=xx.xlsx")
.build();
}
springmvc的示例
@Controller
public class DownloadController {
@GetMapping
public ResponseEntity<Resource> downloadPdf() {
FileSystemResource resource = new FileSystemResource("/xx.xlsx");
ContentDisposition disposition = ContentDisposition
.inline() // or .attachment()
.filename(resource.getFilename())
.build();
headers.setContentDisposition(disposition);
return new ResponseEntity<>(resource, headers, HttpStatus.OK);
}
}