乐趣区

关于后端:不好的编程习惯之文件下载

故事

零碎里有个 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);
    }
}
退出移动版