故事
零碎里有个 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);
}
}