Spring Boot超大文件上傳的正確方式
環(huán)境:SpringBoot3.4.0
1. 簡介
文件上傳功能是個非常常見的需求,它允許用戶將本地計(jì)算機(jī)上的文件通過網(wǎng)絡(luò)傳輸?shù)竭h(yuǎn)程服務(wù)器。然而,如果不對大文件的上傳進(jìn)行適當(dāng)?shù)目刂?,很可能會對服?wù)器造成以下不良影響:
- 網(wǎng)絡(luò)不穩(wěn)定性:大文件上傳耗時較長,期間網(wǎng)絡(luò)的任何不穩(wěn)定性都可能導(dǎo)致上傳失敗,需要重新上傳整個文件,這不僅耗時而且效率低下。
- 帶寬限制:在帶寬有限的網(wǎng)絡(luò)環(huán)境中,大文件上傳可能會占用大量帶寬,導(dǎo)致其他網(wǎng)絡(luò)活動受阻,影響用戶體驗(yàn)。
- 服務(wù)器負(fù)擔(dān):一次性處理大量數(shù)據(jù)會給服務(wù)器帶來巨大負(fù)擔(dān),尤其是在高并發(fā)的情況下,可能導(dǎo)致服務(wù)器響應(yīng)緩慢或崩潰。
如何解決大文件上傳的問題呢?接下來,我們將介紹一種有效的解決方案——分片上傳。
分片上傳文件指的是將大文件分割成較小的部分(即分片),然后依次或并行地將這些分片上傳到服務(wù)器的過程。一旦所有分片都上傳完畢,服務(wù)器會將它們合并以重新創(chuàng)建出原始文件。
分片上傳原理:
- 在客戶端將文件分割成較小的分片
- 將每個分片單獨(dú)上傳到服務(wù)器
- 所有分片上傳完成后,利用這些分片重新構(gòu)建出原始文件
接下來,我們將通過Spring Boot 3與Vue 3的結(jié)合來實(shí)現(xiàn)大文件的分片上傳功能。
2. 實(shí)戰(zhàn)案例
2.1 前端頁面
我們僅為了演示文件的分片上傳功能,所以設(shè)計(jì)的頁面非常簡潔,僅包含三個按鈕,頁面效果如下所示:
前端代碼
<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 = ''
/**拆分文件,這里估計(jì)將文件每2M進(jìn)行拆分*/
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++
}
}
/**對每一個拆分的文件進(jìn)行上傳;這就就成了小文件上傳*/
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對象,然后對文件進(jìn)行拆分。
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.");
}
}
這里非常的簡單與我們平時的文件上傳一模一樣。
接下來,我們就可以進(jìn)行文件的上傳了
圖片
這里選擇了一個25MB大小的文件,點(diǎn)擊上傳后控制臺輸出:
圖片
這里拆分成了13個小文件進(jìn)行上傳。
最終,后臺服務(wù)上的文件如下:
以上傳的文件名創(chuàng)建了目錄,存分塊后的小文件。
文件上傳完成后,最后我們就要對這些文件進(jìn)行合并處理了。
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("文件合并完成");
}
}
這里就是遍歷目錄中的所有文件,然后按照順序?qū)懭氲揭粋€目標(biāo)文件中即可。這樣我們就完成了文件的合并。
到此我們實(shí)現(xiàn)了文件的分塊上傳功能。