SpringBoot集成MinIO實現(xiàn)大文件分片上傳的示例代碼
需求背景:為什么需要分片上傳?
1. 傳統(tǒng)上傳方式的痛點
在文件上傳場景中,當(dāng)用戶嘗試上傳超過 100MB 的大文件時,傳統(tǒng)單次上傳方式會面臨三大核心問題:
(1)網(wǎng)絡(luò)穩(wěn)定性挑戰(zhàn)
- 弱網(wǎng)環(huán)境下(如移動網(wǎng)絡(luò)/跨國傳輸)易出現(xiàn)傳輸中斷
- 網(wǎng)絡(luò)波動可能導(dǎo)致整個文件重傳(用戶需從0%重新開始)
(2)服務(wù)器資源瓶頸
- 單次傳輸大文件占用大量內(nèi)存(如上傳10GB文件需要預(yù)留10GB內(nèi)存)
- 長時間占用線程影響服務(wù)器吞吐量
(3)用戶體驗缺陷
- 無法顯示實時進(jìn)度條
- 不支持?jǐn)帱c續(xù)傳
- 失敗重試成本極高
2. 分片上傳的核心優(yōu)勢
技術(shù)價值
| 特性 | 說明 |
|---|---|
| 可靠性 | 單個分片失敗不影響整體上傳,支持分片級重試 |
| 內(nèi)存控制 | 分片按需加載(如5MB/片),內(nèi)存占用恒定 |
| 并行加速 | 支持多分片并發(fā)上傳(需配合前端Worker實現(xiàn)) |
業(yè)務(wù)價值
- 支持超大文件:可突破GB級文件上傳限制
- 斷點續(xù)傳:刷新頁面/切換設(shè)備后繼續(xù)上傳
- 精準(zhǔn)進(jìn)度:實時顯示每個分片的上傳狀態(tài)
- 容災(zāi)能力:分片可跨服務(wù)器分布式存儲
3. 典型應(yīng)用場景
(1)企業(yè)級網(wǎng)盤系統(tǒng)
- 用戶上傳設(shè)計圖紙(平均500MB-2GB)
- 跨國團(tuán)隊協(xié)作時處理4K視頻素材(10GB+)
(2)醫(yī)療影像系統(tǒng)
- 醫(yī)院PACS系統(tǒng)上傳CT掃描文件(單次檢查約3GB)
- 支持醫(yī)生在弱網(wǎng)環(huán)境下暫停/恢復(fù)上傳
(3)在線教育平臺
- 講師上傳高清課程視頻(1080P視頻約2GB/小時)
- 學(xué)員斷網(wǎng)后自動恢復(fù)上傳至95%進(jìn)度
4. 為什么選擇MinIO?
MinIO作為高性能對象存儲方案,與分片上傳架構(gòu)完美契合:
(1)分布式架構(gòu)
- 自動將分片分布到不同存儲節(jié)點
- 支持EC糾刪碼保障數(shù)據(jù)可靠性
(2)高性能合并
// MinIO服務(wù)端合并只需一次API調(diào)用 minioClient.composeObject(ComposeObjectArgs.builder()...);
相比傳統(tǒng)文件IO合并方式,速度提升5-10倍
(3)生命周期管理
- 可配置自動清理臨時分片
- 合并后文件自動歸檔至冷存儲
一、環(huán)境準(zhǔn)備與依賴配置
1. 開發(fā)環(huán)境要求
- JDK 17+
- Maven 3.6+
- MinIO Server(推薦版本:RELEASE.2023-10-25T06-33-25Z)
2. 項目依賴(pom.xml)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.4</version>
</dependency>
<!-- MinIO Java SDK -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<optional>true</optional>
</dependency>
</dependencies>
二、核心代碼實現(xiàn)解析
1. MinIO服務(wù)配置(FileUploadService)
(1) 客戶端初始化
private MinioClient createMinioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
- 通過
@Value注入配置參數(shù) - 支持自定義endpoint和認(rèn)證信息
(2) 分片上傳實現(xiàn)
public String uploadFilePart(String fileId, String fileName,
MultipartFile filePart, Integer chunkIndex,
Integer totalChunks) throws IOException {
String objectName = fileId + "/" + fileName + '-' + chunkIndex;
PutObjectArgs args = PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(filePart.getInputStream(), filePart.getSize(), -1)
.build();
minioClient.putObject(args);
return objectName;
}
- 分片命名規(guī)則:
{fileId}/{fileName}-{chunkIndex} - 支持任意大小的文件分片
(3) 分片合并邏輯
public void mergeFileParts(FileMergeReqVO reqVO) throws IOException {
String finalObjectName = "merged/" + reqVO.getFileId() + "/" + reqVO.getFileName();
List<ComposeSource> sources = reqVO.getPartNames().stream()
.map(name -> ComposeSource.builder()
.bucket(bucketName)
.object(name)
.build())
.toList();
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(bucketName)
.object(finalObjectName)
.sources(sources)
.build());
// 清理臨時分片
reqVO.getPartNames().forEach(partName -> {
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(partName)
.build());
} catch (Exception e) {
log.error("Delete chunk failed: {}", partName, e);
}
});
}
- 使用MinIO的
composeObject合并分片 - 最終文件存儲在
merged/{fileId}目錄 - 自動清理已合并的分片
2. 控制層設(shè)計(FileUploadController)
@PostMapping("/upload/part/{fileId}")
public CommonResult<String> uploadFilePart(
@PathVariable String fileId,
@RequestParam String fileName,
@RequestParam MultipartFile filePart,
@RequestParam int chunkIndex,
@RequestParam int totalChunks) {
// [邏輯處理...]
}
@PostMapping("/merge")
public CommonResult<String> mergeFileParts(@RequestBody FileMergeReqVO reqVO) {
// [合并邏輯...]
}
3. 前端分片上傳實現(xiàn)
const chunkSize = 5 * 1024 * 1024; // 5MB分片
async function uploadFile() {
const file = document.getElementById('fileInput').files[0];
const fileId = generateUUID();
// 分片上傳循環(huán)
for (let i = 0; i < totalChunks; i++) {
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('filePart', chunk);
formData.append('chunkIndex', i + 1);
await fetch('/upload/part/' + fileId, {
method: 'POST',
body: formData
});
}
// 觸發(fā)合并
await fetch('/merge', {
method: 'POST',
body: JSON.stringify({
fileId: fileId,
partNames: generatedPartNames
})
});
}
三、功能測試驗證
測試用例1:上傳500MB視頻文件
選擇測試文件:sample.mp4(512MB)
觀察分片上傳過程:
- 總生成103個分片(5MB/片)
- 上傳進(jìn)度實時更新
合并完成后檢查MinIO:
sta-bucket
└── merged
└── 6ba7b814...
└── sample.mp4
下載驗證文件完整性

