本文目录导读:

在Java中实现文件分片上传,通常用于解决大文件上传超时、断点续传等问题,下面是一个完整的前后端配合方案,包含核心逻辑、代码示例和注意事项。
核心流程
- 前端切片:将大文件切成固定大小的块(如5MB),计算每块的MD5
- 上传分片:按顺序上传每个分片,携带分片序号和分片总数
- 服务端拼接:接收所有分片后,按序号合并成完整文件
- 断点续传:上传前先查询已上传的分片,跳过已完成的部分
前端代码示例(HTML + JS)
<!DOCTYPE html>
<html>
<body>
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传</button>
<progress id="progress" value="0" max="100"></progress>
<div id="status"></div>
<script>
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
function uploadFile() {
const file = document.getElementById('fileInput').files[0];
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileName = file.name;
let uploadedChunks = [];
// 1. 先查询已上传的分片(断点续传)
checkUploadedChunks(fileName, totalChunks).then(existing => {
uploadedChunks = existing;
// 2. 开始上传未完成的分片
uploadChunks(file, fileName, totalChunks, uploadedChunks);
});
}
// 查询已上传的分片列表
async function checkUploadedChunks(fileName, totalChunks) {
const res = await fetch(`/check?fileName=${fileName}&totalChunks=${totalChunks}`);
const data = await res.json();
return data.uploadedChunks || [];
}
// 递归上传所有分片
async function uploadChunks(file, fileName, totalChunks, uploadedChunks) {
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('file', chunk);
formData.append('fileName', fileName);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
// 上传当前分片
const res = await fetch('/upload', { method: 'POST', body: formData });
if (!res.ok) throw new Error(`分片${i}上传失败`);
// 更新进度
const progress = Math.round(((i + 1) / totalChunks) * 100);
document.getElementById('progress').value = progress;
document.getElementById('status').innerText = `上传中: ${i+1}/${totalChunks}`;
}
// 3. 所有分片上传完成,通知服务端合并
const mergeRes = await fetch('/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName, totalChunks })
});
const result = await mergeRes.json();
alert('上传完成: ' + result.url);
}
</script>
</body>
</html>
后端代码示例(Spring Boot)
依赖引入(pom.xml)
<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>
控制器(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.HttpServletRequest;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
@RestController
public class UploadController {
@Value("${upload.dir:/tmp/uploads}")
private String uploadDir;
// 临时分片存储目录
private String getChunkDir(String fileName) {
return uploadDir + "/chunks/" + fileName;
}
// 1. 查询已上传的分片
@GetMapping("/check")
public Map<String, Object> check(@RequestParam String fileName,
@RequestParam int totalChunks) {
Map<String, Object> result = new HashMap<>();
List<Integer> uploaded = new ArrayList<>();
String chunkDir = getChunkDir(fileName);
File dir = new File(chunkDir);
if (dir.exists()) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
try {
uploaded.add(Integer.parseInt(f.getName()));
} catch (NumberFormatException ignored) {}
}
}
}
result.put("uploadedChunks", uploaded);
return result;
}
// 2. 上传分片
@PostMapping("/upload")
public String uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileName") String fileName,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("totalChunks") int totalChunks) throws Exception {
String chunkDir = getChunkDir(fileName);
File dir = new File(chunkDir);
if (!dir.exists()) dir.mkdirs();
// 保存分片到独立文件
File chunkFile = new File(chunkDir, String.valueOf(chunkIndex));
file.transferTo(chunkFile);
return "ok";
}
// 3. 合并分片
@PostMapping("/merge")
public Map<String, Object> merge(@RequestBody Map<String, Object> params) throws Exception {
String fileName = (String) params.get("fileName");
int totalChunks = (int) params.get("totalChunks");
String chunkDir = getChunkDir(fileName);
String finalPath = uploadDir + "/final/" + fileName;
File finalFile = new File(finalPath);
// 使用RandomAccessFile进行高效合并
try (RandomAccessFile raf = new RandomAccessFile(finalFile, "rw")) {
for (int i = 0; i < totalChunks; i++) {
File chunk = new File(chunkDir, String.valueOf(i));
if (!chunk.exists()) {
throw new RuntimeException("分片 " + i + " 缺失");
}
byte[] bytes = Files.readAllBytes(chunk.toPath());
raf.write(bytes);
}
}
// 清理临时分片
deleteDirectory(new File(chunkDir));
Map<String, Object> result = new HashMap<>();
result.put("url", "/files/" + fileName);
return result;
}
// 辅助删除目录
private void deleteDirectory(File dir) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
deleteDirectory(child);
}
}
}
dir.delete();
}
}
静态文件映射(可选,用于下载)
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**")
.addResourceLocations("file:/tmp/uploads/final/");
}
}
application.yml 配置
server:
port: 8080
upload:
dir: /tmp/uploads
spring:
servlet:
multipart:
max-file-size: -1
max-request-size: -1
关键优化与注意事项
并发控制
- 可以允许前端并发上传多个分片(使用
Promise.all),但服务端需要处理并发写入问题 - 建议分片写入独立文件,合并时按顺序读取,天然支持并发
分片校验
- 前端计算每个分片的 MD5,服务端校验,防止数据损坏
- 示例(前端用 SparkMD5 库,服务端用 MessageDigest)
断点续传实现
- 前端上传前调用
/check接口 - 服务端记录已上传的分片索引(可用 Redis 或数据库)
生产环境建议
- 使用 Redis 存储分片状态和文件关联信息
- 大文件合并时使用
RandomAccessFile或FileChannel,避免 OOM - 添加分片过期删除机制(24 小时未完成自动清理)
安全性
- 限制文件名,防止路径穿越(如
../../etc/passwd) - 对文件类型进行校验
- 添加鉴权(JWT Token 等)
测试示例
使用 curl 手动测试:
# 1. 检查已上传分片
curl "http://localhost:8080/check?fileName=test.txt&totalChunks=3"
# 2. 上传分片1
curl -X POST http://localhost:8080/upload \
-F "file=@test.part0" \
-F "fileName=test.txt" \
-F "chunkIndex=0" \
-F "totalChunks=3"
# 3. 上传分片2
curl -X POST http://localhost:8080/upload \
-F "file=@test.part1" \
-F "fileName=test.txt" \
-F "chunkIndex=1" \
-F "totalChunks=3"
# 4. 上传分片3
curl -X POST http://localhost:8080/upload \
-F "file=@test.part2" \
-F "fileName=test.txt" \
-F "chunkIndex=2" \
-F "totalChunks=3"
# 5. 合并
curl -X POST http://localhost:8080/merge \
-H "Content-Type: application/json" \
-d '{"fileName":"test.txt","totalChunks":3}'
常见问题
Q1: 分片大小如何选择?
- 推荐 1MB~10MB,太大会失去分片优势,太小导致请求过多
- 根据网络状况动态调整(如检测到弱网自动减小分片)
Q2: 如何防止重复上传?
- 前端生成文件唯一标识(MD5+文件名+大小)
- 服务端检查文件是否已存在,直接返回已有 URL
Q3: 内存溢出怎么办?
- 使用流式写入,避免一次加载整个文件到内存
- 合并时使用
Files.write(Path, byte[])会被加载到内存,推荐RandomAccessFile
这个方案实现了从零开始的分片上传、断点续传和合并功能,可根据实际需求进行扩展(如增加进度通知、暂停/恢复、秒传等功能)。