大文件上传不再卡顿:Spring Boot 分片上传、断点续传与进度条实现全解析

大文件上传不再卡顿:Spring Boot 分片上传、断点续传与进度条实现全解析

一、引言

在日常 Web 应用开发中,文件上传是一个极为常见的功能。无论是用户头像的上传、文档资料的提交,还是图片、视频等多媒体文件的传输,文件上传功能都扮演着关键角色。然而,当涉及到大文件上传时,传统的单次上传方式往往会暴露出诸多问题。

大文件上传时,若遭遇网络不稳定的情况,如网络突然中断或波动,整个文件就需要重新上传,这不仅浪费用户的时间和流量,还会导致用户体验急剧下降。服务器也会对上传文件的大小有所限制,一旦文件超出限制,上传便会失败。同时,一次性接收整个大文件可能会使服务端内存不足,出现内存溢出的情况。而且,在长时间的上传过程中,若无法展示上传进度条,用户只能在无尽的等待中猜测上传状态,这很容易让用户产生焦虑情绪,甚至可能导致用户误操作中断上传。

为了解决这些问题,分片上传、断点续传与进度条展示等技术应运而生。而 Spring Boot 作为当下流行的 Java 开发框架,提供了丰富的功能和便捷的工具,能够帮助我们高效地实现这些功能。接下来,就让我们深入探讨如何使用 Spring Boot 实现分片上传、断点续传与进度条展示。

二、技术原理介绍

(一)分片上传

分片上传,即将一个大文件按照固定的大小分割成多个较小的分片(也叫块),然后分别对这些分片进行上传 。每个分片都有一个唯一的标识,通常可以通过文件的唯一标识(如文件的 MD5 值)与分片的序号来确定。在前端,通过 JavaScript 的 File API 可以方便地实现文件的分片操作。例如,使用file\.slice\(\)方法按照设定的分片大小(如 5MB)对文件进行切割。

在后端,Spring Boot 接收到每个分片后,会根据文件唯一标识与分片序号,将分片临时存储在服务器的指定目录中。当所有分片都上传完成后,后端会按照分片序号的顺序,将这些分片依次读取并合并成最终的完整文件。这样做的好处是,即使在上传过程中某个分片上传失败,也只需重新上传该分片,而无需重新上传整个文件,大大提高了上传的稳定性和效率。

(二)断点续传

断点续传是建立在分片上传基础之上的一项技术。当文件上传过程中由于网络中断、服务器故障等原因导致上传中断时,客户端会记录下已经上传成功的分片信息,比如已经上传的分片序号。当用户再次尝试上传该文件时,客户端会根据之前记录的信息,从上次中断的位置开始,继续上传剩余的分片。

在服务端,需要保存已上传分片的状态信息。可以通过在数据库中记录每个文件的上传进度,或者使用缓存(如 Redis)来存储已上传的分片列表等方式实现。当客户端发起续传请求时,服务端会根据这些记录,判断哪些分片已经上传成功,哪些还需要上传,从而实现断点续传的功能。这样就避免了用户因为上传中断而需要重新上传整个文件的烦恼,极大地提升了用户体验。

(三)进度条

进度条的实现原理是通过监听文件上传过程中的数据传输量,实时计算并展示上传进度。在前端,当使用 XMLHttpRequest 或 Fetch API 进行文件上传时,可以监听其progress事件。这个事件会在上传过程中不断触发,通过事件对象可以获取到已经上传的字节数和文件的总字节数。根据这两个数据,就可以计算出上传的进度百分比,即已上传字节数 / 文件总字节数 \* 100%

然后,将这个进度百分比更新到前端页面的进度条元素上,比如通过修改\<progress\>标签的value属性,或者使用 CSS 动画来模拟进度条的增长,从而让用户直观地了解文件上传的进度情况。在后端,Spring Boot 可以配合前端,通过一些机制(如 WebSocket、SSE 等)将上传进度实时推送给前端,确保前端展示的进度与后端实际的上传进度保持一致 。

三、Spring Boot 实现分片上传

(一)项目搭建

