不好的编程习惯之文件下载

Categories:

系统里有个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);
    }
}

如果你觉得本文对你有帮助或不错,可略表心意,请我喝一杯冰可乐。

Comments