本文目录导读:

在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
这个方案实现了从零开始的分片上传、断点续传和合并功能,可根据实际需求进行扩展(如增加进度通知、暂停/恢复、秒传等功能)。
版权声明: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直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播