Spring Boot超大文件上传的正确方式
文章标签:
文件上传 html
1. 简介
文件上传功能是个非常常见的需求,它允许用户将本地计算机上的文件通过网络传输到远程服务器。然而,如果不对大文件的上传进行适当的控制,很可能会对服务器造成以下不良影响:
- 网络不稳定性:大文件上传耗时较长,期间网络的任何不稳定性都可能导致上传失败,需要重新上传整个文件,这不仅耗时而且效率低下。
- 带宽限制:在带宽有限的网络环境中,大文件上传可能会占用大量带宽,导致其他网络活动受阻,影响用户体验。
- 服务器负担:一次性处理大量数据会给服务器带来巨大负担,尤其是在高并发的情况下,可能导致服务器响应缓慢或崩溃。
如何解决大文件上传的问题呢?接下来,我们将介绍一种有效的解决方案——分片上传。
分片上传文件指的是将大文件分割成较小的部分(即分片),然后依次或并行地将这些分片上传到服务器的过程。一旦所有分片都上传完毕,服务器会将它们合并以重新创建出原始文件。
分片上传原理:
- 在客户端将文件分割成较小的分片
- 将每个分片单独上传到服务器
- 所有分片上传完成后,利用这些分片重新构建出原始文件
接下来,我们将通过Spring Boot 3与Vue 3的结合来实现大文件的分片上传功能。
2. 实战案例
2.1 前端页面
我们仅为了演示文件的分片上传功能,所以设计的页面非常简洁,仅包含三个按钮,页面效果如下所示:
前端代码
<el-upload ref="upload" class="upload-demo"
:limit="1" :auto-upload="false" :http-request="uploadFile">
<template #trigger>
<el-button type="primary" style="margin-right: 10px;">选择文件</el-button>
</template>
<el-button class="ml-3" type="success" @click="submitUpload">
上传文件
</el-button>
</el-upload>
<el-button class="ml-3" type="primary" @click="mergeFile">合并文件</el-button>
JavaScript代码
<script setup name="upload">
import { ref } from 'vue'
const upload = ref('')
let fileName = ''
/**拆分文件,这里估计将文件每2M进行拆分*/
const uploadFileInChunks = file => {
const chunkSize = 1024 * 1024 * 2
let start = 0
let chunkIndex = 0
while (start < file.size) {
const chunk = file.slice(start, start + chunkSize)
console.log(chunk)
fileName = file.name
uploadChunk(chunk, chunkIndex, fileName)
start += chunkSize
chunkIndex++
}
}
/**对每一个拆分的文件进行上传;这就就成了小文件上传*/
const uploadChunk = (chunk, chunkIndex, fileName) => {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', chunkIndex)
formData.append('fileName', fileName)
fetch('http://localhost:8080/upload-chunk', {
method: 'POST',
body: formData
}).then(resp => {
console.log(resp)
})
}
const uploadFile = (opt) => {
uploadFileInChunks(opt.file)
}
const submitUpload = () => {
upload.value.submit()
}
/**合并文件*/
const mergeFile = () => {
const formData = new FormData()
formData.append('fileName', fileName)
fetch('http://localhost:8080/merge-chunks', {
method: 'POST',
body: formData
}).then(resp => {
console.log(resp)
})
}
</script>
前端代码还是非常简单的;其核心就是拿到上传文件的File对象,然后对文件进行拆分。
2.2 文件上传接口
@RestController
public class ChunkController {
private static final String TEMP_DIR = "d:\\upload\\";
@PostMapping("/upload-chunk")
public ResponseEntity<String> uploadChunk(
@RequestParam("chunk") MultipartFile chunk,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("fileName") String fileName) throws IOException {
File dir = new File(TEMP_DIR + fileName);
if (!dir.exists()) {
dir.mkdirs();
}
File chunkFile = new File(dir, "chunk_" + chunkIndex);
try (OutputStream os = new FileOutputStream(chunkFile)) {
os.write(chunk.getBytes());
}
return ResponseEntity.ok("Chunk " + chunkIndex + " uploaded successfully.");
}
}
这里非常的简单与我们平时的文件上传一模一样。
接下来,我们就可以进行文件的上传了
这里选择了一个25MB大小的文件,点击上传后控制台输出:
这里拆分成了13个小文件进行上传。
最终,后台服务上的文件如下:
以上传的文件名创建了目录,存分块后的小文件。
文件上传完成后,最后我们就要对这些文件进行合并处理了。
2.3 合并文件接口
@RestController
public class ChunkController {
private static final String TEMP_DIR = "d:\\upload\\";
private static final String TARGET_DIR = "d:\\upload\\result\\";
@PostMapping("/merge-chunks")
public ResponseEntity<String> mergeChunks(
@RequestParam("fileName") String fileName) throws IOException {
File dir = new File(TEMP_DIR + fileName);
File mergedFile = new File(TARGET_DIR + fileName);
try (OutputStream os = new FileOutputStream(mergedFile)) {
for (int i = 0, len = dir.listFiles().length; i < len; i++) {
File chunkFile = new File(dir, "chunk_" + i);
Files.copy(chunkFile.toPath(), os);
chunkFile.delete();
}
}
dir.delete();
return ResponseEntity.ok("文件合并完成");
}
}
这里就是遍历目录中的所有文件,然后按照顺序写入到一个目标文件中即可。这样我们就完成了文件的合并。
到此我们实现了文件的分块上传功能。