故事

零碎里有个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的接口实质也是一个函数。

// 例子1public String sayHello(String uername){     return "hello " + username;}// 不言而喻,假如输出"张三",那么函数的返回是"hello 张三",// 如果是在controller里,那客户端收到的报文是"hello 张三"

十分直观的输出、输入对吧,

惯例的rest接口不会有人想着要间接入操作outputStream,对吧?

尬来一波,如果有人非要!

如果你在controller里看到上面的代码,你认为这个函数的输入是什么?客户端收到的返回又是什么?

// 例子2pubilc String sayHello(String username){     response.getOutputStream().write("world".getBytes());     return "hello " + username;}// 同样输出"张三",尽管这个函数的返回值是 "hello 张三",// 然而对于http申请来说,客户端收到的返回是 "world hello 张三"// 看看本文结尾的例子,尽管函数自身的返回值仅仅是一个JSON字符串,// 然而客户端收到的返回却是【excel文件+JSON字符串】。

持续往下看,加个 response.getOutputStream().close()你认为这个函数的输入是什么?客户端收到的返回又是什么?

// 例子3pubilc 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的示例

@Controllerpublic 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);    }}