Java 实现大文件上传与断点续传:原理、实践与优化

在现代 Web 应用中,用户经常需要上传大型文件------如高清视频、设计图纸或数据库备份。然而,受限于网络稳定性、服务器内存和浏览器限制,直接上传大文件极易失败。为此,"分片上传 + 断点续传"成为业界标准解决方案。本文将深入讲解如何使用 Java(基于 Spring Boot) 实现高效、可靠的大文件上传系统。


一、为什么需要断点续传?

  • 网络不稳定:上传过程中断后,无需从头开始。
  • 节省带宽与时间:仅重传失败或未传的分片。
  • 提升用户体验:支持上传进度显示、暂停/恢复。
  • 避免服务器压力:避免一次性加载整个大文件到内存。

二、核心设计思想

1. 文件分片(Chunking)

将一个大文件按固定大小(如 2MB)切分为多个小块(chunks),每个块独立上传。

2. 唯一标识

使用文件内容的 MD5 值 作为唯一 ID,既可识别文件,又能避免重复上传相同内容。

3. 分片管理

  • 后端为每个文件创建临时目录,按序号存储分片。
  • 提供接口查询"已上传分片列表",实现断点检测。

4. 合并与清理

所有分片收齐后,按顺序合并成完整文件,并清理临时数据。


三、后端实现(Spring Boot)

1. 项目依赖(Maven)

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
</dependencies>

2. 控制器代码

java 复制代码
@RestController
@RequestMapping("/api/upload")
public class ResumableUploadController {

    private static final String TEMP_DIR = "uploads/temp/";
    private static final String FINAL_DIR = "uploads/final/";

    /**
     * 检查哪些分片已上传(用于断点续传)
     */
    @GetMapping("/check")
    public Set<Integer> checkUploadedChunks(@RequestParam String fileMd5) {
        Set<Integer> uploaded = new HashSet<>();
        Path tempPath = Paths.get(TEMP_DIR, fileMd5);
        if (Files.exists(tempPath)) {
            try (Stream<Path> stream = Files.list(tempPath)) {
                stream.forEach(p -> {
                    String name = p.getFileName().toString();
                    if (name.matches("\\d+")) {
                        uploaded.add(Integer.parseInt(name));
                    }
                });
            } catch (IOException e) {
                // 日志记录
            }
        }
        return uploaded;
    }

    /**
     * 上传单个分片
     */
    @PostMapping("/chunk")
    public ResponseEntity<?> uploadChunk(
            @RequestParam String fileMd5,
            @RequestParam int chunkIndex,
            @RequestParam MultipartFile chunk) {

        try {
            Path tempDir = Paths.get(TEMP_DIR, fileMd5);
            Files.createDirectories(tempDir);
            Path chunkFile = tempDir.resolve(String.valueOf(chunkIndex));
            chunk.transferTo(chunkFile.toFile());
            return ResponseEntity.ok().build();
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 合并所有分片为完整文件
     */
    @PostMapping("/merge")
    public ResponseEntity<?> mergeChunks(
            @RequestParam String fileMd5,
            @RequestParam String fileName,
            @RequestParam int totalChunks) {

        try {
            Path finalPath = Paths.get(FINAL_DIR, fileName);
            Files.createDirectories(finalPath.getParent());

            try (OutputStream out = Files.newOutputStream(finalPath)) {
                for (int i = 0; i < totalChunks; i++) {
                    Path chunk = Paths.get(TEMP_DIR, fileMd5, String.valueOf(i));
                    if (!Files.exists(chunk)) {
                        return ResponseEntity.badRequest()
                                .body("Missing chunk: " + i);
                    }
                    byte[] data = Files.readAllBytes(chunk);
                    out.write(data);
                }
            }

            // 清理临时分片
            FileUtils.deleteDirectory(new File(TEMP_DIR + fileMd5));

            return ResponseEntity.ok(Map.of("success", true, "path", finalPath.toString()));
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

注意:实际项目中应加入文件名校验、权限控制、异常日志等。


四、前端配合逻辑(简要)

前端需完成以下步骤:

  1. 计算文件 MD5 (推荐使用 spark-md5 库)。
  2. 调用 /check 接口 获取已上传分片索引。
  3. 循环上传未完成的分片 ,每个请求携带:
    • fileMd5
    • chunkIndex
    • 分片 Blob 数据
  4. 全部上传完成后,调用 /merge 触发合并
javascript 复制代码
// 示例:上传一个分片
const uploadChunk = async (fileMd5, index, blob) => {
  const formData = new FormData();
  formData.append('fileMd5', fileMd5);
  formData.append('chunkIndex', index);
  formData.append('chunk', blob);
  await fetch('/api/upload/chunk', { method: 'POST', body: formData });
};

五、关键优化建议

优化点 说明
MD5 计算 前端计算可避免重复上传;若安全要求高,后端也可二次校验。
并发上传 允许同时上传多个分片(如 3~5 个),提升速度。
超时清理 定时任务删除 24 小时未合并的临时分片,防止磁盘占满。
限流与鉴权 防止恶意上传,限制单用户上传频率和总大小。
进度反馈 前端根据已传分片数实时更新进度条。

六、进阶方案

1. 使用 TUS 协议

TUS 是一个开源的 Resumable Upload 标准。Java 社区有 tus-java-server 实现,开箱即用,支持跨平台、跨语言。

2. 对接云存储

阿里云 OSS、腾讯云 COS、AWS S3 等均提供 分片上传 API。可将分片直接上传至云存储,后端仅负责协调,大幅降低服务器负载。


七、总结

通过"分片上传 + 断点续传",我们不仅能可靠地处理 GB 级文件,还能显著提升用户体验和系统健壮性。虽然实现细节较多,但核心逻辑清晰:切分 → 上传 → 校验 → 合并

在实际项目中,建议结合业务需求选择自研或集成成熟方案。对于高并发、高可靠场景,优先考虑云厂商的分片上传能力;对于内部系统或定制化需求,本文提供的 Java 实现可作为坚实基础。

源码参考 :完整示例项目可参考 GitHub 上的 spring-boot-resumable-upload 开源模板。

相关推荐
Kratzdisteln2 小时前
【1902】自适应学习系统 - 完整技术方案
java·python·学习
橘橙黄又青2 小时前
Spring篇
java·后端·spring
JaredYe2 小时前
node-plantuml-2:革命性的纯Node.js PlantUML渲染器,告别Java依赖!
java·开发语言·node.js·uml·plantuml·jre
hhzz2 小时前
Springboot项目中使用EasyPOI方式导出合同word文档
java·spring boot·后端·word·poi·easypoi
派大鑫wink2 小时前
【Day38】Spring 框架入门:IOC 容器与 DI 依赖注入
java·开发语言·html
爱丽_2 小时前
Spring Bean 管理与依赖注入实践
java·后端·spring
独自破碎E2 小时前
什么是Spring Bean?
java·后端·spring
人道领域2 小时前
JavaWeb从入门到进阶(Maven的安装和在idea中的构建)
java·maven
XXOOXRT2 小时前
基于SpringBoot的留言板
java·spring boot·后端