本文目录导读:

这是一个非常经典的面试题和实际开发需求,分片上传的核心目的是解决大文件上传超时、失败重传成本高、内存溢出等问题。
下面我提供一个完整且具有生产参考价值的 Java 分片上传案例,包括前端(HTML+JS) 和后端(Spring Boot) 的核心代码。
核心设计思路
- 前端: 将文件切成固定大小(5MB)的块,为每个块生成唯一标识(如
fileMd5 + chunkIndex),并发或串行发送到后端。 - 后端: 接收每一个分片,暂存到服务器临时目录,当所有分片上传完成后,提供合并接口,按分片序号顺序合并成完整文件。
- 断点续传: 上传前先请求后端接口查询已上传的分片列表,前端跳过已上传的分片。
前端代码(Vue/原生JS示例)
这里以纯 HTML + JS 为例,使用 spark-md5 计算文件 MD5(用于标识文件)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">分片上传示例</title>
<script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="upload()">开始上传</button>
<div id="progress"></div>
<script>
const chunkSize = 5 * 1024 * 1024; // 每个分片 5MB
function upload() {
const file = document.getElementById('fileInput').files[0];
if (!file) return;
// 1. 计算文件 MD5
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (e) => {
spark.append(e.target.result);
const fileMd5 = spark.end();
console.log('文件MD5:', fileMd5);
// 2. 开始分片上传
uploadChunks(file, fileMd5);
};
}
async function uploadChunks(file, fileMd5) {
const totalChunks = Math.ceil(file.size / chunkSize);
// 先查询已上传的分片(断点续传)
const uploadedChunks = await checkUploadedChunks(fileMd5);
console.log('已上传分片:', uploadedChunks);
for (let i = 0; i < totalChunks; i++) {
// 如果该分片已上传,跳过
if (uploadedChunks.includes(i)) {
continue;
}
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', i);
formData.append('totalChunks', totalChunks);
formData.append('fileMd5', fileMd5);
formData.append('originalFilename', file.name);
// 上传单个分片
await fetch('/upload/chunk', {
method: 'POST',
body: formData
});
// 更新进度
document.getElementById('progress').innerText =
`上传进度: ${((i + 1) / totalChunks * 100).toFixed(2)}%`;
}
// 3. 所有分片上传完成后,发起合并请求
await fetch('/upload/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileMd5, originalFilename: file.name, totalChunks })
});
alert('上传完成!');
}
// 查询已有分片(用于断点续传)
async function checkUploadedChunks(fileMd5) {
const resp = await fetch(`/upload/chunks?fileMd5=${fileMd5}`);
return await resp.json(); // 返回已上传的分片索引数组,[0,1,2]
}
</script>
</body>
</html>
后端代码(Spring Boot)
项目依赖(pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
控制器(UploadController.java)
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.*;
import java.util.*;
@RestController
@RequestMapping("/upload")
public class UploadController {
// 分片临时存储目录
@Value("${upload.chunk-dir:/tmp/upload/chunks/}")
private String chunkDir;
// 合并后的文件存储目录
@Value("${upload.final-dir:/tmp/upload/final/}")
private String finalDir;
/**
* 接收单个分片
*/
@PostMapping("/chunk")
public String uploadChunk(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("index") int index,
@RequestParam("fileMd5") String fileMd5) throws IOException {
// 创建该文件的临时分片目录
Path chunkPath = Paths.get(chunkDir, fileMd5);
if (!Files.exists(chunkPath)) {
Files.createDirectories(chunkPath);
}
// 保存分片文件,命名规则:0, 1, 2, ...
File targetFile = new File(chunkPath.toFile(), String.valueOf(index));
chunk.transferTo(targetFile);
return "chunk " + index + " uploaded";
}
/**
* 查询已上传的分片索引列表(用于断点续传)
*/
@GetMapping("/chunks")
public List<Integer> getUploadedChunks(@RequestParam("fileMd5") String fileMd5) {
List<Integer> list = new ArrayList<>();
Path chunkPath = Paths.get(chunkDir, fileMd5);
if (Files.exists(chunkPath)) {
File[] files = chunkPath.toFile().listFiles();
if (files != null) {
for (File f : files) {
list.add(Integer.parseInt(f.getName()));
}
}
}
Collections.sort(list);
return list;
}
/**
* 合并所有分片
*/
@PostMapping("/merge")
public String mergeChunks(@RequestBody MergeRequest request) throws IOException {
String fileMd5 = request.getFileMd5();
String originalFilename = request.getOriginalFilename();
int totalChunks = request.getTotalChunks();
// 准备最终文件路径
Path finalPath = Paths.get(finalDir);
if (!Files.exists(finalPath)) {
Files.createDirectories(finalPath);
}
// 生成最终文件名:使用MD5前缀防止冲突,保留原始后缀
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
Path targetFile = finalPath.resolve(fileMd5 + suffix);
// 如果最终文件已存在,直接返回
if (Files.exists(targetFile)) {
return "file already exists";
}
// 按顺序合并分片
Path chunkPath = Paths.get(chunkDir, fileMd5);
try (FileOutputStream fos = new FileOutputStream(targetFile.toFile())) {
for (int i = 0; i < totalChunks; i++) {
Path chunk = chunkPath.resolve(String.valueOf(i));
if (Files.exists(chunk)) {
Files.copy(chunk, fos);
} else {
throw new RuntimeException("Missing chunk: " + i);
}
}
}
// 合并成功后,删除临时分片目录
deleteDirectory(chunkPath.toFile());
return "merge success: " + targetFile.toString();
}
/**
* 删除目录及内部文件
*/
private void deleteDirectory(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
deleteDirectory(f);
}
}
}
file.delete();
}
/**
* 合并请求体
*/
static class MergeRequest {
private String fileMd5;
private String originalFilename;
private int totalChunks;
// getter setter(省略)
}
}
配置文件(application.yml)
upload: chunk-dir: /data/tmp/upload/chunks/ # 分片临时目录 final-dir: /data/tmp/upload/final/ # 最终文件目录
关键注意事项(面试加分项)
| 问题 | 解决方案 |
|---|---|
| 并发上传顺序错乱 | 后端不依赖顺序接收,合并时按序号读取 |
| 文件重复上传 | 前端传入 fileMd5,后端判断最终文件是否存在 |
| 服务端重启导致分片丢失 | 可用 Redis 记录分片状态,或直接检查磁盘文件 |
| 大文件 MD5 计算卡顿 | 前端使用 File.slice() 分片计算,或使用 Web Worker |
| 传输安全性 | 分片文件名使用 UUID 避免越界,校验分片大小 |
| 异常处理 | 合并时缺少某分片,应抛出异常并提示前端重传该分片 |
生产环境增强建议
- 异步合并:使用消息队列,分片上传完成后发消息异步合并。
- 校验机制:每个分片上传时携带该分片的 MD5,后端校验完整性。
- 并发限流:对同一个
fileMd5的分片上传请求使用令牌桶限制并发数。 - 存储分布式:分片临时文件可使用分布式文件系统(MinIO/OSS),最终合并可在对象存储中直接调用
Combine接口(OSS 原生支持)。
运行验证
- 启动 Spring Boot 应用。
- 打开前端 HTML 页面,选择一个几百 MB 的大文件。
- 观察控制台和磁盘目录:
/tmp/upload/chunks/{fileMd5}/下出现多个数字命名的分片。- 上传完毕后,
/tmp/upload/final/下生成完整文件。
这个案例覆盖了分片上传、断点续传、合并的核心逻辑,稍加改造即可直接用于真实项目。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播