> 技术文档 > 前端大文件分片上传详解 - Spring Boot 后端接口实现_前端分片上传

前端大文件分片上传详解 - Spring Boot 后端接口实现_前端分片上传

在这里插入图片描述

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
🌞《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

前端大文件分片上传详解:Spring Boot 后端接口实现

  • 1. 前言
  • 2. 为什么要分片
  • 3. 实现思路与流程
  • 4. 完整实现方案
      • ❶ 前端分片逻辑实现
      • ❷ SpringBoot后端实现
      • ❸ 扩展断点续传
  • 5. 高级优化方案
      • 5.1 分片秒传优化
      • 5.2 并行合并加速
      • 5.3 安全增强措施
  • 6. 结语:构建可靠的大文件传输体系

1. 前言

在很多 Web 应用场景下,我们需要上传体积很大的文件(视频、镜像包、数据包等)。一次性将整个文件上传往往会面临以下问题:

  1. 网络不稳定时容易中断:导致上传失败,需要重头再来
  2. 服务器内存/磁盘压力大:一次性接收大文件可能瞬间占满带宽或写满临时目录
  3. 用户体验差:上传过程中无法做到断点续传或重试

为了解决上述问题,分片上传(Chunked Upload)应运而生。它将大文件拆分成一个个小块,按序上传并在后台合并,既可以实现断点续传,也能平滑流量、降低服务器压力。

本文博主将带着小伙伴了解如何基于 前端原生 JavaScript + Spring Boot 实现大文件分片上传。


2. 为什么要分片

在这里插入图片描述

  1. 断点续传
    每个分片上传完成后都会得到确认,下次重试只需上传未成功的分片,用户体验更佳。

  2. 可控并发
    前端可以设置并发上传的分片数量(比如同时 3~5 个),既能提高吞吐量,又不至于瞬时压垮网络或服务器。

  3. 流量均衡
    小块数据平滑地传输,避免一次性大流量冲击。

  4. 兼容性与安全
    后端可对每个分片做校验(大小、哈希、格式等),在合并前即可过滤非法内容。

分片上传的核心优势

痛点 分片方案 收益 超时中断 小片独立上传 避免整体失败 内存压力 单片流式处理 内存占用<10MB 网络波动 失败分片重试 带宽利用率提升40%+ 大文件传输 并行上传机制 速度提升3-5倍 意外中断 断点续传支持 节省90%重复流量

3. 实现思路与流程

  1. 前端

    用户选中文件后,按固定大小(如 1MB)切片
    依次(或并发)将每个分片通过 fetch/XMLHttpRequest 上传到后端;
    上传完所有分片后,通知后端开始合并;

  2. 后端(Spring Boot)

    接收每个分片时,根据文件唯一标识(如 MD5)分片序号,保存到临时目录;
    接收 “合并请求” 时,按序读取所有分片并写入最终文件;
    合并完成后,可删除临时分片,返回成功。


4. 完整实现方案

❶ 前端分片逻辑实现

首先我们编写前端的分片、上传逻辑

