对Web(Springboot + Vue)实现文件下载功能的改进
此为 软件开发与创新 课程的作业
- 对已有项目(非本人)阅读分析
- 找出软件尚存缺陷
- 改进其软件做二次开发
- 整理成一份博客
原项目简介
本篇博客所分析的项目来自于 ジ绯色月下ぎ——vue+axios+springboot文件下载 的博客,在其基础之上进行了一些分析和改进。
原项目前端使用了Vue框架,后端采用Springboot框架进行搭建,通过前端发送请求,后端返回文件流给前端进行文件下载。
源码解读
- 后端主要代码
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完成了输入输出流的写入,此函数内部调用了缓冲区,实现稳定的文件流的写出,后端基本能够应对各种文件的文件流传输。
但查阅相关文档,发现copy方法的buffer大小为固定的 4K
而不同大小的文件不同网速的用户对于文件的下载时缓冲区的大小其实通过调整能够有明显提速,所以需要进一步测试是否通过调整buffer大小能够使用户体验明显提升。
- 前端
<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会将所有的返回数据先进行缓存,等全部缓存完成后再调用then方法。也就是用axios的then方法接收返回数据时,会将用户需要下载的文件先缓存在内存中,等文件全部下载完成再运行then内的代码。
这个特性也是导致问题的关键,导致的问题有:
- 下载大文件占用内存很高
- 在文件下载完成前,用户不会收到任何提示
改进方案
测试文件下载耗时
首先是针对后端的一些优化的尝试
粗略测试方法:使用本地搭建前后端,将 F盘文件夹作为服务器存放文件的位置,文件通过前端下载至 D盘,理论下载速度为100M/s(由实际复制速度估算),通过改变 buffer大小测试文件下载速度差异,平均耗时计算方法为去掉最低最高耗时,取剩下平均值
下载的文件大小为700M,理论最快下载耗时 7s

- 使用copy方法
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
- 使用copyLarge方法
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
- 使用自定义buffer循环读取(20M)
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
- 使用自定义buffer循环读取(40M)
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下载问题的改进之路
首先通过搜素了解为何无法正常触发浏览器下载
- 知乎评论区中找到了相似提问->传送门

其次通过搜素和询问找到了如下几种解决方案
- 使用a标签以前端静态资源的方式提供下载
- 使用form表单进行文件下载
- 询问了解相关建议和解决方案

通过实践,采用第二种方式即使用form表单代替axios的then方法进行文件下载
实现替代方案
- 前端改用动态创建form表单的方式下载文件
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关闭异常");
}
}
}
改进效果
前端下载截图
对Web(Springboot + Vue)实现文件下载功能的改进的更多相关文章
- Springboot+Vue实现仿百度搜索自动提示框匹配查询功能
案例功能效果图 前端初始页面 输入搜索信息页面 点击查询结果页面 环境介绍 前端:vue 后端:springboot jdk:1.8及以上 数据库:mysql 核心代码介绍 TypeCtrler .j ...
- 【Servlet】java web 文件下载功能实现
需求:实现一个具有文件下载功能的网页,主要下载压缩包和图片 两种实现方法: 一:通过超链接实现下载 在HTML网页中,通过超链接链接到要下载的文件的地址 <!DOCTYPE html> & ...
- springboot成神之——spring文件下载功能
本文介绍spring文件下载功能 目录结构 DemoApplication WebConfig TestController MediaTypeUtils 前端测试 本文介绍spring文件下载功能 ...
- java web文件下载功能实现 (转)
http://blog.csdn.net/longshengguoji/article/details/39433307 需求:实现一个具有文件下载功能的网页,主要下载压缩包和图片 两种实现方法: 一 ...
- SpringBoot + Vue + nginx项目部署(零基础带你部署)
一.环境.工具 jdk1.8 maven spring-boot idea VSVode vue 百度网盘(vue+springboot+nginx源码): 链接:https://pan.baidu. ...
- 使用Docker部署Spring-Boot+Vue博客系统
在今年年初的时候,完成了自己的个Fame博客系统的实现,当时也做了一篇博文Spring-boot+Vue = Fame 写blog的一次小结作为记录和介绍.从完成实现到现在,也断断续续的根据实际的使用 ...
- SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(五): 数据表设计、使用 jwt、redis、sms 工具类完善注册登录逻辑
(1) 相关博文地址: SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p ...
- SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(一): 搭建基本环境、整合 Swagger、MyBatisPlus、JSR303 以及国际化操作
相关 (1) 相关博文地址: SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y- ...
- JAVA文件下载功能问题解决日志
今天给报告系统做了个下载功能,遇到了挺多问题,通过查资料一一解决了. 1.首先遇到的问题是:java后台的输出流输出之后,没有任何报错,浏览器端不弹出保存文件的对话框,原本是ajax请求到后台的con ...
随机推荐
- 北京大公司:你是熟悉Map集合吗?
<对线面试官>系列目前已经连载30篇啦,这是一个讲人话面试系列 [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 &am ...
- 一文读懂k8s rbac 权限验证
自我认为的k8s三大难点:权限验证,覆盖网络,各种证书. 今天就说一下我所理解的权限验证rbac. 咱不说rbac0,rbac1,rbac2,rbac3.咱就说怎么控制权限就行. 一.前言 1,反正R ...
- Qt开源作品38-无边框窗体方案(无抖动,支持win、linux、mac等系统,侧边半屏顶部全屏)
一 前言 不知道各位程序员有没有遇到过这样一种困惑,好不容易在开源网站找到了类似的想要的项目代码,结果down下来一编译,我勒个去,几百个错误,根本没法用,熟悉的人还好可以直接阅读代码进行修改(有些只 ...
- 编写mysql多实例启动脚本
脚本原理: 启动MySQL动作: mysqld_safe来执行启动 停止MySQL动作: 使用mysqladmin来执行停止动作 重启的MySQL动作: 原理就是先停止,然后再启动 但是要注意: ...
- tomcat内置jdk(tomcat集成jdk)(windows环境)
tomcat内置jdk,步骤: 1.在一个已经安装了jdk或者jre的机器上,拷贝一个jre到tomcat根目录下. 2.编辑tomcat/bin文件夹下的catalina.bat文件,在文件开头加上 ...
- 第二十二篇 -- 事件与信号(自定义label信号的双击功能)
在第六篇中已经学习过了自定义信号的相关内容了,那一篇中讲的是自定义类中的自定义信号,类和信号都是自己定义的.那么今天想要学习的是事件处理和信号的关系.如同Label标签,它本身有很多的信号,但是它没有 ...
- 什么是ETL--ETL定义、过程和工具选型思路
ETL代表"提取.转换和加载".ETL 过程在数据集成策略中起着关键作用.ETL允许企业从多个来源收集数据并将其整合到一个集中的位置.ETL还使不同类型的数据可以协同工作. 概述 ...
- SpringBoot+Maven 多模块项目的构建、运行、打包实战
前言 最近在做一个很复杂的会员综合线下线上商城大型项目,单模块项目无法满足多人开发和架构,很多模块都是重复的就想到了把模块提出来,做成公共模块,基于maven的多模块项目,也好分工开发,也便于后期微服 ...
- Java面向对象03——类与对象的创建
类的创建与初始化对象 age: 以类的方式组织代码,以对象的组织(封装)数据 package oop.demon01.demon02; // 学生类(抽象模板) public class Stu ...
- postman之变量
前言:postman可以设置(环境变量)和(全局变量) (环境变量):环境变量只能在选择的环境中使用,可以有多组,常用在设置URL和密码当中 (全局变量):只能有一组,整个环境都可以应用 [环境变量] ...