測試用例2:中斷恢復(fù)測試
- 上傳過程中斷網(wǎng)絡(luò)連接
- 重新上傳時:
- 已完成分片跳過上傳
- 繼續(xù)上傳剩余分片
- 最終合并成功
四、關(guān)鍵配置項說明
| 配置項 | 示例值 | 說明 |
|---|---|---|
| minio.endpoint | http://localhost:9991 | MinIO服務(wù)器地址 |
| minio.access-key | root | 訪問密鑰 |
| minio.secret-key | xxxxx | 秘密密鑰 |
| minio.bucket-name | minio-xxxx | 默認(rèn)存儲桶名稱 |
| server.servlet.port | 8080 | Spring Boot服務(wù)端口 |
附錄:完整源代碼
1. 后端核心類
FileUploadService.java
@Service
public class FileUploadService {
@Value("${minio.endpoint:http://localhost:9991}")
private String endpoint; // MinIO服務(wù)器地址
@Value("${minio.access-key:root}")
private String accessKey; // MinIO訪問密鑰
@Value("${minio.secret-key:xxxx}")
private String secretKey; // MinIO秘密密鑰
@Value("${minio.bucket-name:minio-xxxx}")
private String bucketName; // 存儲桶名稱
/**
* 創(chuàng)建 MinIO 客戶端
*
* @return MinioClient 實例
*/
private MinioClient createMinioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
/**
* 如果存儲桶不存在,則創(chuàng)建存儲桶
*/
public void createBucketIfNotExists() throws IOException, NoSuchAlgorithmException, InvalidKeyException {
MinioClient minioClient = createMinioClient();
try {
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
} catch (MinioException e) {
throw new IOException("Error checking or creating bucket: " + e.getMessage(), e);
}
}
/**
* 上傳文件分片到MinIO
*
* @param fileId 文件標(biāo)識符
* @param filePart 文件分片
* @return 分片對象名稱
*/
public String uploadFilePart(String fileId, String fileName, MultipartFile filePart, Integer chunkIndex, Integer totalChunks) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
MinioClient minioClient = createMinioClient();
try {
// 構(gòu)建分片對象名稱
String objectName = fileId + "/" + fileName + '-' + chunkIndex;
// 設(shè)置上傳參數(shù)
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(filePart.getInputStream(), filePart.getSize(), -1)
.contentType(filePart.getContentType())
.build();
// 上傳文件分片
minioClient.putObject(putObjectArgs);
return objectName;
} catch (MinioException e) {
throw new IOException("Error uploading file part: " + e.getMessage(), e);
}
}
/**
* 合并多個文件分片為一個完整文件
*/
public void mergeFileParts(FileMergeReqVO reqVO) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
MinioClient minioClient = createMinioClient();
try {
// 構(gòu)建最終文件對象名稱
String finalObjectName = "merged/" + reqVO.getFileId() + "/" + reqVO.getFileName();
// 構(gòu)建ComposeSource數(shù)組
List<ComposeSource> sources = reqVO.getPartNames().stream().map(name ->
ComposeSource.builder().bucket(bucketName).object(name).build()).toList();
// 設(shè)置合并參數(shù)
ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
.bucket(bucketName)
.object(finalObjectName)
.sources(sources)
.build();
// 合并文件分片
minioClient.composeObject(composeObjectArgs);
// 刪除合并后的分片
for (String partName : reqVO.getPartNames()) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(partName).build());
}
} catch (MinioException e) {
throw new IOException("Error merging file parts: " + e.getMessage(), e);
}
}
/**
* 刪除指定文件
*
* @param fileName 文件名
*/
public void deleteFile(String fileName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
MinioClient minioClient = createMinioClient();
try {
// 刪除文件
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(fileName).build());
} catch (MinioException e) {
throw new IOException("Error deleting file: " + e.getMessage(), e);
}
}
}
FileUploadController.java
@AllArgsConstructor
@RestController
@RequestMapping("/files")
public class FileUploadController {
private final FileUploadService fileUploadService;
/**
* 創(chuàng)建存儲桶
*
* @return 響應(yīng)狀態(tài)
*/
@PostMapping("/bucket")
@PermitAll
public CommonResult<String> createBucket() throws IOException, NoSuchAlgorithmException, InvalidKeyException {
fileUploadService.createBucketIfNotExists();
return CommonResult.success("創(chuàng)建成功");
}
/**
* 上傳文件分片
*
* @param fileId 文件標(biāo)識符
* @param filePart 文件分片
* @param chunkIndex 當(dāng)前分片索引
* @param totalChunks 總分片數(shù)
* @return 響應(yīng)狀態(tài)
*/
@PostMapping("/upload/part/{fileId}")
@PermitAll
public CommonResult<String> uploadFilePart(
@PathVariable String fileId,
@RequestParam String fileName,
@RequestParam MultipartFile filePart,
@RequestParam int chunkIndex,
@RequestParam int totalChunks) {
try {
// 上傳文件分片
String objectName = fileUploadService.uploadFilePart(fileId,fileName, filePart, chunkIndex, totalChunks);
return CommonResult.success("Uploaded file part: " + objectName);
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
return CommonResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Error uploading file part: " + e.getMessage());
}
}
/**
* 合并文件分片
*
* @param reqVO 參數(shù)
* @return 響應(yīng)狀態(tài)
*/
@PostMapping("/merge")
@PermitAll
public CommonResult<String> mergeFileParts(@RequestBody FileMergeReqVO reqVO) {
try {
fileUploadService.mergeFileParts(reqVO);
return CommonResult.success("File parts merged successfully.");
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
return CommonResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Error merging file parts: " + e.getMessage());
}
}
/**
* 刪除指定文件
*
* @param fileId 文件ID
* @return 響應(yīng)狀態(tài)
*/
@DeleteMapping("/delete/{fileId}")
@PermitAll
public CommonResult<String> deleteFile(@PathVariable String fileId) {
try {
fileUploadService.deleteFile(fileId);
return CommonResult.success("File deleted successfully.");
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
return CommonResult.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Error deleting file: " + e.getMessage());
}
}
}
FileMergeReqVO.java
@Data
public class FileMergeReqVO {
/**
* 文件標(biāo)識ID
*/
private String fileId;
/**
* 文件名
*/
private String fileName;
/**
* 合并文件列表
*/
@NotEmpty(message = "合并文件列表不允許為空")
private List<String> partNames;
}
2. 前端HTML頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
<style>
#progressBar {
width: 100%;
background-color: #f3f3f3;
border: 1px solid #ccc;
}
#progress {
height: 30px;
width: 0%;
background-color: #4caf50;
text-align: center;
line-height: 30px;
color: white;
}
</style>
</head>
<body>
<input type="file" id="fileInput" />
<button id="uploadButton">Upload</button>
<div id="progressBar">
<div id="progress">0%</div>
</div>
<script>
const chunkSize = 5 * 1024 * 1024; // 每個分片大小為1MB
// 生成 UUID 的函數(shù)
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
document.getElementById('uploadButton').addEventListener('click', async () => {
const file = document.getElementById('fileInput').files[0];
if (!file) {
alert("Please select a file to upload.");
return;
}
// 生成唯一的 fileId
const fileId = generateUUID();
// 獲取文件名
const fileName = file.name; // 可以直接使用文件名
const totalChunks = Math.ceil(file.size / chunkSize);
let uploadedChunks = 0;
// 上傳每個分片
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('filePart', chunk);
formData.append('fileName', fileName); // 傳遞 文件名
formData.append('fileId', fileId); // 傳遞 fileId
formData.append('chunkIndex', i + 1); // 從1開始
formData.append('totalChunks', totalChunks);
// 發(fā)送分片上傳請求
const response = await fetch('http://localhost:8080/files/upload/part/' + encodeURIComponent(fileId), {
method: 'POST',
headers: {
'tenant-id': '1',
},
body: formData,
});
if (response.ok) {
uploadedChunks++;
const progressPercentage = Math.round((uploadedChunks / totalChunks) * 100);
updateProgressBar(progressPercentage);
} else {
console.error('Error uploading chunk:', await response.text());
alert('Error uploading chunk: ' + await response.text());
break; // 如果上傳失敗,退出循環(huán)
}
}
// 合并分片
const mergeResponse = await fetch('http://localhost:8080/files/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'tenant-id': '1',
},
body: JSON.stringify({
fileId: fileId,
fileName: fileName,
partNames: Array.from({ length: totalChunks }, (_, i) => `${fileId}/${fileName}-${i + 1}`),
}),
});
if (mergeResponse.ok) {
const mergeResult = await mergeResponse.text();
console.log(mergeResult);
} else {
console.error('Error merging chunks:', await mergeResponse.text());
alert('Error merging chunks: ' + await mergeResponse.text());
}
// 最后更新進(jìn)度條為100%
updateProgressBar(100);
});
function updateProgressBar(percent) {
const progress = document.getElementById('progress');
progress.style.width = percent + '%';
progress.textContent = percent + '%';
}
</script>
</body>
</html>
注意事項:
- MinIO服務(wù)需提前啟動并創(chuàng)建好存儲桶
- 生產(chǎn)環(huán)境建議增加分片MD5校驗
- 前端需處理上傳失敗的重試機(jī)制
- 建議配置Nginx反向代理提高性能
通過本方案可實現(xiàn)穩(wěn)定的大文件上傳功能,經(jīng)測試可支持10GB以上文件傳輸,實際應(yīng)用時可根據(jù)業(yè)務(wù)需求調(diào)整分片大小和并發(fā)策略。
到此這篇關(guān)于SpringBoot集成MinIO實現(xiàn)大文件分片上傳的文章就介紹到這了,更多相關(guān)SpringBoot MinIO大文件分片上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Graceful Response 構(gòu)建 Spring Boot 響應(yīng)
Graceful Response是一個Spring Boot技術(shù)棧下的優(yōu)雅響應(yīng)處理器,提供一站式統(tǒng)一返回值封裝、全局異常處理、自定義異常錯誤碼等功能,本文介紹Graceful Response 構(gòu)建 Spring Boot 下優(yōu)雅的響應(yīng)處理,感興趣的朋友一起看看吧2024-01-01
ArrayList詳解和使用示例_動力節(jié)點Java學(xué)院整理
ArrayList 是一個數(shù)組隊列,相當(dāng)于 動態(tài)數(shù)組。與Java中的數(shù)組相比,它的容量能動態(tài)增長。接下來通過本文給大家介紹arraylist詳解和使用示例代碼,需要的的朋友一起學(xué)習(xí)吧2017-05-05
JAVA中通過Redis實現(xiàn)延時任務(wù)demo實例
Redis在2.0版本時引入了發(fā)布訂閱(pub/sub)功能,在發(fā)布訂閱中有一個channel(頻道),與消息隊列中的topic(主題)類似,可以通過redis的發(fā)布訂閱者模式實現(xiàn)延時任務(wù)功能,實例中會議室預(yù)約系統(tǒng),用戶預(yù)約管理員審核后生效,如未審批,需要自動變超期未處理,使用延時任務(wù)2024-08-08
IDEA 中創(chuàng)建Spring Data Jpa 項目的示例代碼
這篇文章主要介紹了IDEA 中創(chuàng)建Spring Data Jpa 項目的示例代碼,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04