<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(\'上传失败\'); return res.json(); });}async function completeUpload(fileId, filename) { return fetch(\'/api/upload/merge\', { method: \'POST\', headers: { \'Content-Type\': \'application/json\' }, body: JSON.stringify({ fileId, filename }) }).then(res => { if (res.ok) alert(\'上传成功!\'); else alert(\'合并失败\'); });}function updateProgress(done, total) { const percent = Math.round((done / total) * 100); document.getElementById(\'progressBar\').innerHTML = ` <div style=\"width: ${percent}%; background: #4CAF50; height: 20px;\"> ${percent}% 
`;}</script>

❷ SpringBoot后端实现

首先配置一下SpringBoot 上传的一些限制

# application.ymlspring: servlet: multipart: max-file-size: 10MB # 单片最大尺寸 max-request-size: 1000MB # 总请求限制file: upload-dir: /data/upload

分片上传控制器Controller

@RestController@RequestMapping(\"/api/upload\")public class FileUploadController { @Value(\"${file.upload-dir}\") // private String uploadDir; // 分片上传接口 @PostMapping(\"/chunk\") public ResponseEntity<?> uploadChunk( @RequestParam(\"file\") MultipartFile file, @RequestParam(\"chunkIndex\") int chunkIndex, @RequestParam(\"totalChunks\") int totalChunks, @RequestParam(\"fileId\") String fileId) { try { // 创建分片存储目录 String chunkDir = uploadDir + \"/chunks/\" + fileId; Path dirPath = Paths.get(chunkDir); if (!Files.exists(dirPath)) { Files.createDirectories(dirPath); } // 保存分片文件 String chunkFilename = chunkIndex + \".part\"; Path filePath = dirPath.resolve(chunkFilename); Files.copy(file.getInputStream(), filePath,  StandardCopyOption.REPLACE_EXISTING); return ResponseEntity.ok().body(Map.of( \"status\", \"success\", \"chunk\", chunkIndex )); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of( \"status\", \"error\", \"message\", e.getMessage() )); } } // 合并文件接口 @PostMapping(\"/merge\") public ResponseEntity<?> mergeChunks( @RequestBody MergeRequest request) { try { String fileId = request.getFileId(); String filename = request.getFilename(); Path chunkDir = Paths.get(uploadDir, \"chunks\", fileId); Path outputFile = Paths.get(uploadDir, filename); // 检查分片完整性 long expectedChunks = Files.list(chunkDir).count(); if (expectedChunks != request.getTotalChunks()) { return ResponseEntity.badRequest().body(  \"分片数量不匹配\"); } // 按序号排序分片 List<Path> chunks = Files.list(chunkDir) .sorted((p1, p2) -> {  String f1 = p1.getFileName().toString();  String f2 = p2.getFileName().toString();  return Integer.compare( Integer.parseInt(f1.split(\"\\\\.\")[0]), Integer.parseInt(f2.split(\"\\\\.\")[0])); }) .collect(Collectors.toList()); // 合并文件 try (OutputStream out = Files.newOutputStream(outputFile,  StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { for (Path chunk : chunks) {  Files.copy(chunk, out); } } // 清理分片目录 FileUtils.deleteDirectory(chunkDir.toFile()); return ResponseEntity.ok().body(Map.of( \"status\", \"success\", \"file\", filename, \"size\", Files.size(outputFile) )); } catch (Exception e) { return ResponseEntity.status(500).body( \"合并失败: \" + e.getMessage()); } } // 请求体定义 @Data public static class MergeRequest { private String fileId; private String filename; private int totalChunks; }}

❸ 扩展断点续传

如果你的项目没有断点续传的需求,可以直接参考 ❶ ❷前后端代码即可,否则可以在分片上传接口中添加续传支持,增加代码如下:

// 在分片上传接口中添加续传支持@GetMapping(\"/check\")public ResponseEntity<?> checkChunks( @RequestParam(\"fileId\") String fileId, @RequestParam(\"totalChunks\") int totalChunks) { Path chunkDir = Paths.get(uploadDir, \"chunks\", fileId); if (!Files.exists(chunkDir)) { return ResponseEntity.ok().body(Map.of( \"exists\", false )); } try { // 获取已上传分片索引 Set<Integer> uploaded = Files.list(chunkDir) .map(p -> Integer.parseInt( p.getFileName().toString().split(\"\\\\.\")[0])) .collect(Collectors.toSet()); return ResponseEntity.ok().body(Map.of( \"exists\", true, \"uploaded\", uploaded )); } catch (IOException e) { return ResponseEntity.status(500).body( \"检查失败: \" + e.getMessage()); }}

前端调用检查接口:

async function checkUploadStatus(fileId, totalChunks) { const res = await fetch(`/api/upload/check?fileId=${fileId}&totalChunks=${totalChunks}`); const data = await res.json(); return data.exists ? data.uploaded : new Set();}// 在上述前端代码 startUpload函数中加入const uploadedChunks = await checkUploadStatus(FILE_ID, TOTAL_CHUNKS);

5. 高级优化方案

通过上面的代码示例,你已经可以轻松使用大文件的分片上传了,如果你还有一些优化需求,博主这里简单罗列三个,供小伙伴们参考

5.1 分片秒传优化

// 在保存分片前计算哈希String hash = DigestUtils.md5DigestAsHex(file.getBytes());String chunkFilename = hash + \".part\"; // 哈希作为文件名// 检查是否已存在相同分片if (Files.exists(dirPath.resolve(chunkFilename))) { return ResponseEntity.ok().body(Map.of( \"status\", \"skip\", \"chunk\", chunkIndex ));}

5.2 并行合并加速

// 使用并行流合并文件List<Path> chunks = ... // 排序后的分片列表try (OutputStream out = Files.newOutputStream(outputFile)) { chunks.parallelStream().forEach(chunk -> { try { Files.copy(chunk, out); } catch (IOException e) { throw new UncheckedIOException(e); } });}

5.3 安全增强措施

// 文件名安全过滤String safeFilename = filename.replaceAll(\"[^a-zA-Z0-9\\\\.\\\\-]\", \"_\");// 文件类型检查String mimeType = Files.probeContentType(filePath);if (!mimeType.startsWith(\"video/\")) { throw new SecurityException(\"非法文件类型\");}

6. 结语:构建可靠的大文件传输体系

本文示例演示了一个从前端分片、并发上传,到后端按序存储与合并的完整流程。并可以按需提供断点续传,以及部分优化的方案参考,这样我们就提高大文件上传的稳定性与用户体验。

通过本文实现的分片上传方案,我们成功解决了大文件传输的核心挑战:
稳定性提升:分片机制有效规避了网络波动影响
资源优化:内存占用从GB级降至MB级
用户体验:进度可视化 + 断点续传
扩展能力:秒传、并行合并等优化空间

希望这篇文章能帮助你快速上手大文件分片上传,如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!


前端技术专栏回顾:

01【前端技术】 ES6 介绍及常用语法说明
02【前端技术】标签页通讯localStorage、BroadcastChannel、SharedWorker的技术详解
03 前端请求乱序问题分析与AbortController、async/await、Promise.all等解决方案
04 前端开发中深拷贝的循环引用问题:从问题复现到完美解决
05 前端AJAX请求上传下载进度监控指南详解与完整代码示例
06 TypeScript 进阶指南 - 使用泛型与keyof约束参数
07 前端实现视频文件动画帧图片提取全攻略 - 附完整代码样例
08 前端函数防抖(Debounce)完整讲解 - 从原理、应用到完整实现
09 JavaScript异步编程 Async/Await 使用详解:从原理到最佳实践
10 前端图片裁剪上传全流程详解:从预览到上传的完整流程
在这里插入图片描述