故事
系统里有个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);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。