首先,我们使用 Spring Initializr(https://start.spring.io/ )来创建一个 Spring Boot 项目。在这个网页中,我们可以对项目进行多项配置,比如选择项目的构建工具(Maven 或 Gradle),指定 Spring Boot 的版本,以及添加所需的依赖。这里,我们主要添加spring\-boot\-starter\-web依赖,它提供了构建 Web 应用所需的核心组件,让我们能够方便地创建 HTTP 接口 。同时,为了处理文件上传,Spring Boot 默认已经集成了相关的依赖,无需额外引入。

如果使用 Maven 构建项目,在pom\.xml文件中,添加如下依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

如果是 Gradle 项目,在build\.gradle文件中添加:

groovy 复制代码
implementation 'org.springframework.boot:spring-boot-starter-web'

创建好项目后,将其导入到我们喜欢的集成开发环境(IDE)中,比如 IntelliJ IDEA 或 Eclipse,这样就可以开始后续的开发工作了。

(二)前端实现

前端我们可以使用原生 JavaScript 结合fetch API 来实现文件分片。下面是一个简单的示例代码:

html 复制代码
<!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;

        // 配置参数
        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 = new Set();

        // 并行上传控制(最大5并发)
        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函数负责处理文件分片和上传的逻辑。首先获取用户选择的文件,然后根据设定的CHUNK\_SIZE(这里是 5MB)计算总分片数TOTAL\_CHUNKS,并生成一个唯一的FILE\_ID。通过一个for循环,对文件进行分片,每个分片通过uploadChunk函数上传到后端。uploadChunk函数将分片数据、分片序号、总分片数和文件唯一标识等信息通过FormData对象发送到/api/upload/chunk接口。updateProgress函数用于更新上传进度条,根据已上传的分片数和总分片数计算进度百分比,并更新页面上进度条的宽度和显示文本 。当所有分片上传完成后,通过completeUpload函数向/api/upload/merge接口发送合并请求。

(三)后端实现

后端在 Spring Boot 中创建接收分片的接口,并将分片保存到临时目录。首先创建一个控制器类FileUploadController

java 复制代码
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 {
            // 创建临时文件存储目录
            Path tempDirPath = Paths.get(TEMP_DIR, fileId);
            Files.createDirectories(tempDirPath);

            // 构建分片文件路径
            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 {
            // 构建临时文件存储目录
            Path tempDirPath = Paths.get(TEMP_DIR, fileId);

            // 构建最终合并文件路径
            Path mergedFilePath = Paths.get(TEMP_DIR, filename);

            // 使用NIO进行文件合并
            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方法用于处理前端上传的分片。它接收前端传来的分片文件、分片序号、总分片数和文件唯一标识。首先创建以fileId命名的临时目录,然后在该目录下根据分片序号构建分片文件路径,最后将接收到的分片文件保存到该路径。mergeChunks方法用于处理文件合并。它接收文件唯一标识和文件名,先构建临时目录和最终合并文件的路径。通过Files\.walk方法遍历临时目录下的所有分片文件,按照分片序号进行排序,然后依次将分片文件内容追加到最终合并文件中,并在合并完成后删除临时分片文件和临时目录 。如果在保存分片或合并文件过程中出现IOException异常,则返回相应的失败信息。

四、Spring Boot 实现断点续传

(一)前端实现

在前端,我们需要记录上传进度,以便在上传中断后能够准确地知道已经上传了哪些分片 。可以通过在本地存储(如localStorage)中记录已上传的分片序号来实现这一功能。当上传中断后,再次点击上传按钮时,前端代码会读取本地存储中的已上传分片信息。

javascript 复制代码
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函数在开始上传前,先从localStorage中读取已上传的分片信息。如果某个分片已经上传过(通过uploadedChunks\.has\(chunkIndex\)判断),则跳过该分片的上传。当一个分片上传成功后,将其序号添加到uploadedChunks集合中,并更新localStorage中的记录 。这样,即使上传过程中出现中断,再次上传时也能从上次中断的位置继续。

(二)后端实现

后端需要提供一个接口,用于查询某个文件已经上传的分片。在FileUploadController中添加如下方法:

java 复制代码
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作为参数,根据fileId构建临时目录路径。如果该目录不存在,则返回一个空列表,表示没有上传任何分片。如果目录存在,则遍历目录下的所有文件,通过文件名解析出分片序号,并将其添加到uploadedChunks列表中,最后返回该列表 。前端在上传前,可以先调用这个接口,获取已上传的分片信息,从而确定需要上传哪些分片,实现断点续传的功能。

五、Spring Boot 实现进度条

(一)前端实现

在前端展示进度条,我们可以使用 HTML5 提供的\&lt;progress\&gt;标签。这个标签提供了一种简单且语义化的方式来显示任务的完成进度。它有两个主要属性:value表示当前已完成的值,max表示任务总值 。浏览器会根据这两个值自动计算并显示进度百分比。

html 复制代码
<progress id="uploadProgress" value="0" max="100"></progress>

然后,通过 JavaScript 监听文件上传的progress事件来动态更新value属性,从而实现进度条的实时更新。结合前面分片上传的前端代码,在uploadChunk函数中,我们可以进一步完善对进度条的更新逻辑:

javascript 复制代码
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);

    // 这里可以添加取消上传的逻辑,比如在某个条件下调用 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事件处理函数会在上传过程中不断被触发。event\.lengthComputable用于判断是否可以计算上传进度,如果可以,则通过event\.loaded(已上传的字节数)和event\.total(文件总字节数)计算出上传的百分比,并更新uploadProgress进度条的value属性 。这样,用户就能在前端直观地看到每个分片的上传进度。

除了使用原生的\&lt;progress\&gt;标签,我们也可以使用一些第三方 UI 组件库来实现更美观、功能更丰富的进度条效果,比如 Element - UI(适用于 Vue 项目)、Ant Design(适用于 React 项目)等 。以 Element - UI 为例,在 Vue 项目中使用其进度条组件的代码如下:

html 复制代码
<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 提供的进度条组件,percentage属性绑定了progress数据,通过在上传过程中更新progress的值,实现进度条的动态展示 。status=\&\#34;active\&\#34;可以让进度条显示为动态加载的效果,增强用户体验。

(二)后端实现

后端将上传进度数据传递给前端,有两种常见的方式:WebSocket 实时推送和接口轮询获取。

WebSocket 实时推送 :使用 WebSocket 可以实现服务器与客户端之间的全双工通信,服务器能够主动将上传进度推送给客户端。首先,在 Spring Boot 项目中添加 WebSocket 依赖,如果使用 Maven,在pom\.xml中添加:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

然后创建一个 WebSocket 配置类WebSocketConfig

java 复制代码
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();
    }
}

