此为 软件开发与创新 课程的做业html
本篇博客所分析的项目来自于 ジ绯色月下ぎ——vue+axios+springboot文件下载 的博客,在其基础之上进行了一些分析和改进。前端
原项目前端使用了Vue框架,后端采用Springboot框架进行搭建,经过前端发送请求,后端返回文件流给前端进行文件下载。vue
public class DownLoadFile { @RequestMapping(value = "/downLoad", method = RequestMethod.GET) public static final void downLoad(HttpServletResponse res) throws UnsupportedEncodingException { //文件名 能够经过形参传进来 String fileName = "t_label.txt"; //要下载的文件地址 能够经过形参传进来 String filepath = "f:/svs/" + fileName; OutputStream os = null;//输出文件流 InputStream is = null;//输入文件流 try { // 取得输出流 os = res.getOutputStream(); // 清空输出流 res.reset(); res.setContentType("application/x-download;charset=GBK");//设置响应头为文件流 res.setHeader("Content-Disposition","attachment;filename=" + new String(fileName.getBytes("utf-8"), "iso-8859-1"));//设置文件名 // 读取流 File f = new File(filepath); is = new FileInputStream(f); if (is == null) { System.out.println("下载附件失败"); } // 复制 IOUtils.copy(is, res.getOutputStream());//经过IOUtils的copy函数直接将输入文件流的内容复制到输出文件流内 res.getOutputStream().flush();//刷新输出流 } catch (IOException e) { System.out.println("下载附件失败"); } // 文件的关闭放在finally中 finally { try { if (is != null) { is.close(); } } catch (IOException e) { System.out.println("输入流关闭异常"); } try { if (os != null) { os.close(); } } catch (IOException e) { System.out.println("输出流关闭异常"); } } } }
原做者后端利用IOUtils.copy完成了输入输出流的写入,此函数内部调用了缓冲区,实现稳定的文件流的写出,后端基本可以应对各类文件的文件流传输。java
但查阅相关文档,发现copy方法的buffer大小为固定的 4Kios
而不一样大小的文件不一样网速的用户对于文件的下载时缓冲区的大小其实经过调整可以有明显提速,因此须要进一步测试是否经过调整buffer大小可以使用户体验明显提高。spring
<el-button size="medium" type="primary" @click="downloadFile">Test</el-button>
//js downloadFile(){ this.axios({ method: "get", url: '/api/downloadFile', responseType: 'blob', headers: { Authorization: localStorage.getItem("token") } }) .then(response => { //文件名 文件保存对话框中的默认显示 let fileName = 'test.txt'; let data = response.data; if(!data){ return } console.log(response); //构造a标签 经过a标签来下载 let url = window.URL.createObjectURL(new Blob([data])) let a = document.createElement('a') a.style.display = 'none' a.href = url //此处的download是a标签的内容,固定写法,不是后台api接口 a.setAttribute('download',fileName) document.body.appendChild(a) //点击下载 a.click() // 下载完成移除元素 document.body.removeChild(a); // 释放掉blob对象 window.URL.revokeObjectURL(url); }) .catch(response => { this.$message.error(response); }); },
做者前端使用动态建立a标签的方式进行前端用户进行文件下载的操做。这里就有一个比较大的问题。axios
这个问题是由axios自身的特性产生的,在使用axios进行下载请求后,axios会将全部的返回数据先进行缓存,等所有缓存完成后再调用then方法。也就是用axios的then方法接收返回数据时,会将用户须要下载的文件先缓存在内存中,等文件所有下载完成再运行then内的代码。后端
这个特性也是致使问题的关键,致使的问题有:api
首先是针对后端的一些优化的尝试浏览器
粗略测试方法:使用本地搭建先后端,将 F盘文件夹做为服务器存放文件的位置,文件经过前端下载至 D盘,理论下载速度为100M/s(由实际复制速度估算),经过改变 buffer大小测试文件下载速度差别,平均耗时计算方法为去掉最低最高耗时,取剩下平均值
下载的文件大小为700M,理论最快下载耗时 7s
public String downloadFile(@RequestParam("filename") String filename, @RequestParam("sha1Hash") String sha1Hash, HttpServletRequest request, HttpServletResponse response) throws IOException { //…………………………略去细节 FileInfo fileInfo = new FileInfo();//将请求信息转为bean fileInfo.setFilename(filename); fileInfo.setSha1Hash(sha1Hash); String resPath = fileInfoRepository.searchFilePath(fileInfo);//查询文件在服务器的位置 FileInputStream fileInputStream = null;//输入流 ServletOutputStream os = null;//输出流 try { File fileRes = new File(resPath);//经过路径获取文件 os = response.getOutputStream();//获取输出流 fileInputStream = new FileInputStream(fileRes);//获取文件流 long start = System.currentTimeMillis();//下载开始时间 IOUtils.copy(fileInputStream , response.getOutputStream());//使用已有库进行数据流传输 long end = System.currentTimeMillis();//下载结束时间 System.out.println("遍历" + filename + "文件流,耗时:" + (end - start) + " ms");//输出下载所用时间 os.flush();//刷新输出流 response.setStatus(HttpServletResponse.SC_OK); //…………………… }
次序 | 耗时 |
---|---|
1 | 21823ms |
2 | 20098ms |
3 | 12643ms |
4 | 22284ms |
5 | 23779ms |
平均耗时:21402ms——21.4s
long start = System.currentTimeMillis();//下载开始时间 IOUtils.copyLarge(fileInputStream , response.getOutputStream());//使用已有库进行数据流传输 long end = System.currentTimeMillis();//下载结束时间 System.out.println("遍历" + filename + "文件流,耗时:" + (end - start) + " ms");//输出下载所用时间
次序 | 耗时 |
---|---|
1 | 23351ms |
2 | 21046ms |
3 | 26786ms |
4 | 22190ms |
5 | 28389ms |
平均耗时:24109ms——24.1s
byte[] bytes = new byte[1024 * 1024 * 20];//静态buffer int len = 0; long start = System.currentTimeMillis();//下载开始时间 while ((len = bufferedInputStream.read(bytes)) != -1) { os.write(bytes, 0, len); } long end = System.currentTimeMillis();//下载结束时间 System.out.println("遍历" + filename + "文件流,耗时:" + (end - start) + " ms");//输出下载所用时间
次序 | 耗时 |
---|---|
1 | 20212ms |
2 | 16648ms |
3 | 15591ms |
4 | 15496ms |
5 | 13185ms |
平均耗时:15911ms——15.9s
byte[] bytes = new byte[1024 * 1024 * 40];//静态buffer int len = 0; long start = System.currentTimeMillis();//下载开始时间 while ((len = bufferedInputStream.read(bytes)) != -1) { os.write(bytes, 0, len); } long end = System.currentTimeMillis();//下载结束时间 System.out.println("遍历" + filename + "文件流,耗时:" + (end - start) + " ms");//输出下载所用时间
次序 | 耗时 |
---|---|
1 | 12194ms |
2 | 10198ms |
3 | 9794ms |
4 | 15116ms |
5 | 16523ms |
平均耗时:12503ms——12.5s
结论:可见在网速恒定,文件大小恒定的状况下,缓冲区大小对于文件下载速度会形成必定差别。而在实际应用环境中缓冲区大小会受:文件大小、内存使用状况、网速状况、带宽占用量的多方面因素影响,因此选择一个合适的缓冲区大小,甚至是动态调整缓冲区大小都是可以改善用户体验的一个方法。
这是针对前端axios下载问题的改进之路
首先经过搜素了解为什么没法正常触发浏览器下载
其次经过搜素和询问找到了以下几种解决方案
经过实践,采用第二种方式即便用form表单代替axios的then方法进行文件下载
downloadFile (file,scope) { var form = document.createElement("form");//建立form元素 form.setAttribute("style", "display:none"); form.setAttribute("method", "post");//post方式提交 var input = document.createElement('input');//用input标签传递参数 input.setAttribute('type', 'hidden'); input.setAttribute('name', 'filename'); input.setAttribute('value', file.filename); form.append(input); var input2 = document.createElement('input'); input2.setAttribute('name', 'sha1Hash'); input2.setAttribute('value', file.sha1Hash); form.append(input2); form.setAttribute("action", initialization.downloadFileInterface);//请求地址 form.setAttribute("target", "_self");//不跳转至新页面 var body = document.createElement("body"); body.setAttribute("style", "display:none"); document.body.appendChild(form); form.submit(); form.remove(); },
public String downloadFile(@RequestParam("filename") String filename, @RequestParam("sha1Hash") String sha1Hash, HttpServletRequest request, HttpServletResponse response) throws IOException { request.setCharacterEncoding("UTF-8"); FileInfo fileInfo = new FileInfo(); fileInfo.setFilename(filename); fileInfo.setSha1Hash(sha1Hash); String resPath = fileInfoRepository.searchFilePath(fileInfo); FileInputStream fileInputStream = null; BufferedInputStream bufferedInputStream = null; ServletOutputStream os = null; try { File fileRes = new File(resPath); response.reset(); response.addHeader("Access-Control-Allow-Origin", "*");//设置响应头 response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); response.addHeader("Access-Control-Allow-Headers", "Content-Type"); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileInfo.getFilename().getBytes(StandardCharsets.UTF_8), "ISO-8859-1")); os = response.getOutputStream(); fileInputStream = new FileInputStream(fileRes); bufferedInputStream = new BufferedInputStream(fileInputStream); byte[] bytes = new byte[1024 * 1024 * 20];//静态buffer int len = 0; while ((len = bufferedInputStream.read(bytes)) != -1) {//循环读取 os.write(bytes, 0, len); } os.flush(); response.setStatus(HttpServletResponse.SC_OK); return "success"; } catch (Exception e){ response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); return null; } finally { try{ if(bufferedInputStream != null) bufferedInputStream.close(); }catch(IOException e){ System.out.println("bufferedInputStream关闭异常"); } try{ if(fileInputStream != null) fileInputStream.close(); }catch(IOException e){ System.out.println("fileInputStream关闭异常"); } try{ if(os != null) os.close(); }catch(IOException e){ System.out.println("os关闭异常"); } } }
前端下载截图