-
网络传输不稳定: 单次请求时间长,容易中断
-
服务器资源耗尽: 大文件一次性加载导致内存溢出
-
上传失败代价高: 需要重新上传整个文件
-
减小单次请求负载
-
支持断点续传
-
并发上传提高效率
-
降低服务器内存压力

<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>
@RestController
@RequestMapping("/upload")
publicclassChunkUploadController{
privatefinal String CHUNK_DIR = "uploads/chunks/";
privatefinal String FINAL_DIR = "uploads/final/";
/**
* 初始化上传
* @param fileName 文件名
* @param fileMd5 文件唯一标识
*/
@PostMapping("/init")
public ResponseEntity<String> initUpload(
@RequestParam String fileName,
@RequestParam String fileMd5){
// 创建分块临时目录
String uploadId = UUID.randomUUID().toString();
Path chunkDir = Paths.get(CHUNK_DIR, fileMd5 + "_" + uploadId);
try {
Files.createDirectories(chunkDir);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("创建目录失败");
}
return ResponseEntity.ok(uploadId);
}
/**
* 上传分块
* @param chunk 分块文件
* @param index 分块索引
*/
@PostMapping("/chunk")
public ResponseEntity<String> uploadChunk(
@RequestParam MultipartFile chunk,
@RequestParam String uploadId,
@RequestParam String fileMd5,
@RequestParam Integer index){
// 生成分块文件名
String chunkName = "chunk_" + index + ".tmp";
Path filePath = Paths.get(CHUNK_DIR, fileMd5 + "_" + uploadId, chunkName);
try {
chunk.transferTo(filePath);
return ResponseEntity.ok("分块上传成功");
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("分块保存失败");
}
}
/**
* 合并文件分块
*/
@PostMapping("/merge")
public ResponseEntity<String> mergeChunks(
@RequestParam String fileName,
@RequestParam String uploadId,
@RequestParam String fileMd5){
// 1. 获取分块目录
File chunkDir = new File(CHUNK_DIR + fileMd5 + "_" + uploadId);
// 2. 获取排序后的分块文件
File[] chunks = chunkDir.listFiles();
if (chunks == null || chunks.length == 0) {
return ResponseEntity.badRequest().body("无分块文件");
}
Arrays.sort(chunks, Comparator.comparingInt(f ->
Integer.parseInt(f.getName().split("_")[1].split("\\.")[0])));
// 3. 合并文件
Path finalPath = Paths.get(FINAL_DIR, fileName);
try (BufferedOutputStream outputStream =
new BufferedOutputStream(Files.newOutputStream(finalPath))) {
for (File chunkFile : chunks) {
Files.copy(chunkFile.toPath(), outputStream);
}
// 4. 清理临时分块
FileUtils.deleteDirectory(chunkDir);
return ResponseEntity.ok("文件合并成功:" + finalPath);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("合并失败:" + e.getMessage());
}
}
}
-
核心依赖 -
关键控制器实现 -
高性能文件合并优化
// 使用RandomAccessFile提高性能
publicvoidmergeFiles(File targetFile, List<File> chunkFiles)throws IOException {
try (RandomAccessFile target =
new RandomAccessFile(targetFile, "rw")) {
byte[] buffer = newbyte[1024 * 8]; // 8KB缓冲区
long position = 0;
for (File chunk : chunkFiles) {
try (RandomAccessFile src =
new RandomAccessFile(chunk, "r")) {
int bytesRead;
while ((bytesRead = src.read(buffer)) != -1) {
target.write(buffer, 0, bytesRead);
}
position += chunk.length();
}
}
}
}
// 5MB分块大小
const CHUNK_SIZE = 5 * 1024 * 1024;
/**
* 处理文件分块
*/
functionprocessFile(file) {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
const chunks = [];
for (let i = 0; i < chunkCount; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(file.size, start + CHUNK_SIZE);
chunks.push(file.slice(start, end));
}
return chunks;
}
asyncfunctionuploadFile(file) {
// 1. 初始化上传
const { data: uploadId } = await axios.post('/upload/init', {
fileName: file.name,
fileMd5: await calculateFileMD5(file)// 文件MD5计算
});
// 2. 分块上传
const chunks = processFile(file);
const total = chunks.length;
let uploaded = 0;
awaitPromise.all(chunks.map((chunk, index) => {
const formData = new FormData();
formData.append('chunk', chunk, `chunk_${index}`);
formData.append('index', index);
formData.append('uploadId', uploadId);
formData.append('fileMd5', fileMd5);
return axios.post('/upload/chunk', formData, {
headers: {'Content-Type': 'multipart/form-data'},
onUploadProgress: progress => {
// 更新进度条
const percent = ((uploaded * 100) / total).toFixed(1);
updateProgress(percent);
}
}).then(() => uploaded++);
}));
// 3. 触发合并
const result = await axios.post('/upload/merge', {
fileName: file.name,
uploadId,
fileMd5
});
alert(`上传成功: ${result.data}`);
}
-
分块处理函数 -
带进度显示的上传逻辑
@GetMapping("/check/{fileMd5}/{uploadId}")
public ResponseEntity<List<Integer>> getUploadedChunks(
@PathVariable String fileMd5,
@PathVariable String uploadId) {
Path chunkDir = Paths.get(CHUNK_DIR, fileMd5 + "_" + uploadId);
if (!Files.exists(chunkDir)) {
return ResponseEntity.ok(Collections.emptyList());
}
try {
List<Integer> uploaded = Files.list(chunkDir)
.map(p -> p.getFileName().toString())
.filter(name -> name.startsWith("chunk_"))
.map(name -> name.replace("chunk_", "").replace(".tmp", ""))
.map(Integer::parseInt)
.collect(Collectors.toList());
return ResponseEntity.ok(uploaded);
} catch (IOException e) {
return ResponseEntity.status(500).body(Collections.emptyList());
}
}
const uploadedChunks = await axios.get(
`/upload/check/${fileMd5}/${uploadId}`
);
chunks.map((chunk, index) => {
if (uploadedChunks.includes(index)) {
uploaded++; // 已上传则跳过
returnPromise.resolve();
}
// 执行上传...
});
@PostMapping("/chunk")
public ResponseEntity<?> uploadChunk(
@RequestParam MultipartFile chunk,
@RequestParam String sign // 前端生成的签名
) {
// 使用密钥验证签名
String secretKey = "your-secret-key";
String serverSign = HmacUtils.hmacSha256Hex(secretKey,
chunk.getBytes());
if (!serverSign.equals(sign)) {
return ResponseEntity.status(403).body("签名验证失败");
}
// 处理分块...
}
@Configuration
publicclassMinioConfig{
@Bean
public MinioClient minioClient(){
return MinioClient.builder()
.endpoint("http://minio:9000")
.credentials("minio-access", "minio-secret")
.build();
}
}
@Service
publicclassMinioUploadService{
@Autowired
private MinioClient minioClient;
publicvoiduploadChunk(String bucket,
String object,
InputStream chunkStream,
long length)throws Exception {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(object)
.stream(chunkStream, length, -1)
.build()
);
}
}
|
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
内网环境:10MB-20MB
-
移动网络:1MB-5MB
-
广域网:500KB-1MB
@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每日清理
publicvoidcleanTempFiles(){
File tempDir = new File(CHUNK_DIR);
// 删除超过24小时的临时目录
FileUtils.deleteDirectory(tempDir);
}
spring:
servlet:
multipart:
max-file-size:100MB#单块最大限制
max-request-size:100MB
往期推荐
掌握 Spring 框架这 10 个扩展点,开发效率直接翻倍!
告别繁琐集成,一行代码搞定全球AI大模型!
再见Maven!官方推出全新一代Java项目构建工具,性能提升2~10倍
Docker 拉取镜像超时?别再瞎抄配置了!亲测 3 个有效镜像源 + 避坑指南
Redis官方发布高颜值可视化工具,功能更是强的离谱!
五大Java对象映射工具终极对决:从Spring、Apache到MapStruct的性能深度评测