接下来创建一个 WebSocket 处理器类FileUploadWebSocketHandler

java 复制代码
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 {
        // 处理接收到的消息,这里暂不处理前端发来的消息
    }

    @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中,当处理分片上传时,计算上传进度并通过 WebSocket 推送:

java 复制代码
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 {
            // 创建临时文件存储目录
            Path tempDirPath = Paths.get(TEMP_DIR, fileId);
            Files.createDirectories(tempDirPath);

            // 构建分片文件路径
            Path chunkFilePath = tempDirPath.resolve(chunkIndex + ".part");

            // 保存分片文件
            file.transferTo(chunkFilePath.toFile());

            // 计算上传进度
            int progress = (int) ((chunkIndex + 1) * 100 / totalChunks);
            fileUploadWebSocketHandler.sendProgress(sessionId, progress);

            return "分片上传成功";
        } catch (IOException e) {
            e.printStackTrace();
            return "分片上传失败";
        }
    }

    // 其他方法...
}

在前端,建立 WebSocket 连接并监听进度消息:

javascript 复制代码
const socket = new WebSocket('ws://localhost:8080/websocket');
socket.onmessage = function (event) {
    const progress = parseInt(event.data);
    document.getElementById('uploadProgress').value = progress;
};

在上述代码中,后端通过FileUploadWebSocketHandlersendProgress方法将上传进度发送给对应的 WebSocket 会话。前端建立 WebSocket 连接后,通过onmessage事件接收并更新进度条 。

接口轮询获取 :接口轮询的方式是前端定时向后端发送请求,获取最新的上传进度。在后端FileUploadController中添加一个获取进度的接口:

