Java 网页开发中,大文件分块上传 + 断点续传的完整实现方案
在现代 Web 应用中,上传几百 MB 甚至几个 GB 的文件时,一次性上传 很容易失败(超时、断网、浏览器崩溃等),所以行业标准做法是:
前端分片(chunk) + 后端支持断点续传 + 秒传
下面是目前(2025年)最主流、最实用的前后端实现方案。
一、整体流程图
前端 后端
│ │
1. 计算文件整体 hash (md5/sha1/sha256) │
2. 向后端发送 “检查文件” 请求 │
│ fileHash、fileSize、fileName │
│ ────────────────────────────────────► │
│ │ 3. 查询是否已存在完整文件
│ ◄──────────────────────────────────── │ 或哪些分片已上传成功
│ { code: 200/201/404,
│ uploadedChunks: [0,1,3,5,...] 或秒传url }
│
如果秒传 → 直接显示成功
否则 → 并发/串行上传未上传的分片
4. 上传分片
│ chunkIndex、chunkHash、chunkBlob、fileHash
│ ────────────────────────────────────► │
│ │ 5. 接收分片 → 存临时目录
│ │ 记录已上传分片(redis/db)
│ ◄──────────────────────────────────── │ 返回成功
│
6. 所有分片上传完成 → 发送 “合并” 请求
│ fileHash、totalChunks、fileName 等
│ ────────────────────────────────────► │
│ │ 7. 校验分片完整性
│ │ 合并文件
│ │ 移动到正式目录
│ │ 保存文件元信息(db)
│ ◄──────────────────────────────────── │ 返回最终文件地址
二、前端实现(推荐使用 axios + spark-md5 或 Web Worker)
<!-- index.html -->
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传</button>
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
// upload.js
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
async function uploadFile() {
const file = document.getElementById('fileInput').files[0];
if (!file) return;
// 1. 计算文件 hash(用于秒传和续传)
const fileHash = await calculateHash(file);
const fileName = file.name;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// 2. 检查文件是否已存在 / 哪些分片已上传
const { data } = await axios.get('/api/file/check', {
params: { fileHash, fileSize: file.size, fileName }
});
if (data.code === 200) {
// 秒传成功
console.log('秒传成功', data.url);
return;
}
const uploadedChunks = new Set(data.uploadedChunks || []);
// 3. 上传未上传的分片(可并发)
const promises = [];
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.has(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('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('fileHash', fileHash);
formData.append('fileName', fileName);
promises.push(
axios.post('/api/file/upload/chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
);
}
// 并发上传(可控制并发数)
await Promise.all(promises);
// 4. 所有分片上传完成 → 合并
const mergeRes = await axios.post('/api/file/merge', {
fileHash,
totalChunks,
fileName
});
console.log('上传完成', mergeRes.data.url);
}
// 使用 spark-md5 计算文件 hash
async function calculateHash(file) {
return new Promise((resolve) => {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunks = [];
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < 100) { // 采样前100块,够用了
loadNext();
} else {
resolve(spark.end());
}
};
function loadNext() {
const start = currentChunk * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const blob = blobSlice.call(file, start, end);
fileReader.readAsArrayBuffer(blob);
}
loadNext();
});
}
三、后端实现(Spring Boot 示例)
@RestController
@RequestMapping("/api/file")
public class FileUploadController {
private static final String TEMP_DIR = "/upload_temp/";
private static final String FINAL_DIR = "/upload_final/";
// 1. 检查文件是否存在 / 已上传分片
@GetMapping("/check")
public Map<String, Object> checkFile(
@RequestParam String fileHash,
@RequestParam Long fileSize,
@RequestParam String fileName) {
// 假设使用 Redis 或数据库记录
// 这里简化用文件系统判断
File finalFile = new File(FINAL_DIR + fileHash + "_" + fileName);
if (finalFile.exists() && finalFile.length() == fileSize) {
return Map.of("code", 200, "url", "/files/" + fileHash + "_" + fileName);
}
// 返回已上传的分片索引
Set<Integer> uploaded = getUploadedChunks(fileHash);
return Map.of("code", 201, "uploadedChunks", uploaded);
}
// 2. 上传分片
@PostMapping("/upload/chunk")
public Map<String, Object> uploadChunk(
@RequestParam("chunk") MultipartFile chunk,
@RequestParam Integer chunkIndex,
@RequestParam Integer totalChunks,
@RequestParam String fileHash,
@RequestParam String fileName) throws IOException {
String chunkPath = TEMP_DIR + fileHash + "/" + chunkIndex;
File chunkFile = new File(chunkPath);
// 幂等:已存在则跳过
if (chunkFile.exists()) {
return Map.of("code", 200, "msg", "已存在");
}
chunk.transferTo(chunkFile);
// 记录已上传分片(可用 redis set)
saveUploadedChunk(fileHash, chunkIndex);
// 如果所有分片齐了,可以自动合并(或让前端再发合并请求)
if (isAllChunksUploaded(fileHash, totalChunks)) {
mergeFile(fileHash, totalChunks, fileName);
}
return Map.of("code", 200);
}
// 3. 合并文件
@PostMapping("/merge")
public Map<String, Object> merge(
@RequestParam String fileHash,
@RequestParam Integer totalChunks,
@RequestParam String fileName) throws IOException {
if (!isAllChunksUploaded(fileHash, totalChunks)) {
return Map.of("code", 400, "msg", "分片不完整");
}
File target = new File(FINAL_DIR + fileHash + "_" + fileName);
try (FileOutputStream fos = new FileOutputStream(target, true)) {
for (int i = 0; i < totalChunks; i++) {
File chunk = new File(TEMP_DIR + fileHash + "/" + i);
Files.copy(chunk.toPath(), fos);
chunk.delete();
}
}
// 可存入数据库:文件名、路径、大小、上传时间、上传人等
saveFileRecord(fileHash, fileName, target.length());
return Map.of("code", 200, "url", "/files/" + fileHash + "_" + fileName);
}
// 辅助方法(可替换为 Redis / DB)
private Set<Integer> getUploadedChunks(String fileHash) {
// 实际项目建议用 Redis Set: "file:chunks:" + fileHash
return new HashSet<>();
}
private void saveUploadedChunk(String fileHash, int chunkIndex) {
// redis.sadd("file:chunks:" + fileHash, chunkIndex)
}
private boolean isAllChunksUploaded(String fileHash, int totalChunks) {
// 判断 redis set 长度 == totalChunks
return true; // 简化
}
}
四、关键优化点(生产必做)
- 分片大小:5MB~20MB(太小请求多,太大重传代价高)
- 幂等性:同一个分片重复上传不报错
- 秒传:文件 hash 已存在直接返回成功
- 断点续传:前端记录已上传分片,后端返回已上传列表
- 并发控制:前端可控制同时上传 3~6 个分片(用 p-limit 或 Promise.allSettled)
- 进度条:已上传分片数 / 总分片数
- 失败重试:单个分片失败自动重试 3 次
- 存储已上传状态:推荐 Redis Set,设置过期时间(7天或30天)
- 文件名冲突:建议用 hash + 原文件名后缀存储,数据库保存映射
- Nginx/网关:配置
client_max_body_size 10G;
五、云厂商推荐方案(更省事)
- 阿里云 OSS:Multipart Upload + 断点续传 SDK
- 腾讯云 COS:分块上传 + 续传
- 七牛云 KODO:分片上传
- AWS S3:Multipart Upload
这些云存储都提供了完整的断点续传和秒传能力,前端只需调用 SDK,后端负责签名。
希望以上内容对你有帮助!
你现在更倾向于自己实现完整方案,还是想直接对接云存储?
或者有某个具体环节(redis 记录、并发控制、前端进度条、合并校验等)需要更详细代码?