SpringBoot中大文件分片上傳、斷點(diǎn)續(xù)傳與進(jìn)度條的實現(xiàn)全解析
一、引言
在日常 Web 應(yīng)用開發(fā)中,文件上傳是一個極為常見的功能。無論是用戶頭像的上傳、文檔資料的提交,還是圖片、視頻等多媒體文件的傳輸,文件上傳功能都扮演著關(guān)鍵角色。然而,當(dāng)涉及到大文件上傳時,傳統(tǒng)的單次上傳方式往往會暴露出諸多問題。
大文件上傳時,若遭遇網(wǎng)絡(luò)不穩(wěn)定的情況,如網(wǎng)絡(luò)突然中斷或波動,整個文件就需要重新上傳,這不僅浪費(fèi)用戶的時間和流量,還會導(dǎo)致用戶體驗急劇下降。服務(wù)器也會對上傳文件的大小有所限制,一旦文件超出限制,上傳便會失敗。同時,一次性接收整個大文件可能會使服務(wù)端內(nèi)存不足,出現(xiàn)內(nèi)存溢出的情況。而且,在長時間的上傳過程中,若無法展示上傳進(jìn)度條,用戶只能在無盡的等待中猜測上傳狀態(tài),這很容易讓用戶產(chǎn)生焦慮情緒,甚至可能導(dǎo)致用戶誤操作中斷上傳。
為了解決這些問題,分片上傳、斷點(diǎn)續(xù)傳與進(jìn)度條展示等技術(shù)應(yīng)運(yùn)而生。而 Spring Boot 作為當(dāng)下流行的 Java 開發(fā)框架,提供了豐富的功能和便捷的工具,能夠幫助我們高效地實現(xiàn)這些功能。接下來,就讓我們深入探討如何使用 Spring Boot 實現(xiàn)分片上傳、斷點(diǎn)續(xù)傳與進(jìn)度條展示。
二、技術(shù)原理介紹
分片上傳
分片上傳,即將一個大文件按照固定的大小分割成多個較小的分片(也叫塊),然后分別對這些分片進(jìn)行上傳 。每個分片都有一個唯一的標(biāo)識,通常可以通過文件的唯一標(biāo)識(如文件的 MD5 值)與分片的序號來確定。在前端,通過 JavaScript 的 File API 可以方便地實現(xiàn)文件的分片操作。例如,使用file\.slice\(\)方法按照設(shè)定的分片大?。ㄈ?5MB)對文件進(jìn)行切割。
在后端,Spring Boot 接收到每個分片后,會根據(jù)文件唯一標(biāo)識與分片序號,將分片臨時存儲在服務(wù)器的指定目錄中。當(dāng)所有分片都上傳完成后,后端會按照分片序號的順序,將這些分片依次讀取并合并成最終的完整文件。這樣做的好處是,即使在上傳過程中某個分片上傳失敗,也只需重新上傳該分片,而無需重新上傳整個文件,大大提高了上傳的穩(wěn)定性和效率。
斷點(diǎn)續(xù)傳
斷點(diǎn)續(xù)傳是建立在分片上傳基礎(chǔ)之上的一項技術(shù)。當(dāng)文件上傳過程中由于網(wǎng)絡(luò)中斷、服務(wù)器故障等原因?qū)е律蟼髦袛鄷r,客戶端會記錄下已經(jīng)上傳成功的分片信息,比如已經(jīng)上傳的分片序號。當(dāng)用戶再次嘗試上傳該文件時,客戶端會根據(jù)之前記錄的信息,從上次中斷的位置開始,繼續(xù)上傳剩余的分片。
在服務(wù)端,需要保存已上傳分片的狀態(tài)信息??梢酝ㄟ^在數(shù)據(jù)庫中記錄每個文件的上傳進(jìn)度,或者使用緩存(如 Redis)來存儲已上傳的分片列表等方式實現(xiàn)。當(dāng)客戶端發(fā)起續(xù)傳請求時,服務(wù)端會根據(jù)這些記錄,判斷哪些分片已經(jīng)上傳成功,哪些還需要上傳,從而實現(xiàn)斷點(diǎn)續(xù)傳的功能。這樣就避免了用戶因為上傳中斷而需要重新上傳整個文件的煩惱,極大地提升了用戶體驗。
進(jìn)度條
進(jìn)度條的實現(xiàn)原理是通過監(jiān)聽文件上傳過程中的數(shù)據(jù)傳輸量,實時計算并展示上傳進(jìn)度。在前端,當(dāng)使用 XMLHttpRequest 或 Fetch API 進(jìn)行文件上傳時,可以監(jiān)聽其progress事件。這個事件會在上傳過程中不斷觸發(fā),通過事件對象可以獲取到已經(jīng)上傳的字節(jié)數(shù)和文件的總字節(jié)數(shù)。根據(jù)這兩個數(shù)據(jù),就可以計算出上傳的進(jìn)度百分比,即已上傳字節(jié)數(shù) / 文件總字節(jié)數(shù) \* 100%。
然后,將這個進(jìn)度百分比更新到前端頁面的進(jìn)度條元素上,比如通過修改\<progress\>標(biāo)簽的value屬性,或者使用 CSS 動畫來模擬進(jìn)度條的增長,從而讓用戶直觀地了解文件上傳的進(jìn)度情況。在后端,Spring Boot 可以配合前端,通過一些機(jī)制(如 WebSocket、SSE 等)將上傳進(jìn)度實時推送給前端,確保前端展示的進(jìn)度與后端實際的上傳進(jìn)度保持一致 。
三、Spring Boot 實現(xiàn)分片上傳
項目搭建
首先,我們使用 Spring Initializr(start.spring.io/ )來創(chuàng)建一個 Spring Boot 項目。在這個網(wǎng)頁中,我們可以對項目進(jìn)行多項配置,比如選擇項目的構(gòu)建工具(Maven 或 Gradle),指定 Spring Boot 的版本,以及添加所需的依賴。這里,我們主要添加spring\-boot\-starter\-web依賴,它提供了構(gòu)建 Web 應(yīng)用所需的核心組件,讓我們能夠方便地創(chuàng)建 HTTP 接口 。同時,為了處理文件上傳,Spring Boot 默認(rèn)已經(jīng)集成了相關(guān)的依賴,無需額外引入。
如果使用 Maven 構(gòu)建項目,在pom\.xml文件中,添加如下依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>如果是 Gradle 項目,在build\.gradle文件中添加:
implementation 'org.springframework.boot:spring-boot-starter-web'
創(chuàng)建好項目后,將其導(dǎo)入到我們喜歡的集成開發(fā)環(huán)境(IDE)中,比如 IntelliJ IDEA 或 Eclipse,這樣就可以開始后續(xù)的開發(fā)工作了。
前端實現(xiàn)
前端我們可以使用原生 JavaScript 結(jié)合fetch API 來實現(xiàn)文件分片。下面是一個簡單的示例代碼:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文件分片上傳</title>
</head>
<body>
<input type="file" id="largeFile">
<button onclick="startUpload()">開始上傳</button>
<div id="progressBar"></div>
<script>
async function startUpload() {
const file = document.getElementById('largeFile').files[0];
if (!file) return;
// 配置參數(shù)
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB分片
const TOTAL_CHUNKS = Math.ceil(file.size / CHUNK_SIZE);
const FILE_ID = `${file.name}-${file.size}-${Date.now()}`;
// 創(chuàng)建進(jìn)度跟蹤器
const uploadedChunks = new Set();
// 并行上傳控制(最大5并發(fā))
const parallelLimit = 5;
let currentUploads = 0;
let activeChunks = 0;
for (let chunkIndex = 0; chunkIndex < TOTAL_CHUNKS; ) {
if (currentUploads >= parallelLimit) {
await new Promise(resolve => setTimeout(resolve, 500));
continue;
}
if (uploadedChunks.has(chunkIndex)) {
chunkIndex++;
continue;
}
currentUploads++;
activeChunks++;
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
uploadChunk(chunk, chunkIndex, FILE_ID, TOTAL_CHUNKS, file.name).then(() => {
uploadedChunks.add(chunkIndex);
updateProgress(uploadedChunks.size, TOTAL_CHUNKS);
}).catch(err => console.error(`分片${chunkIndex}失敗:`, err)).finally(() => {
currentUploads--;
activeChunks--;
});
chunkIndex++;
}
// 檢查所有分片完成
const checkCompletion = setInterval(() => {
if (activeChunks === 0 && uploadedChunks.size === TOTAL_CHUNKS) {
clearInterval(checkCompletion);
completeUpload(FILE_ID, file.name);
}
}, 1000);
}
async function uploadChunk(chunk, index, fileId, total, filename) {
const formData = new FormData();
formData.append('file', chunk, filename);
formData.append('chunkIndex', index);
formData.append('totalChunks', total);
formData.append('fileId', fileId);
return fetch('/api/upload/chunk', {
method: 'POST',
body: formData
}).then(res => {
if (!res.ok) throw new Error('上傳失敗');
});
}
function updateProgress(uploaded, total) {
const progressBar = document.getElementById('progressBar');
const progress = (uploaded / total) * 100;
progressBar.style.width = `${progress}%`;
progressBar.textContent = `${progress.toFixed(2)}%`;
}
async function completeUpload(fileId, filename) {
const formData = new FormData();
formData.append('fileId', fileId);
formData.append('filename', filename);
return fetch('/api/upload/merge', {
method: 'POST',
body: formData
}).then(res => {
if (res.ok) {
alert('上傳成功!');
} else {
throw new Error('合并失敗');
}
});
}
</script>
</body>
</html>在這段代碼中,startUpload函數(shù)負(fù)責(zé)處理文件分片和上傳的邏輯。首先獲取用戶選擇的文件,然后根據(jù)設(shè)定的CHUNK\_SIZE(這里是 5MB)計算總分片數(shù)TOTAL\_CHUNKS,并生成一個唯一的FILE\_ID。通過一個for循環(huán),對文件進(jìn)行分片,每個分片通過uploadChunk函數(shù)上傳到后端。uploadChunk函數(shù)將分片數(shù)據(jù)、分片序號、總分片數(shù)和文件唯一標(biāo)識等信息通過FormData對象發(fā)送到/api/upload/chunk接口。updateProgress函數(shù)用于更新上傳進(jìn)度條,根據(jù)已上傳的分片數(shù)和總分片數(shù)計算進(jìn)度百分比,并更新頁面上進(jìn)度條的寬度和顯示文本 。當(dāng)所有分片上傳完成后,通過completeUpload函數(shù)向/api/upload/merge接口發(fā)送合并請求。
后端實現(xiàn)
后端在 Spring Boot 中創(chuàng)建接收分片的接口,并將分片保存到臨時目錄。首先創(chuàng)建一個控制器類FileUploadController:
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
@PostMapping("/chunk")
public String handleChunk(@RequestParam("file") MultipartFile file,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("fileId") String fileId) {
try {
// 創(chuàng)建臨時文件存儲目錄
Path tempDirPath = Paths.get(TEMP_DIR, fileId);
Files.createDirectories(tempDirPath);
// 構(gòu)建分片文件路徑
Path chunkFilePath = tempDirPath.resolve(chunkIndex + ".part");
// 保存分片文件
file.transferTo(chunkFilePath.toFile());
return "分片上傳成功";
} catch (IOException e) {
e.printStackTrace();
return "分片上傳失敗";
}
}
@PostMapping("/merge")
public String mergeChunks(@RequestParam("fileId") String fileId,
@RequestParam("filename") String filename) {
try {
// 構(gòu)建臨時文件存儲目錄
Path tempDirPath = Paths.get(TEMP_DIR, fileId);
// 構(gòu)建最終合并文件路徑
Path mergedFilePath = Paths.get(TEMP_DIR, filename);
// 使用NIO進(jìn)行文件合并
Files.walk(tempDirPath)
.filter(path ->!path.equals(tempDirPath))
.sorted((p1, p2) -> {
String name1 = p1.getFileName().toString();
String name2 = p2.getFileName().toString();
int index1 = Integer.parseInt(name1.substring(0, name1.lastIndexOf(".")));
int index2 = Integer.parseInt(name2.substring(0, name2.lastIndexOf(".")));
return Integer.compare(index1, index2);
})
.forEach(chunkPath -> {
try {
Files.copy(chunkPath, mergedFilePath, java.nio.file.StandardCopyOption.APPEND);
Files.delete(chunkPath);
} catch (IOException e) {
e.printStackTrace();
}
});
// 刪除臨時目錄
Files.delete(tempDirPath);
return "文件合并成功";
} catch (IOException e) {
e.printStackTrace();
return "文件合并失敗";
}
}
}
在FileUploadController中,handleChunk方法用于處理前端上傳的分片。它接收前端傳來的分片文件、分片序號、總分片數(shù)和文件唯一標(biāo)識。首先創(chuàng)建以fileId命名的臨時目錄,然后在該目錄下根據(jù)分片序號構(gòu)建分片文件路徑,最后將接收到的分片文件保存到該路徑。mergeChunks方法用于處理文件合并。它接收文件唯一標(biāo)識和文件名,先構(gòu)建臨時目錄和最終合并文件的路徑。通過Files\.walk方法遍歷臨時目錄下的所有分片文件,按照分片序號進(jìn)行排序,然后依次將分片文件內(nèi)容追加到最終合并文件中,并在合并完成后刪除臨時分片文件和臨時目錄 。如果在保存分片或合并文件過程中出現(xiàn)IOException異常,則返回相應(yīng)的失敗信息。
四、Spring Boot 實現(xiàn)斷點(diǎn)續(xù)傳
前端實現(xiàn)
在前端,我們需要記錄上傳進(jìn)度,以便在上傳中斷后能夠準(zhǔn)確地知道已經(jīng)上傳了哪些分片 ??梢酝ㄟ^在本地存儲(如localStorage)中記錄已上傳的分片序號來實現(xiàn)這一功能。當(dāng)上傳中斷后,再次點(diǎn)擊上傳按鈕時,前端代碼會讀取本地存儲中的已上傳分片信息。
async function startUpload() {
const file = document.getElementById('largeFile').files[0];
if (!file) return;
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB分片
const TOTAL_CHUNKS = Math.ceil(file.size / CHUNK_SIZE);
const FILE_ID = `${file.name}-${file.size}-${Date.now()}`;
// 從本地存儲獲取已上傳分片
const uploadedChunks = JSON.parse(localStorage.getItem(FILE_ID)) || new Set();
for (let chunkIndex = 0; chunkIndex < TOTAL_CHUNKS; chunkIndex++) {
if (uploadedChunks.has(chunkIndex)) {
continue;
}
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
try {
await uploadChunk(chunk, chunkIndex, FILE_ID, TOTAL_CHUNKS, file.name);
uploadedChunks.add(chunkIndex);
localStorage.setItem(FILE_ID, JSON.stringify(Array.from(uploadedChunks)));
updateProgress(uploadedChunks.size, TOTAL_CHUNKS);
} catch (err) {
console.error(`分片${chunkIndex}上傳失敗:`, err);
// 上傳失敗時可以在這里添加重試邏輯
}
}
if (uploadedChunks.size === TOTAL_CHUNKS) {
completeUpload(FILE_ID, file.name);
}
}
在上述代碼中,startUpload函數(shù)在開始上傳前,先從localStorage中讀取已上傳的分片信息。如果某個分片已經(jīng)上傳過(通過uploadedChunks\.has\(chunkIndex\)判斷),則跳過該分片的上傳。當(dāng)一個分片上傳成功后,將其序號添加到uploadedChunks集合中,并更新localStorage中的記錄 。這樣,即使上傳過程中出現(xiàn)中斷,再次上傳時也能從上次中斷的位置繼續(xù)。
后端實現(xiàn)
后端需要提供一個接口,用于查詢某個文件已經(jīng)上傳的分片。在FileUploadController中添加如下方法:
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
@GetMapping("/uploadedChunks")
public List<Integer> getUploadedChunks(@RequestParam("fileId") String fileId) {
Path tempDirPath = Paths.get(TEMP_DIR, fileId);
if (!Files.exists(tempDirPath)) {
return new ArrayList<>();
}
List<Integer> uploadedChunks = new ArrayList<>();
try {
Files.list(tempDirPath).forEach(path -> {
String filename = path.getFileName().toString();
int index = Integer.parseInt(filename.substring(0, filename.lastIndexOf(".")));
uploadedChunks.add(index);
});
} catch (Exception e) {
e.printStackTrace();
}
return uploadedChunks;
}
// 其他方法...
}
getUploadedChunks方法接收fileId作為參數(shù),根據(jù)fileId構(gòu)建臨時目錄路徑。如果該目錄不存在,則返回一個空列表,表示沒有上傳任何分片。如果目錄存在,則遍歷目錄下的所有文件,通過文件名解析出分片序號,并將其添加到uploadedChunks列表中,最后返回該列表 。前端在上傳前,可以先調(diào)用這個接口,獲取已上傳的分片信息,從而確定需要上傳哪些分片,實現(xiàn)斷點(diǎn)續(xù)傳的功能。
五、Spring Boot 實現(xiàn)進(jìn)度條
前端實現(xiàn)
在前端展示進(jìn)度條,我們可以使用 HTML5 提供的\<progress\>標(biāo)簽。這個標(biāo)簽提供了一種簡單且語義化的方式來顯示任務(wù)的完成進(jìn)度。它有兩個主要屬性:value表示當(dāng)前已完成的值,max表示任務(wù)總值 。瀏覽器會根據(jù)這兩個值自動計算并顯示進(jìn)度百分比。
<progress id="uploadProgress" value="0" max="100"></progress>
然后,通過 JavaScript 監(jiān)聽文件上傳的progress事件來動態(tài)更新value屬性,從而實現(xiàn)進(jìn)度條的實時更新。結(jié)合前面分片上傳的前端代碼,在uploadChunk函數(shù)中,我們可以進(jìn)一步完善對進(jìn)度條的更新邏輯:
async function uploadChunk(chunk, index, fileId, total, filename) {
const formData = new FormData();
formData.append('file', chunk, filename);
formData.append('chunkIndex', index);
formData.append('totalChunks', total);
formData.append('fileId', fileId);
const controller = new AbortController();
const { signal } = controller;
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload/chunk', true);
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
document.getElementById('uploadProgress').value = percentComplete;
}
};
xhr.onload = function () {
if (xhr.status === 200) {
// 上傳成功后的處理
} else {
// 上傳失敗后的處理
}
};
xhr.send(formData, signal);
// 這里可以添加取消上傳的邏輯,比如在某個條件下調(diào)用 controller.abort()
return new Promise((resolve, reject) => {
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error('上傳失敗'));
}
}
};
});
}
在上述代碼中,xhr\.upload\.onprogress事件處理函數(shù)會在上傳過程中不斷被觸發(fā)。event\.lengthComputable用于判斷是否可以計算上傳進(jìn)度,如果可以,則通過event\.loaded(已上傳的字節(jié)數(shù))和event\.total(文件總字節(jié)數(shù))計算出上傳的百分比,并更新uploadProgress進(jìn)度條的value屬性 。這樣,用戶就能在前端直觀地看到每個分片的上傳進(jìn)度。
除了使用原生的\<progress\>標(biāo)簽,我們也可以使用一些第三方 UI 組件庫來實現(xiàn)更美觀、功能更豐富的進(jìn)度條效果,比如 Element - UI(適用于 Vue 項目)、Ant Design(適用于 React 項目)等 。以 Element - UI 為例,在 Vue 項目中使用其進(jìn)度條組件的代碼如下:
<template>
<el-progress :percentage="progress" status="active"></el-progress>
</template>
<script>
export default {
data() {
return {
progress: 0
};
},
methods: {
async uploadChunk(chunk, index, fileId, total, filename) {
// 上傳邏輯...
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload/chunk', true);
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
this.progress = percentComplete;
}
}.bind(this);
// 其他代碼...
}
}
};
</script>在這段 Vue 代碼中,el \- progress是 Element - UI 提供的進(jìn)度條組件,percentage屬性綁定了progress數(shù)據(jù),通過在上傳過程中更新progress的值,實現(xiàn)進(jìn)度條的動態(tài)展示 。status=\&\#34;active\&\#34;可以讓進(jìn)度條顯示為動態(tài)加載的效果,增強(qiáng)用戶體驗。
后端實現(xiàn)
后端將上傳進(jìn)度數(shù)據(jù)傳遞給前端,有兩種常見的方式:WebSocket 實時推送和接口輪詢獲取。
WebSocket 實時推送:使用 WebSocket 可以實現(xiàn)服務(wù)器與客戶端之間的全雙工通信,服務(wù)器能夠主動將上傳進(jìn)度推送給客戶端。首先,在 Spring Boot 項目中添加 WebSocket 依賴,如果使用 Maven,在pom\.xml中添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>然后創(chuàng)建一個 WebSocket 配置類WebSocketConfig:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
接下來創(chuàng)建一個 WebSocket 處理器類FileUploadWebSocketHandler:
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Component
public class FileUploadWebSocketHandler implements WebSocketHandler {
private static final ConcurrentMap<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessionMap.put(session.getId(), session);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
// 處理接收到的消息,這里暫不處理前端發(fā)來的消息
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
session.close(CloseStatus.SERVER_ERROR);
sessionMap.remove(session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessionMap.remove(session.getId());
}
@Override
public boolean supportsPartialMessages() {
return false;
}
public void sendProgress(String sessionId, int progress) {
WebSocketSession session = sessionMap.get(sessionId);
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(String.valueOf(progress)));
} catch (Exception e) {
e.printStackTrace();
sessionMap.remove(sessionId);
}
}
}
}
在FileUploadController中,當(dāng)處理分片上傳時,計算上傳進(jìn)度并通過 WebSocket 推送:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
@Autowired
private FileUploadWebSocketHandler fileUploadWebSocketHandler;
@PostMapping("/chunk")
public String handleChunk(@RequestParam("file") MultipartFile file,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("fileId") String fileId,
@RequestParam("sessionId") String sessionId) {
try {
// 創(chuàng)建臨時文件存儲目錄
Path tempDirPath = Paths.get(TEMP_DIR, fileId);
Files.createDirectories(tempDirPath);
// 構(gòu)建分片文件路徑
Path chunkFilePath = tempDirPath.resolve(chunkIndex + ".part");
// 保存分片文件
file.transferTo(chunkFilePath.toFile());
// 計算上傳進(jìn)度
int progress = (int) ((chunkIndex + 1) * 100 / totalChunks);
fileUploadWebSocketHandler.sendProgress(sessionId, progress);
return "分片上傳成功";
} catch (IOException e) {
e.printStackTrace();
return "分片上傳失敗";
}
}
// 其他方法...
}
在前端,建立 WebSocket 連接并監(jiān)聽進(jìn)度消息:
const socket = new WebSocket('ws://localhost:8080/websocket');
socket.onmessage = function (event) {
const progress = parseInt(event.data);
document.getElementById('uploadProgress').value = progress;
};
在上述代碼中,后端通過FileUploadWebSocketHandler的sendProgress方法將上傳進(jìn)度發(fā)送給對應(yīng)的 WebSocket 會話。前端建立 WebSocket 連接后,通過onmessage事件接收并更新進(jìn)度條 。
接口輪詢獲取:接口輪詢的方式是前端定時向后端發(fā)送請求,獲取最新的上傳進(jìn)度。在后端FileUploadController中添加一個獲取進(jìn)度的接口:
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/upload")
public class FileUploadController {
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
private final Map<String, Integer> progressMap = new HashMap<>();
@PostMapping("/chunk")
public String handleChunk(@RequestParam("file") MultipartFile file,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks,
@RequestParam("fileId") String fileId) {
try {
// 保存分片邏輯...
// 計算上傳進(jìn)度
int progress = (int) ((chunkIndex + 1) * 100 / totalChunks);
progressMap.put(fileId, progress);
return "分片上傳成功";
} catch (IOException e) {
e.printStackTrace();
return "分片上傳失敗";
}
}
@GetMapping("/progress")
public int getProgress(@RequestParam("fileId") String fileId) {
return progressMap.getOrDefault(fileId, 0);
}
// 其他方法...
}
在前端,使用setInterval定時調(diào)用接口獲取進(jìn)度并更新進(jìn)度條:
const fileId = "your - unique - file - id";
const progressInterval = setInterval(() => {
fetch(`/api/upload/progress?fileId=${fileId}`)
.then(response => response.json())
.then(data => {
const progress = data;
document.getElementById('uploadProgress').value = progress;
});
}, 1000);
在上述代碼中,前端每隔 1 秒(1000 毫秒)向后端的/api/upload/progress接口發(fā)送請求,獲取文件的上傳進(jìn)度,并更新進(jìn)度條。這種方式實現(xiàn)相對簡單,但相比 WebSocket 實時推送,會增加一定的網(wǎng)絡(luò)開銷,并且進(jìn)度更新可能不夠及時 。
六、整合與優(yōu)化
整合步驟
將分片上傳、斷點(diǎn)續(xù)傳和進(jìn)度條功能整合在一起時,首先要確保前端和后端的交互邏輯清晰且一致 。在前端,需要統(tǒng)一文件選擇、上傳操作的觸發(fā)邏輯。例如,在一個文件上傳組件中,通過一個按鈕來觸發(fā)文件選擇對話框,當(dāng)用戶選擇文件后,自動觸發(fā)分片上傳流程。在這個流程中,既要包含分片上傳的邏輯,如按固定大小對文件進(jìn)行分片、生成唯一的文件標(biāo)識等,也要融入斷點(diǎn)續(xù)傳的邏輯,即從本地存儲或后端獲取已上傳的分片信息,跳過已上傳的分片 。同時,實時更新進(jìn)度條的顯示,將每個分片的上傳進(jìn)度及時反饋給用戶。
在后端,各個功能對應(yīng)的接口要協(xié)同工作。接收分片上傳的接口不僅要保存分片文件,還要記錄上傳進(jìn)度,以便為進(jìn)度條功能提供數(shù)據(jù)支持 。查詢已上傳分片的接口要與斷點(diǎn)續(xù)傳功能緊密配合,準(zhǔn)確地返回已上傳的分片列表,讓前端能夠根據(jù)這個列表確定需要上傳的剩余分片。文件合并的接口要在所有分片上傳完成后,正確地將分片合并成完整文件,并處理好合并過程中的異常情況 。在整合過程中,要注意數(shù)據(jù)的一致性和準(zhǔn)確性,比如文件唯一標(biāo)識在前端和后端的傳遞和使用要保持一致,避免出現(xiàn)因標(biāo)識不一致而導(dǎo)致的上傳錯誤。
性能優(yōu)化
從網(wǎng)絡(luò)優(yōu)化方面來看,可以采用 CDN(內(nèi)容分發(fā)網(wǎng)絡(luò))加速技術(shù) 。CDN 通過在全球各地部署節(jié)點(diǎn)服務(wù)器,將文件緩存到離用戶更近的節(jié)點(diǎn)。當(dāng)用戶上傳文件時,請求會被路由到距離最近的 CDN 節(jié)點(diǎn),這樣可以大大減少網(wǎng)絡(luò)傳輸?shù)难舆t,提高上傳速度。例如,對于跨國用戶上傳文件的場景,CDN 可以有效地解決因網(wǎng)絡(luò)距離遠(yuǎn)而導(dǎo)致的上傳緩慢問題 。同時,合理調(diào)整上傳的并發(fā)數(shù)也能提升性能。在前端,可以根據(jù)用戶設(shè)備的性能和網(wǎng)絡(luò)狀況,動態(tài)調(diào)整并發(fā)上傳的分片數(shù)量。如果用戶設(shè)備性能較強(qiáng)且網(wǎng)絡(luò)穩(wěn)定,可以適當(dāng)增加并發(fā)數(shù),如將并發(fā)上傳的分片數(shù)從默認(rèn)的 5 個增加到 10 個,從而加快上傳速度;反之,則減少并發(fā)數(shù),避免因過多的并發(fā)請求導(dǎo)致網(wǎng)絡(luò)擁塞。
在服務(wù)器資源利用方面,合理配置服務(wù)器參數(shù)至關(guān)重要。對于 Java 應(yīng)用服務(wù)器(如 Tomcat),可以調(diào)整 JVM 堆大小。如果上傳的文件較大且并發(fā)上傳請求較多,適當(dāng)增加 JVM 堆大小,比如將堆大小從默認(rèn)的 512MB 增加到 1GB,能夠避免因內(nèi)存不足而導(dǎo)致的上傳失敗 。同時,優(yōu)化線程池配置也能提高服務(wù)器的并發(fā)處理能力。根據(jù)服務(wù)器的硬件配置和預(yù)計的上傳并發(fā)量,調(diào)整線程池的核心線程數(shù)、最大線程數(shù)和隊列容量。例如,將核心線程數(shù)設(shè)置為 10,最大線程數(shù)設(shè)置為 50,隊列容量設(shè)置為 100,這樣可以使服務(wù)器在高并發(fā)上傳場景下,更有效地處理上傳請求,避免線程資源的浪費(fèi)和請求的積壓。此外,采用異步處理機(jī)制,將文件上傳任務(wù)放入任務(wù)隊列中,由后臺線程異步處理,也能提高服務(wù)器的響應(yīng)速度,避免因同步處理上傳任務(wù)而導(dǎo)致服務(wù)器長時間阻塞,影響其他請求的處理。
異常處理
上傳過程中可能出現(xiàn)多種異常情況。網(wǎng)絡(luò)超時是比較常見的異常,當(dāng)網(wǎng)絡(luò)不穩(wěn)定或上傳時間過長時,就可能發(fā)生網(wǎng)絡(luò)超時。為了處理這種異常,在前端可以設(shè)置上傳請求的超時時間,比如設(shè)置為 30 秒。當(dāng)請求超時后,自動觸發(fā)重試機(jī)制,重新上傳該分片。在后端,也可以配置網(wǎng)絡(luò)連接的超時時間,并且當(dāng)接收到超時的上傳請求時,記錄相關(guān)日志,以便后續(xù)分析問題 。如果多次重試后仍然失敗,可以提示用戶檢查網(wǎng)絡(luò)連接,并提供一些常見的網(wǎng)絡(luò)問題排查建議。
文件損壞也是可能出現(xiàn)的異常情況。在前端,可以在上傳前對文件進(jìn)行完整性校驗,比如計算文件的 MD5 值或 SHA - 1 值。在后端,接收到分片文件后,再次計算分片的校驗值,并與前端傳來的校驗值進(jìn)行比對。如果校驗值不一致,說明文件在傳輸過程中可能損壞,此時可以返回錯誤信息給前端,告知用戶文件損壞,需要重新上傳 。同時,后端可以記錄損壞文件的相關(guān)信息,如文件名、文件大小、上傳時間等,以便進(jìn)一步分析文件損壞的原因,是網(wǎng)絡(luò)傳輸問題還是其他因素導(dǎo)致的。此外,對于服務(wù)器內(nèi)部錯誤,如磁盤空間不足、文件系統(tǒng)故障等,后端要捕獲相應(yīng)的異常,返回友好的錯誤提示給前端,并且在服務(wù)器端詳細(xì)記錄異常日志,包括異常類型、異常堆棧信息等,方便運(yùn)維人員快速定位和解決問題 。
以上就是SpringBoot中大文件分片上傳、斷點(diǎn)續(xù)傳與進(jìn)度條的實現(xiàn)全解析的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot大文件上傳的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springMVC 用戶登錄權(quán)限驗證實現(xiàn)過程解析
這篇文章主要介紹了springMVC 用戶登錄權(quán)限驗證實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
springboot使用DynamicDataSource動態(tài)切換數(shù)據(jù)源的實現(xiàn)過程
這篇文章主要給大家介紹了關(guān)于springboot使用DynamicDataSource動態(tài)切換數(shù)據(jù)源的實現(xiàn)過程,Spring Boot應(yīng)用中可以配置多個數(shù)據(jù)源,并根據(jù)注解靈活指定當(dāng)前使用的數(shù)據(jù)源,需要的朋友可以參考下2023-08-08
java調(diào)用chatgpt接口來實現(xiàn)專屬于自己的人工智能助手
這篇文章主要介紹了用java來調(diào)用chatget的接口,實現(xiàn)自己的聊天機(jī)器人,對人工智能感興趣的小伙伴可以參考閱讀2023-03-03
MyBatis Mapper.xml入?yún)ist使用in函數(shù)問題
文章主要講述了在使用MyBatis的Mapper.xml文件時,如何正確地在in函數(shù)中使用List作為入?yún)?作者強(qiáng)調(diào)了完整拷貝<if>...</if>格式的重要性,并指出稍微的改動就會導(dǎo)致錯誤2025-02-02
Java使用Apache?PDFBox進(jìn)行PDF轉(zhuǎn)圖片體積過大的優(yōu)化指南
在項目中,我們有一個接口用于將 PDF 按頁拆分為圖片,方便前端逐頁展示,上線一段時間后發(fā)現(xiàn)體積過大了,下面我們就來看看如何進(jìn)行簡單的優(yōu)化吧2026-03-03