java 复制代码
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 {
            // 保存分片逻辑...

            // 计算上传进度
            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定时调用接口获取进度并更新进度条:

javascript 复制代码
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接口发送请求,获取文件的上传进度,并更新进度条。这种方式实现相对简单,但相比 WebSocket 实时推送,会增加一定的网络开销,并且进度更新可能不够及时 。

六、整合与优化

(一)整合步骤

将分片上传、断点续传和进度条功能整合在一起时,首先要确保前端和后端的交互逻辑清晰且一致 。在前端,需要统一文件选择、上传操作的触发逻辑。例如,在一个文件上传组件中,通过一个按钮来触发文件选择对话框,当用户选择文件后,自动触发分片上传流程。在这个流程中,既要包含分片上传的逻辑,如按固定大小对文件进行分片、生成唯一的文件标识等,也要融入断点续传的逻辑,即从本地存储或后端获取已上传的分片信息,跳过已上传的分片 。同时,实时更新进度条的显示,将每个分片的上传进度及时反馈给用户。

在后端,各个功能对应的接口要协同工作。接收分片上传的接口不仅要保存分片文件,还要记录上传进度,以便为进度条功能提供数据支持 。查询已上传分片的接口要与断点续传功能紧密配合,准确地返回已上传的分片列表,让前端能够根据这个列表确定需要上传的剩余分片。文件合并的接口要在所有分片上传完成后,正确地将分片合并成完整文件,并处理好合并过程中的异常情况 。在整合过程中,要注意数据的一致性和准确性,比如文件唯一标识在前端和后端的传递和使用要保持一致,避免出现因标识不一致而导致的上传错误。

(二)性能优化

从网络优化方面来看,可以采用 CDN(内容分发网络)加速技术 。CDN 通过在全球各地部署节点服务器,将文件缓存到离用户更近的节点。当用户上传文件时,请求会被路由到距离最近的 CDN 节点,这样可以大大减少网络传输的延迟,提高上传速度。例如,对于跨国用户上传文件的场景,CDN 可以有效地解决因网络距离远而导致的上传缓慢问题 。同时,合理调整上传的并发数也能提升性能。在前端,可以根据用户设备的性能和网络状况,动态调整并发上传的分片数量。如果用户设备性能较强且网络稳定,可以适当增加并发数,如将并发上传的分片数从默认的 5 个增加到 10 个,从而加快上传速度;反之,则减少并发数,避免因过多的并发请求导致网络拥塞。

在服务器资源利用方面,合理配置服务器参数至关重要。对于 Java 应用服务器(如 Tomcat),可以调整 JVM 堆大小。如果上传的文件较大且并发上传请求较多,适当增加 JVM 堆大小,比如将堆大小从默认的 512MB 增加到 1GB,能够避免因内存不足而导致的上传失败 。同时,优化线程池配置也能提高服务器的并发处理能力。根据服务器的硬件配置和预计的上传并发量,调整线程池的核心线程数、最大线程数和队列容量。例如,将核心线程数设置为 10,最大线程数设置为 50,队列容量设置为 100,这样可以使服务器在高并发上传场景下,更有效地处理上传请求,避免线程资源的浪费和请求的积压。此外,采用异步处理机制,将文件上传任务放入任务队列中,由后台线程异步处理,也能提高服务器的响应速度,避免因同步处理上传任务而导致服务器长时间阻塞,影响其他请求的处理。

(三)异常处理

上传过程中可能出现多种异常情况。网络超时是比较常见的异常,当网络不稳定或上传时间过长时,就可能发生网络超时。为了处理这种异常,在前端可以设置上传请求的超时时间,比如设置为 30 秒。当请求超时后,自动触发重试机制,重新上传该分片。在后端,也可以配置网络连接的超时时间,并且当接收到超时的上传请求时,记录相关日志,以便后续分析问题 。如果多次重试后仍然失败,可以提示用户检查网络连接,并提供一些常见的网络问题排查建议。

文件损坏也是可能出现的异常情况。在前端,可以在上传前对文件进行完整性校验,比如计算文件的 MD5 值或 SHA - 1 值。在后端,接收到分片文件后,再次计算分片的校验值,并与前端传来的校验值进行比对。如果校验值不一致,说明文件在传输过程中可能损坏,此时可以返回错误信息给前端,告知用户文件损坏,需要重新上传 。同时,后端可以记录损坏文件的相关信息,如文件名、文件大小、上传时间等,以便进一步分析文件损坏的原因,是网络传输问题还是其他因素导致的。此外,对于服务器内部错误,如磁盘空间不足、文件系统故障等,后端要捕获相应的异常,返回友好的错误提示给前端,并且在服务器端详细记录异常日志,包括异常类型、异常堆栈信息等,方便运维人员快速定位和解决问题 。

相关推荐
念何架构之路2 小时前
图解常见网络I/O复用模型
服务器·网络·php
用户79140679683932 小时前
MySQL的索引类型
后端
@insist1232 小时前
网络工程师-实战配置篇(一):深入 BGP 与 VRRP,构建高可靠网络
服务器·网络·php·网络工程师·软件水平考试
楼田莉子2 小时前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式
geNE GENT2 小时前
Spring Boot管理用户数据
java·spring boot·后端
怒放吧德德2 小时前
Spring Boot实战:Event事件机制解析与实战
java·spring boot·后端
梦无矶3 小时前
快速设置uv默认源为国内镜像
数据库·redis·后端·python·uv
㳺三才人子3 小时前
SpringDoc OpenAPI 配置問題
服务器·spring boot
yoyo_zzm3 小时前
SpringBoot Test详解
spring boot·后端·log4j