SpringBoot 中大文件(分片上传)断点续传与极速秒传功能的实现
1. 背景
在实际项目中,我们经常会遇到需要上传大文件的情况,常见的方法是将整个文件一次性上传,在文件较大时会出现上传速度慢或者可能导致上传失败等问题。针对这种情况,通常可以采用分片上传的方式,将文件分为多个小片段进行上传,降低单个上传请求的负载,提高上传效率。同时,考虑到用户的网络环境可能不稳定,也需要支持断点续传功能,让用户可以在上传失败后恢复上传。
另外,对于重复上传同一文件的情况,为了节省用户的流量以及服务器的负载,可以实现极速秒传功能,即在上传同一文件时,服务器可以判断已经存在的文件,直接返回结果,无需再次上传。
2. 解决方案
基于上述需求,我们可以采用以下方案:
- 在前端通过 JavaScript 将文件进行分片,上传到后端服务器。
- 在后端通过 SpringBoot 的 MVC 框架对分片进行接收、合并处理,同时支持断点续传功能。
- 判断已存在的文件(文件名 + 文件大小),实现极速秒传功能。
3. 代码实现
下面我们详细讲解如何实现上述方案。
3.1 前端实现
前端实现主要使用了 WebUploader 插件进行文件上传,同时通过 JavaScript 实现了分片上传的逻辑,代码示例如下:
// 创建 WebUploader 对象
var uploader = WebUploader.create({
// 设置文件上传的服务器端地址
server: '/upload',
// 每个分片的大小,单位为字节,默认大小为 5MB
chunkSize: 5 * 1024 * 1024,
// 是否开启分片上传,默认为 true
chunked: true,
// 同时发送的最大请求数,默认为 3
threads: 3,
// 开启文件分片处理
prepareNextFile: true,
// 文件类型限制
accept: {
extensions: 'jpg,jpeg,png,gif'
},
// 配置参数
formData: {
// 文件存储的路径
path: 'images'
}
});
// 添加文件选择的监听事件
uploader.on('fileQueued', function(file) {
console.log('添加文件', file);
});
// 添加分片上传进度的监听事件
uploader.on('uploadProgress', function(file, percentage) {
console.log('上传进度', percentage);
});
// 添加分片上传完成的监听事件
uploader.on('uploadComplete', function(file) {
console.log('上传完成', file);
});
// 点击上传按钮时,开始上传文件
$('#uploadBtn').click(function() {
uploader.upload();
});
3.2 后端实现
后端实现主要使用了 SpringBoot 的 MVC 框架,具体实现步骤如下:
3.2.1 定义文件上传接口
定义一个文件上传的接口,在 Controller 中添加如下方法:
@RestController
@RequestMapping("/upload")
public class UploadController {
@PostMapping("/")
public ResponseVO upload(@RequestParam("file") MultipartFile file,
@RequestParam(value = "md5", required = false) String md5,
@RequestParam(value = "chunk", required = false) Integer chunk,
@RequestParam(value = "chunks", required = false) Integer chunks,
@RequestParam("path") String path) {
// TODO: 文件上传逻辑
return ResponseVO.success();
}
}
在这个接口中,我们通过 @RequestParam
注解获取上传的文件、MD5 值、分片编号、总文件数和存储路径等信息,使用 ResponseVO
对象返回处理结果。
3.2.2 实现分片上传逻辑
根据上传分片的编号,将分片存储到文件系统中,代码示例如下:
// 设置存储路径和文件名
String newPath = Paths.get(path, md5, chunk.toString()).toString();
File newFile = new File(newPath);
// 创建存储路径
FileUtils.forceMkdirParent(newFile);
// 将分片写入文件系统
try (InputStream is = file.getInputStream()) {
FileUtils.copyToFile(is, newFile);
} catch (IOException e) {
e.printStackTrace();
return ResponseVO.failure("文件上传失败");
}
3.2.3 实现分片合并逻辑
根据已上传的分片列表,将分片合并成完整的文件,代码示例如下:
// 获取所有分片文件
List<File> chunkFiles = Stream.of(dir.listFiles())
.filter(file -> file.getName().startsWith(md5) && file.getName().contains("."))
.sorted(Comparator.comparingInt(o -> Integer.valueOf(o.getName().substring(o.getName().lastIndexOf(".") + 1))))
.collect(Collectors.toList());
// 合并文件
String filePath = Paths.get(path, md5).toString();
FileOutputStream fos = null;
try {
fos = new FileOutputStream(filePath);
for (File chunkFile : chunkFiles) {
// 将分片文件写入目标文件
try (InputStream is = new FileInputStream(chunkFile)) {
IOUtils.copy(is, fos);
}
}
} catch (IOException e) {
e.printStackTrace();
return ResponseVO.failure("文件合并失败");
} finally {
IOUtils.closeQuietly(fos);
}
3.2.4 实现断点续传逻辑
当文件上传中断时,重新上传时可以从已上传的分片开始上传,代码示例如下:
// 判断文件是否已上传
String filePath = Paths.get(path, md5).toString();
File targetFile = new File(filePath);
if (targetFile.exists()) {
return ResponseVO.success();
}
// 获取所有已上传的分片文件
List<File> chunkFiles = Stream.of(dir.listFiles())
.filter(file -> file.getName().startsWith(md5) && file.getName().contains("."))
.sorted(Comparator.comparingInt(o -> Integer.valueOf(o.getName().substring(o.getName().lastIndexOf(".") + 1))))
.collect(Collectors.toList());
// 判断分片是否上传完整
if (chunk == null || chunks == null) {
return ResponseVO.failure("分片上传失败");
} else if (chunk.equals(chunks - 1) && chunkFiles.size() == chunks) {
// 如果分片全部上传完成,则进行分片合并
// ...
} else {
// 否则,只需要继续上传未上传的分片即可
// ...
}
3.2.5 实现极速秒传逻辑
当存在完全相同的文件时,无需再次上传,直接返回上传成功的结果即可,代码示例如下:
// 判断文件是否已上传
String filePath = Paths.get(path, md5).toString();
File targetFile = new File(filePath);
if (targetFile.exists()) {
return ResponseVO.success();
}
4. 示例说明
下面我们通过两个示例进一步说明上述方案的实现。
4.1 示例一
假设用户上传一个 10MB 大小的图片文件,我们可以通过以下步骤进行处理:
- 前端 JavaScript 代码将图片文件切分为若干 5MB 大小的文件,分别进行上传,并将上传的路径设置为
/upload
接口路径。 - 后端通过 SpringBoot 的 MVC 框架接收上传的文件,判断文件是否已上传,如果不存在则创建对应目录,并将文件保存到目录中。
- 当所有分片上传完成后,后端通过将已上传的分片文件合并为完整的文件。
- 判断文件是否为重复上传,如果已经存在相同的文件,则直接返回上传成功的结果。
4.2 示例二
现有一个 10MB 大小的图片文件已经上传过,用户想要重新上传相同的文件,我们可以通过以下步骤进行处理:
- 前端 JavaScript 代码计算已有的图片文件的 MD5 值,并将该值传递到后端
/upload
接口中。 - 后端接收到 MD5 值后,查询文件系统中是否存在相同的文件,如果存在则直接返回上传成功的结果。
- 如果文件不存在或者某些分片没有上传完成,则从未上传的分片开始继续上传,直到上传完成,在此过程中需要进行断点续传的处理。
参考文章:http://blog.ncmem.com/wordpress/2023/11/16/springboot-%e4%b8%ad%e5%a4%a7%e6%96%87%e4%bb%b6%ef%bc%88%e5%88%86%e7%89%87%e4%b8%8a%e4%bc%a0%ef%bc%89%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e4%b8%8e%e6%9e%81%e9%80%9f%e7%a7%92%e4%bc%a0%e5%8a%9f-2/
欢迎入群一起讨论
posted on 2023-11-16 15:43 Xproer-松鼠 阅读(1212) 评论(0) 编辑 收藏 举报