后端开发中,大文件上传是高频需求(如视频、压缩包、日志文件),但直接上传易出现超时、断连、内存溢出等问题,甚至导致服务崩溃。本文结合2026年最新技术选型,手把手教你实现Spring Boot大文件上传的分片+秒传方案,兼顾性能与稳定性,新手也能直接落地。
大文件上传
传统大文件上传(单文件直接提交)的核心痛点的是:HTTP请求超时(文件超过100MB后,传输耗时远超默认超时时间)、服务器内存溢出(Spring Boot接收文件时,会先将文件缓存到内存,大文件直接占满堆内存)、断点无法续传(网络中断后需重新上传,浪费带宽)。
目前行业主流解决方案是「分片上传+秒传+断点续传」结合,适配Spring Boot 3.x版本(兼容2.7.x),核心优势的是:

- 降低内存压力:将大文件分割为固定大小的分片(如10MB/片),分片单独上传,服务器逐片处理,无需缓存完整文件。
- 避免超时断连:分片体积小,传输耗时短,配合超时重试机制,大幅降低断连概率。
- 节省带宽成本:秒传功能通过文件唯一标识(MD5)判断文件是否已存在,已存在则直接返回上传成功,无需重复传输。
- 提升用户体验:断点续传可记录已上传分片,网络中断后恢复上传时,仅上传未完成分片。
适配场景:后端接口开发、管理系统文件上传、用户头像/附件上传(支持GB级文件),兼容主流浏览器和Postman调用。
底层原理剖析
整个方案的底层逻辑围绕「分片拆分-分片传输-分片校验-合并归档」四大步骤,配合秒传和断点续传的辅助逻辑,具体原理如下:
1. 分片上传原理
前端将大文件通过File API分割为N个固定大小的分片(分片大小可配置,推荐10-50MB,兼顾传输效率和请求次数),每个分片携带3个核心参数:文件唯一标识(fileMd5,整个文件的MD5值)、分片索引(chunkIndex,从0开始计数)、总分片数(chunkTotal)。
后端接收分片后,先校验分片的合法性(分片大小、索引范围、MD5一致性),校验通过后,将分片临时存储到服务器本地目录(或OSS对象存储),并记录分片上传状态(已上传/未上传)。
2. 秒传原理
秒传的核心是「文件唯一标识校验」:前端在上传文件前,先计算整个文件的MD5值(通过前端JS库计算,避免后端重复计算),并调用后端「预上传接口」,传入fileMd5。
后端查询数据库(或缓存),判断该fileMd5对应的文件是否已存在(是否已完成分片合并):若存在,直接返回「秒传成功」;若不存在,返回「可上传」,并返回已上传的分片索引(用于断点续传)。
3. 断点续传原理
基于分片上传的状态记录:后端通过「fileMd5+chunkIndex」作为唯一键,记录每个分片的上传状态(可存储在Redis或数据库,推荐Redis,性能更高)。
前端在恢复上传时,先调用「预上传接口」,获取已上传的分片索引,然后仅上传未完成的分片,避免重复上传已成功的分片,实现断点续传。
4. 分片合并原理
当后端接收完所有分片(通过chunkIndex和chunkTotal判断),前端调用「分片合并接口」,后端根据fileMd5找到所有临时分片,按分片索引顺序合并为完整文件,合并完成后删除临时分片,更新文件状态(已完成),并记录文件存储路径、大小等信息。
具体实战
本次实战基于Spring Boot 3.2.2版本,配合Redis(记录分片状态)、LocalStorage(前端临时存储),实现完整的分片+秒传+断点续传功能,步骤清晰,可直接复制代码落地。
实战准备
- 技术栈:Spring Boot 3.2.2、Redis 7.2、JDK 17、Maven 3.8.6
- 依赖导入:在pom.xml中添加核心依赖(Spring Web、Redis、commons-io)
- 配置文件:配置Redis连接、文件临时存储路径、分片大小等参数
步骤1:导入核心依赖(pom.xml)
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis commons-io commons-io 2.15.1 org.projectlombok lombok true 步骤2:配置文件(application.yml)
spring: redis: host: localhost # 本地Redis地址,生产环境替换为真实地址 port: 6379 password: # 无密码则留空 database: 0 servlet: multipart: max-file-size: 50MB # 单个分片最大大小(与前端分片大小一致) max-request-size: 10GB # 单个请求最大大小(大于最大文件大小)# 自定义配置file: temp-path: D:/temp/chunk/ # 分片临时存储路径(生产环境用Linux路径,如/var/temp/chunk/) upload-path: D:/upload/ # 完整文件最终存储路径 chunk-size: 10485760 # 分片大小(10MB,单位:字节) 步骤3:核心实体类(封装请求参数)
import lombok.Data;import org.springframework.web.multipart.MultipartFile;/** * 分片上传请求参数 */@Datapublic class ChunkUploadDTO { // 文件唯一标识(整个文件的MD5) private String fileMd5; // 分片索引(从0开始) private Integer chunkIndex; // 总分片数 private Integer chunkTotal; // 分片文件 private MultipartFile chunkFile; // 原文件名 private String fileName; // 文件大小(单位:字节) private Long fileSize;} 步骤4:Redis工具类(记录分片状态)
import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.Set;import java.util.concurrent.TimeUnit;@Componentpublic class RedisChunkUtil { @Resource private StringRedisTemplate stringRedisTemplate; // Redis键前缀(区分不同业务) private static final String CHUNK_KEY_PREFIX = "file:chunk:"; /** * 记录已上传的分片 * @param fileMd5 文件唯一标识 * @param chunkIndex 分片索引 */ public void addUploadedChunk(String fileMd5, Integer chunkIndex) { String key = CHUNK_KEY_PREFIX + fileMd5; stringRedisTemplate.opsForSet().add(key, chunkIndex.toString()); // 设置过期时间(24小时,避免Redis内存溢出) stringRedisTemplate.expire(key, 24, TimeUnit.HOURS); } /** * 获取已上传的分片索引集合 * @param fileMd5 文件唯一标识 * @return 已上传的分片索引(字符串形式) */ public Set getUploadedChunks(String fileMd5) { String key = CHUNK_KEY_PREFIX + fileMd5; return stringRedisTemplate.opsForSet().members(key); } /** * 判断分片是否已上传 * @param fileMd5 文件唯一标识 * @param chunkIndex 分片索引 * @return true:已上传,false:未上传 */ public boolean isChunkUploaded(String fileMd5, Integer chunkIndex) { String key = CHUNK_KEY_PREFIX + fileMd5; return stringRedisTemplate.opsForSet().isMember(key, chunkIndex.toString()); } /** * 删除分片状态记录(文件合并成功后调用) * @param fileMd5 文件唯一标识 */ public void deleteChunkRecord(String fileMd5) { String key = CHUNK_KEY_PREFIX + fileMd5; stringRedisTemplate.delete(key); }} 步骤5:核心服务类(分片处理+合并)
import cn.hutool.core.io.FileUtil;import lombok.extern.slf4j.Slf4j;import org.apache.commons.io.FileUtils;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;import java.io.File;import java.io.IOException;import java.util.Set;@Service@Slf4jpublic class FileUploadService { // 从配置文件读取参数 @Value("${file.temp-path}") private String tempPath; @Value("${file.upload-path}") private String uploadPath; @Value("${file.chunk-size}") private Long chunkSize; @Resource private RedisChunkUtil redisChunkUtil; /** * 预上传接口(秒传+断点续传校验) * @param fileMd5 文件唯一标识 * @param fileName 原文件名 * @return 预上传结果(秒传/可上传+已上传分片) */ public PreUploadVO preUpload(String fileMd5, String fileName) { // 1. 拼接完整文件存储路径 String fileFullPath = uploadPath + File.separator + fileName; File fullFile = new File(fileFullPath); // 2. 判断文件是否已存在(秒传逻辑) if (fullFile.exists()) { return PreUploadVO.builder() .status("success") // 秒传成功 .message("文件已存在,秒传成功") .filePath(fileFullPath) .build(); } // 3. 若文件不存在,获取已上传的分片(断点续传逻辑) Set uploadedChunks = redisChunkUtil.getUploadedChunks(fileMd5); return PreUploadVO.builder() .status("continue") // 继续上传 .message("文件未上传完成,可继续上传") .uploadedChunks(uploadedChunks) .build(); } /** * 分片上传接口 * @param dto 分片上传参数 * @return 分片上传结果 * @throws IOException IO异常 */ public String chunkUpload(ChunkUploadDTO dto) throws IOException { String fileMd5 = dto.getFileMd5(); Integer chunkIndex = dto.getChunkIndex(); Integer chunkTotal = dto.getChunkTotal(); MultipartFile chunkFile = dto.getChunkFile(); // 1. 校验分片合法性 if (chunkFile.getSize() > chunkSize) { throw new RuntimeException("分片大小超过限制,最大允许" + chunkSize / 1024 / 1024 + "MB"); } if (chunkIndex < 0 || chunkIndex >= chunkTotal) { throw new RuntimeException("分片索引无效"); } // 2. 校验分片是否已上传(避免重复上传) if (redisChunkUtil.isChunkUploaded(fileMd5, chunkIndex)) { return "当前分片已上传,无需重复提交"; } // 3. 存储分片(临时路径) String chunkTempPath = tempPath + File.separator + fileMd5; File chunkTempDir = new File(chunkTempPath); // 若临时目录不存在,创建目录 if (!chunkTempDir.exists()) { boolean mkdirs = chunkTempDir.mkdirs(); if (!mkdirs) { throw new RuntimeException("分片临时目录创建失败"); } } // 分片文件存储路径(以分片索引命名,避免混乱) String chunkFilePath = chunkTempPath + File.separator + chunkIndex; File chunkFileDest = new File(chunkFilePath); // 保存分片文件 chunkFile.transferTo(chunkFileDest); // 4. 记录分片上传状态到Redis redisChunkUtil.addUploadedChunk(fileMd5, chunkIndex); log.info("分片上传成功:fileMd5={}, chunkIndex={}/{}", fileMd5, chunkIndex, chunkTotal); return "分片上传成功"; } /** * 分片合并接口 * @param fileMd5 文件唯一标识 * @param fileName 原文件名 * @return 合并结果(文件存储路径) * @throws IOException IO异常 */ public String mergeChunk(String fileMd5, String fileName) throws IOException { // 1. 拼接临时分片目录和最终文件路径 String chunkTempPath = tempPath + File.separator + fileMd5; File chunkTempDir = new File(chunkTempPath); String fileFullPath = uploadPath + File.separator + fileName; File fullFile = new File(fileFullPath); // 2. 校验临时目录是否存在(避免无分片可合并) if (!chunkTempDir.exists()) { throw new RuntimeException("分片临时目录不存在,无法合并"); } // 3. 获取所有分片文件,按索引排序 File[] chunkFiles = chunkTempDir.listFiles(); if (chunkFiles == null || chunkFiles.length == 0) { throw new RuntimeException("无分片文件,无法合并"); } // 按分片索引排序(确保合并顺序正确) File[] sortedChunkFiles = FileUtil.sortFiles(chunkFiles, (f1, f2) -> { Integer index1 = Integer.parseInt(f1.getName()); Integer index2 = Integer.parseInt(f2.getName()); return index1.compareTo(index2); }); // 4. 合并分片到最终文件 for (File chunkFile : sortedChunkFiles) { FileUtils.writeByteArrayToFile(fullFile, FileUtils.readFileToByteArray(chunkFile), true); } // 5. 合并成功后,删除临时分片目录和Redis分片记录 FileUtils.deleteDirectory(chunkTempDir); redisChunkUtil.deleteChunkRecord(fileMd5); log.info("文件合并成功:fileMd5={}, fileName={}, 存储路径={}", fileMd5, fileName, fileFullPath); return "文件合并成功,存储路径:" + fileFullPath; }} 步骤6:控制器(对外提供接口)
import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.io.IOException;@RestController@RequestMapping("/file/upload")public class FileUploadController { @Resource private FileUploadService fileUploadService; /** * 预上传接口(秒传+断点续传校验) */ @PostMapping("/pre") public PreUploadVO preUpload(@RequestParam String fileMd5, @RequestParam String fileName) { return fileUploadService.preUpload(fileMd5, fileName); } /** * 分片上传接口 */ @PostMapping("/chunk") public ResultVO chunkUpload(ChunkUploadDTO dto) { try { String message = fileUploadService.chunkUpload(dto); return ResultVO.success(message); } catch (Exception e) { return ResultVO.fail(e.getMessage()); } } /** * 分片合并接口 */ @PostMapping("/merge") public ResultVO mergeChunk(@RequestParam String fileMd5, @RequestParam String fileName) { try { String message = fileUploadService.mergeChunk(fileMd5, fileName); return ResultVO.success(message); } catch (Exception e) { return ResultVO.fail(e.getMessage()); } }} 步骤7:前端简单演示(HTML+JS,计算MD5)
前端核心是计算文件MD5、拆分分片、调用后端接口,这里提供简化版代码(可直接保存为HTML文件运行),需引入spark-md5.js(计算MD5)。
大文件分片上传演示 [xss_clean][xss_clean] 上传进度:0% [xss_clean] let file = null; let fileMd5 = ""; const chunkSize = 10 * 1024 * 1024; // 10MB分片(与后端一致) // 选择文件,计算文件MD5 function handleFileSelect(e) { file = e.files[0]; if (!file) return; // 计算文件MD5(异步,避免阻塞页面) const fileReader = new FileReader(); const spark = new SparkMD5.ArrayBuffer(); fileReader.onload = function (e) { spark.append(e.target.result); fileMd5 = spark.end(); console.log("文件MD5:", fileMd5); }; fileReader.readAsArrayBuffer(file); } // 开始上传(预上传→分片上传→合并) async function startUpload() { if (!file || !fileMd5) { alert("请先选择文件"); return; } const fileName = file.name; const fileSize = file.size; const chunkTotal = Math.ceil(fileSize / chunkSize); // 总分片数 let uploadedCount = 0; // 已上传分片数 // 1. 调用预上传接口,判断是否秒传/断点续传 const preRes = await fetch("/file/upload/pre", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `fileMd5=${fileMd5}&fileName=${fileName}` }).then(res => res.json()); if (preRes.status === "success") { alert(preRes.message); return; } // 2. 处理断点续传:获取已上传分片,跳过已上传的 const uploadedChunks = preRes.uploadedChunks || new Set(); uploadedCount = uploadedChunks.size; // 3. 分片上传 for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) { // 跳过已上传的分片 if (uploadedChunks.has(chunkIndex.toString())) { updateProgress(++uploadedCount, chunkTotal); continue; } // 拆分分片 const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, fileSize); const chunk = file.slice(start, end); // 构造FormData,提交分片 const formData = new FormData(); formData.append("fileMd5", fileMd5); formData.append("chunkIndex", chunkIndex); formData.append("chunkTotal", chunkTotal); formData.append("chunkFile", chunk); formData.append("fileName", fileName); formData.append("fileSize", fileSize); // 调用分片上传接口 const chunkRes = await fetch("/file/upload/chunk", { method: "POST", body: formData }).then(res => res.json()); if (chunkRes.code === 200) { updateProgress(++uploadedCount, chunkTotal); } else { alert(`分片${chunkIndex}上传失败:${chunkRes.message}`); return; } } // 4. 所有分片上传完成,调用合并接口 if (uploadedCount === chunkTotal) { const mergeRes = await fetch("/file/upload/merge", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: `fileMd5=${fileMd5}&fileName=${fileName}` }).then(res => res.json()); alert(mergeRes.message); updateProgress(100, 100); } } // 更新上传进度 function updateProgress(uploaded, total) { const progress = Math.ceil((uploaded / total) * 100); document.getElementById("progress").innerText = `上传进度:${progress}%`; } [xss_clean] 步骤8:测试验证
- 启动Spring Boot项目和Redis服务,确保Redis正常连接。
- 打开上述HTML文件,选择一个大文件(如50MB的压缩包),点击「开始上传」。
- 验证秒传:同一文件再次上传,会直接提示「秒传成功」。
- 验证断点续传:上传过程中刷新页面/关闭浏览器,重新选择同一文件,会继续上传未完成的分片。
- 验证合并:所有分片上传完成后,查看配置的upload-path目录,确认完整文件已生成。
经验总结
结合实战过程和生产环境落地经验,总结6个核心避坑点,避免出现上传失败、服务异常等问题,新手必看:
1. 分片大小合理配置
分片大小并非越大越好,也并非越小越好:太大易超时,太小会导致请求次数过多(增加服务器压力)。推荐配置10-50MB,根据服务器带宽和内存调整;若上传文件以GB级为主,可调整为50-100MB。
2. 前端MD5计算优化
大文件(GB级)前端计算MD5会阻塞页面,建议使用「分片计算MD5」(将文件分片后逐片计算,避免一次性读取整个文件到内存),或使用Web Worker计算,不阻塞主线程。
3. 临时分片清理机制
实战中配置了Redis分片记录24小时过期,但临时分片目录可能残留未合并的分片(如用户上传一半中断),需添加定时任务(如Spring Schedule),定期清理超过24小时的临时分片目录,避免磁盘占满。
4. 生产环境存储选型
实战中使用本地存储,生产环境建议替换为OSS对象存储(如阿里云OSS、腾讯云COS),直接将分片上传到OSS,避免服务器磁盘压力过大;OSS支持分片上传、断点续传,可直接对接Spring Boot。
5. 接口限流与权限控制
大文件上传接口需添加限流(如使用Redis限流),避免恶意请求导致服务崩溃;同时添加权限控制(如Token验证),禁止未授权用户上传文件,防止恶意上传垃圾文件。
6. 异常处理完善
补充分片上传失败重试机制(前端捕获异常后,重试3次);后端添加文件MD5校验(合并后校验文件MD5,确保与前端一致,避免分片丢失或损坏);添加日志打印,方便排查上传失败问题。
总结
Spring Boot大文件上传的核心是「拆分分片、分而治之」,通过「分片上传」解决超时和内存溢出问题,通过「秒传」节省带宽,通过「断点续传」提升用户体验,三者结合是目前行业主流且成熟的解决方案。
本文从原理剖析到具体实战,提供了可直接落地的完整代码,适配Spring Boot 3.x版本,兼顾专业性和实操性,无论是新手还是资深开发者,都能快速应用到项目中。
生产环境落地时,只需根据自身业务需求,调整分片大小、存储方式、限流规则,即可满足不同场景的大文件上传需求;同时注意避坑点,可大幅提升上传稳定性,减少线上问题。
最后,你在落地过程中遇到任何问题(如OSS对接、MD5计算异常、分片合并失败),欢迎在评论区留言,一起交流探讨!