Spring Boot 实现分片上传+断点续传+实时进度条,彻底解决大文件上传痛点!
大家好,我是你们的技术博主~平时做项目或者日常使用网盘、后台管理系统时,肯定都遇到过这种糟心场景:
上传几个G的视频、安装包或者备份文件,传了一半网络断了、页面刷新了,只能从头重新上传;漫长的上传过程中,只有一个转圈加载动画,完全不知道传了多少、还要等多久;大文件直接整包上传,服务器压力拉满,还容易超时失败。
这些问题,其实靠分片上传+断点续传+实时进度条就能完美解决!今天就带大家从零到一,用 Spring Boot 后端搭配简易前端,完整实现这套实用功能,代码可直接移植到项目里,新手也能看懂上手~
一、核心原理先搞懂,知其然更知其所以然
很多同学一上来就敲代码,结果遇到bug不知道根源在哪,咱们先把核心逻辑捋清楚,后续写代码思路会特别顺。
1. 分片上传:把大文件“拆碎了”传
分片上传的核心,就是把一个大文件按照固定大小切割成若干个小分片,比如每片5MB、10MB,然后逐个上传这些小分片,而不是一次性上传整个大文件。
这样做的好处特别明显:单个分片体积小,上传失败概率大幅降低,就算某一片传失败了,只需要重传这一片,不用动其他已经传好的;服务器接收小分片,内存和IO压力小很多,不会出现大文件上传导致的服务卡顿、超时问题;还能支持多线程并行上传分片,进一步提速。
所有分片上传完成后,后端再按照分片序号,把这些小分片按顺序合并成完整的原始文件,整个流程就完成了。
2. 断点续传:断网刷新也不怕,接着上次进度传
断点续传是在分片上传基础上做的优化,核心是记录已上传成功的分片信息,避免重复上传。
具体逻辑:上传文件前,先给文件生成一个唯一标识(通常用文件MD5值,文件内容不变,MD5就不变,精准标识文件);每次上传前,后端根据这个唯一标识,查询已经上传成功的分片列表;前端拿到已上传分片后,跳过这些分片,只上传剩下未完成的,就算中途断网、关闭页面,下次上传同一个文件,直接从断点继续,不用从头再来。
3. 实时进度条:上传进度看得见,告别盲等
进度条实现不难,核心是前后端配合计算上传比例:前端统计已成功上传的分片数量/总分片数量,或者已上传字节数/总文件字节数,实时计算进度百分比,同步渲染到页面进度条;后端也可以缓存上传进度,支持前端主动查询,就算页面刷新,也能回显上次进度,体验更丝滑。
核心关键点总结:文件唯一标识(MD5)+ 分片存储 + 已上传分片记录 + 后端合并分片 + 进度实时计算,这五个点是整套功能的灵魂,缺一不可。
二、项目环境搭建,极简配置不折腾
这套方案采用Spring Boot 2.7.x做后端(稳定兼容,大部分公司都在用),前端用原生HTML+JS+Axios实现,不用搭建Vue/React脚手架,打开浏览器就能跑,测试起来特别方便。
后端 Spring Boot 项目搭建
1. 核心依赖(pom.xml)
只需要引入基础Web依赖和文件操作相关工具,不用多余依赖,轻量无负担:
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 简化实体类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- commons-lang3 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
2. application.yml 配置
主要配置文件上传大小限制、临时分片存储路径、文件最终存储路径,避免上传报错:
server:
port: 8080
spring:
servlet:
multipart:
# 单个分片文件大小限制
max-file-size: 10MB
# 单次请求大小限制
max-request-size: 100MB
# 自定义配置
upload:
# 临时分片存储目录
chunk-path: D:/upload/chunk
# 最终完整文件存储目录
file-path: D:/upload/file
提前手动创建对应的文件夹,避免后端启动报错找不到路径~
前端准备
前端只需要一个HTML页面,引入Axios做接口请求,引入spark-md5计算文件MD5(用于生成文件唯一标识),全程原生代码,逻辑清晰,方便修改适配自己的页面。
三、后端实战代码,核心接口全实现
后端主要实现四大核心接口,覆盖整个上传流程:文件MD5校验(秒传+断点查询)、分片上传接口、分片合并接口、上传进度查询接口。
1. 自定义配置类读取路径
用ConfigurationProperties读取配置文件里的存储路径,方便后续复用:
@Component
@ConfigurationProperties(prefix = "upload")
@Data
public class UploadProperties {
/**
* 分片存储路径
*/
private String chunkPath;
/**
* 完整文件存储路径
*/
private String filePath;
}
2. 分片上传DTO(接收前端参数)
接收前端传递的分片相关参数,包含文件唯一标识、分片序号、总分片数、分片文件等:
@Data
public class ChunkUploadDTO {
/**
* 文件唯一标识(MD5值)
*/
private String fileMd5;
/**
* 文件名
*/
private String fileName;
/**
* 当前分片索引(从0开始)
*/
private Integer chunkIndex;
/**
* 总分片数
*/
private Integer totalChunks;
/**
* 分片文件
*/
private MultipartFile chunkFile;
}
3. 核心上传Controller(四大接口)
这是整个后端的核心,把所有逻辑封装在Controller里,业务简单的场景直接用,复杂业务可以抽离Service层:
@RestController
@RequestMapping("/upload")
@CrossOrigin(origins = "*") // 跨域配置,本地测试方便
@RequiredArgsConstructor
public class UploadController {
private final UploadProperties uploadProperties;
/**
* 1. 检查文件是否已上传(秒传)+ 查询已上传分片(断点续传)
*/
@GetMapping("/check")
public R check(String fileMd5, String fileName) {
// 构建完整文件路径
File targetFile = new File(uploadProperties.getFilePath(), fileName);
// 如果文件已存在,直接返回秒传
if (targetFile.exists()) {
return R.success().message("文件已存在,秒传成功!").data("exists", true);
}
// 如果文件不存在,查询已上传的分片
File chunkDir = new File(uploadProperties.getChunkPath(), fileMd5);
List<Integer> uploadedChunks = new ArrayList<>();
if (chunkDir.exists()) {
File[] chunks = chunkDir.listFiles();
if (chunks != null) {
for (File chunk : chunks) {
// 分片文件名是索引,解析成数字
uploadedChunks.add(Integer.parseInt(chunk.getName()));
}
}
}
return R.success().data("uploadedChunks", uploadedChunks).data("exists", false);
}
/**
* 2. 分片上传接口
*/
@PostMapping("/chunk")
public R uploadChunk(ChunkUploadDTO dto) throws IOException {
// 分片存储目录:按文件MD5创建文件夹,隔离不同文件的分片
File chunkDir = new File(uploadProperties.getChunkPath(), dto.getFileMd5());
if (!chunkDir.exists()) {
chunkDir.mkdirs();
}
// 分片文件:以分片索引为文件名
File chunkFile = new File(chunkDir, dto.getChunkIndex().toString());
// 写入分片文件
dto.getChunkFile().transferTo(chunkFile);
return R.success().message("分片上传成功");
}
/**
* 3. 合并分片接口
*/
@PostMapping("/merge")
public R merge(String fileMd5, String fileName, Integer totalChunks) throws IOException {
File chunkDir = new File(uploadProperties.getChunkPath(), fileMd5);
File targetFile = new File(uploadProperties.getFilePath(), fileName);
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
// 创建文件输出流,按顺序合并分片
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024 * 1024];
// 按分片索引从小到大合并
for (int i = 0; i < totalChunks; i++) {
File chunkFile = new File(chunkDir, String.valueOf(i));
if (!chunkFile.exists()) {
return R.error().message("分片缺失,合并失败");
}
// 读取分片写入目标文件
try (FileInputStream fis = new FileInputStream(chunkFile)) {
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
// 合并后删除分片(可选,节省磁盘空间)
chunkFile.delete();
}
}
// 删除空的分片目录
chunkDir.delete();
return R.success().message("文件合并成功");
}
/**
* 4. 查询上传进度
*/
@GetMapping("/progress")
public R getProgress(String fileMd5, Integer totalChunks) {
File chunkDir = new File(uploadProperties.getChunkPath(), fileMd5);
int uploadedCount = 0;
if (chunkDir.exists()) {
File[] chunks = chunkDir.listFiles();
uploadedCount = chunks != null ? chunks.length : 0;
}
// 计算进度百分比
int progress = (int) ((uploadedCount * 1.0 / totalChunks) * 100);
return R.success().data("progress", progress);
}
}
4. 统一返回结果类R
标准化接口返回格式,前端处理更方便:
@Data
public class R {
private Integer code;
private String message;
private Map<String, Object> data = new HashMap<>();
public static R success() {
R r = new R();
r.setCode(200);
return r;
}
public static R error() {
R r = new R();
r.setCode(500);
return r;
}
public R message(String message) {
this.setMessage(message);
return this;
}
public R data(String key, Object value) {
this.data.put(key, value);
return this;
}
}
四、前端极简实现,进度条+断点续传一步到位
前端核心做这几件事:选择文件 → 计算MD5 → 查询已上传分片(断点)→ 分片切割 → 并行上传 → 实时更新进度 → 合并分片。
直接贴完整HTML代码,新建一个html文件,替换接口地址就能运行:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>大文件分片上传</title>
<style>
.upload-box {width: 600px;margin: 50px auto;}
.progress-bar {width: 100%;height: 20px;background: #f5f5f5;border-radius: 10px;margin: 20px 0;}
.progress {height: 100%;background: #409eff;border-radius: 10px;width: 0%;transition: width 0.3s;}
.progress-text {text-align: center;font-size: 16px;}
button {padding: 8px 20px;background: #409eff;color: #fff;border: none;border-radius: 4px;cursor: pointer;}
</style>
</head>
<body>
<div class="upload-box">
<input type="file" id="fileInput" multiple>
<button onclick="uploadFile()">开始上传</button>
<div class="progress-bar"><div class="progress" id="progress"></div></div>
<div class="progress-text" id="progressText">0%</div>
</div>
<!-- 引入axios和spark-md5 -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
<script>
// 配置分片大小(10MB,可根据网络调整)
const CHUNK_SIZE = 10 * 1024 * 1024;
let file = null;
let fileMd5 = '';
let totalChunks = 0;
let uploadedChunkCount = 0;
// 选择文件
document.getElementById('fileInput').addEventListener('change', (e) => {
file = e.target.files[0];
});
// 计算文件MD5
function calculateMd5(file) {
return new Promise((resolve) => {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
let start = 0;
const end = start + CHUNK_SIZE;
function loadNext() {
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
fileReader.onload = (e) => {
spark.append(e.target.result);
start += CHUNK_SIZE;
if (start < file.size) {
loadNext();
} else {
resolve(spark.end());
}
};
loadNext();
});
}
// 上传文件主函数
async function uploadFile() {
if (!file) {alert('请选择文件');return;}
// 1. 计算文件MD5
fileMd5 = await calculateMd5(file);
totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// 2. 查询已上传分片(断点续传+秒传)
const checkRes = await axios.get('/upload/check', {
params: {fileMd5, fileName: file.name}
});
// 秒传逻辑
if (checkRes.data.data.exists) {
document.getElementById('progressText').innerText = checkRes.data.message;
document.getElementById('progress').style.width = '100%';
return;
}
// 获取已上传分片列表
const uploadedChunks = checkRes.data.data.uploadedChunks || [];
uploadedChunkCount = uploadedChunks.length;
// 更新初始进度
updateProgress();
// 3. 循环上传未完成的分片
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) continue; // 跳过已上传分片
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
// 构建表单数据
const formData = new FormData();
formData.append('fileMd5', fileMd5);
formData.append('fileName', file.name);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('chunkFile', chunk);
// 上传分片
await axios.post('/upload/chunk', formData);
uploadedChunkCount++;
updateProgress();
}
// 4. 所有分片上传完成,调用合并接口
await axios.post('/upload/merge', null, {
params: {fileMd5, fileName: file.name, totalChunks}
});
document.getElementById('progressText').innerText = '上传合并完成!';
}
// 更新进度条
function updateProgress() {
const progress = Math.round((uploadedChunkCount / totalChunks) * 100);
document.getElementById('progress').style.width = progress + '%';
document.getElementById('progressText').innerText = progress + '%';
}
</script>
</body>
</html>
五、实测效果+常见避坑要点
实测效果演示
-
正常上传:选择大文件,页面实时显示进度条,从0%稳步涨到100%,最后提示合并完成;
-
断点续传:上传到50%时关闭页面,重新选择同一个文件,直接从50%开始继续上传;
-
秒传功能:文件已经上传过,再次选择直接提示秒传成功,不用重复上传;
常见避坑要点(踩坑总结)
1. 跨域问题:本地测试前端和后端端口不一样,一定要加@CrossOrigin跨域注解,生产环境用Nginx配置跨域;
2. 文件大小限制:Spring Boot默认上传大小有限制,必须在yml里配置max-file-size和max-request-size,否则分片过大会报错;
3. 分片大小适配:分片不要太小(请求次数多),也不要太大(容易超时),普通网络建议5-10MB最合适;
4. MD5计算卡顿:超大文件计算MD5会阻塞页面,可优化成WebWorker异步计算,提升前端体验;
5. 磁盘权限:后端存储目录一定要有读写权限,Linux环境尤其注意,否则会出现文件写入失败;
6. 分片清理:合并完成后记得删除临时分片,避免占用服务器磁盘空间,也可以加定时任务清理过期分片。
六、总结与拓展方向
今天这套 Spring Boot + 原生前端的分片上传、断点续传、进度条方案,已经覆盖了90%的业务场景,代码简洁易懂,没有多余的第三方中间件,拿来就能改、改完就能用。
如果想进一步优化,还可以往这些方向拓展:
-
接入云存储:把本地存储换成MinIO、阿里云OSS、腾讯云COS,适配分布式部署;
-
多线程并行上传:前端优化成多线程同时上传多个分片,大幅提升上传速度;
-
进度持久化:把上传进度存入数据库,支持多设备、跨页面同步进度;
-
失败重试机制:单个分片上传失败自动重试,提升弱网环境下的成功率;
-
权限校验:加上登录校验、文件上传权限控制,适配企业级后台系统。
大文件上传是后端开发的高频考点和实用技能,吃透这套逻辑,不管是面试还是做项目,都能轻松拿捏~
觉得有用的话,记得点赞、在看、转发,下期给大家更进阶的分布式场景下大文件上传优化,咱们下期见!