最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot中大文件分片上傳、斷點(diǎn)續(xù)傳與進(jìn)度條的實現(xiàn)全解析

 更新時間:2026年04月20日 09:34:53   作者:小碼哥_常  
在日常?Web?應(yīng)用開發(fā)中,文件上傳是一個極為常見的功能,這篇文章主要為大家詳細(xì)介紹了SpringBoot中大文件分片上傳、斷點(diǎn)續(xù)傳與進(jìn)度條的相關(guā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 提供的\&lt;progress\&gt;標(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)度。

除了使用原生的\&lt;progress\&gt;標(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;
};

在上述代碼中,后端通過FileUploadWebSocketHandlersendProgress方法將上傳進(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)文章

最新評論

莱芜市| 苏尼特左旗| 海丰县| 章丘市| 南木林县| 增城市| 广昌县| 竹北市| 临猗县| 潮州市| 江华| 锡林浩特市| 炉霍县| 图们市| 仁怀市| 陇川县| 朝阳区| 丰镇市| 承德市| 扎鲁特旗| 黎川县| 长丰县| 石景山区| 江西省| 垣曲县| 栖霞市| 湾仔区| 华阴市| 佳木斯市| 滁州市| 玛多县| 砚山县| 剑川县| 昌宁县| 绥芬河市| 泽普县| 颍上县| 崇阳县| 阜城县| 新乡县| 定西市|