Spring Boot超大文件上傳的正確方式
環境:SpringBoot3.4.0
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("文件合并完成");
}
}
這里就是遍歷目錄中的所有文件,然后按照順序寫入到一個目標文件中即可。這樣我們就完成了文件的合并。
到此我們實現了文件的分塊上傳功能。