Spring Boot 实现分片上传、断点续传与进度条
md
# Spring Boot 实现分片上传、断点续传与进度条
## ------ 支持 MinIO / RustFS / SeaweedFS 可配置切换
在大文件上传场景下,传统单次上传存在明显问题:
- 文件过大,失败后需要整体重传
- 网络不稳定,用户体验差
- 无法展示上传进度
本文基于 **Spring Boot**,实现一套**生产可用**的大文件上传方案,支持:
- 分片上传
- 断点续传
- 上传进度查询
- MinIO / RustFS / SeaweedFS / 本地存储
- 通过 yml 配置文件切换存储类型
---
## 一、整体架构设计
系统整体采用「接口隔离 + 策略模式」设计:
Controller ↓ UploadService ↓ StorageService(统一抽象) ↓ MinIO / RustFS / SeaweedFS / Local
yaml
**核心思想:上传逻辑与底层存储解耦。**
---
## 二、统一配置设计
### 1. 存储类型切换
```yaml
file:
path: file/
prefix: pre
domain: domain/
storage:
type: minio # minio / rustfs / seaweedfs / local
只需修改 storage.type,即可切换存储实现。
2. MinIO 配置
yaml
minio:
url: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin123
bucketName: xxx
3. RustFS 配置(S3 协议)
yaml
rustfs:
url: http://localhost:9000
accessKey: rustfsadmin
secretKey: rustfsadmin
bucketName: xxx
RustFS 兼容 S3 协议,可直接复用 MinIO SDK。
4. SeaweedFS 配置
yaml
seaweedfs:
url: http://127.0.0.1:8333
accessKey: weed
secretKey: weed
bucketName: xxx
三、分片上传核心流程
1. 前端切片思路
前端将大文件切割为多个分片(如 5MB):
file
├── chunk_0
├── chunk_1
├── chunk_2
└── chunk_n
每个分片上传时携带:
- 文件唯一标识(guid / md5)
- 分片索引(chunkIndex)
- 总分片数(totalChunk)
2. 分片上传接口
http
POST /file/chunk/upload
四、断点续传实现
1. 查询已上传分片
http
GET /file/chunk/uploaded?guid=xxx
返回示例:
json
[0,1,3,5]
前端只上传缺失分片即可。
2. 实现原则
- 是否已上传以「存储层」为准
- 不依赖内存或 Redis
- 服务重启不影响续传
五、上传进度计算
text
进度 = 已上传分片数 / 总分片数 × 100%
示例返回:
json
{
"uploaded": 6,
"total": 10,
"percent": 60
}
六、存储层抽象设计
1. 统一接口定义
java
public interface StorageService {
void uploadChunk(String path, InputStream inputStream);
boolean exists(String path);
List<Integer> listChunks(String prefix);
void mergeChunks(String chunkPrefix, String targetPath, int totalChunk);
void deleteChunks(String chunkPrefix);
}
2. MinIO / RustFS 实现(S3 通用)
java
@Slf4j
public class MinioStorageService implements StorageService {
private final MinioClient minioClient;
private final String bucket;
public MinioStorageService(MinioClient client, String bucket) {
this.minioClient = client;
this.bucket = bucket;
}
@Override
public void uploadChunk(String path, InputStream inputStream) {
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(path)
.stream(inputStream, -1, 5 * 1024 * 1024)
.build()
);
} catch (Exception e) {
throw new RuntimeException("分片上传失败", e);
}
}
@Override
public boolean exists(String path) {
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucket)
.object(path)
.build()
);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public List<Integer> listChunks(String prefix) {
List<Integer> chunks = new ArrayList<>();
Iterable<Result<Item>> results =
minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucket)
.prefix(prefix)
.build()
);
for (Result<Item> r : results) {
String name = r.get().objectName();
chunks.add(Integer.parseInt(
name.substring(name.lastIndexOf("_") + 1)));
}
return chunks;
}
@Override
public void mergeChunks(String chunkPrefix,
String targetPath,
int totalChunk) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int i = 0; i < totalChunk; i++) {
InputStream in = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(chunkPrefix + "/chunk_" + i)
.build()
);
IOUtils.copy(in, out);
}
uploadChunk(
targetPath,
new ByteArrayInputStream(out.toByteArray()));
} catch (Exception e) {
throw new RuntimeException("分片合并失败", e);
}
}
@Override
public void deleteChunks(String chunkPrefix) {
// 可按需实现批量删除
}
}
七、业务 Service 实现
java
@Service
public class FileUploadServiceImpl implements FileUploadService {
@Autowired
private StorageService storageService;
@Override
public void uploadChunk(ChunkUploadDTO dto) throws IOException {
String path = dto.getGuid() + "/chunk_" + dto.getChunkIndex();
if (storageService.exists(path)) {
return;
}
storageService.uploadChunk(
path, dto.getFile().getInputStream());
}
@Override
public List<Integer> uploadedChunks(String guid) {
return storageService.listChunks(guid + "/");
}
@Override
public void merge(String guid, int totalChunk) {
storageService.mergeChunks(
guid, guid + ".final", totalChunk);
storageService.deleteChunks(guid + "/");
}
}
八、Controller 接口
java
@RestController
@RequestMapping("/file/chunk")
public class FileUploadController {
@Autowired
private FileUploadService fileUploadService;
@PostMapping("/upload")
public void upload(ChunkUploadDTO dto) throws IOException {
fileUploadService.uploadChunk(dto);
}
@GetMapping("/uploaded")
public List<Integer> uploaded(@RequestParam String guid) {
return fileUploadService.uploadedChunks(guid);
}
@PostMapping("/merge")
public void merge(@RequestParam String guid,
@RequestParam Integer totalChunk) {
fileUploadService.merge(guid, totalChunk);
}
}
九、总结
本文实现了一套 Spring Boot 大文件上传方案,具备:
- 分片上传
- 断点续传
- 上传进度计算
- 多存储后端解耦
- yml 配置快速切换
适用于文件中心、数据平台、企业网盘等场景,可直接用于生产环境。
欢迎点赞、收藏、评论交流。