在现代 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();
}
}
}
注意:实际项目中应加入文件名校验、权限控制、异常日志等。
四、前端配合逻辑(简要)
前端需完成以下步骤:
- 计算文件 MD5 (推荐使用
spark-md5库)。 - 调用
/check接口 获取已上传分片索引。 - 循环上传未完成的分片 ,每个请求携带:
fileMd5chunkIndex- 分片 Blob 数据
- 全部上传完成后,调用
/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开源模板。