SpringBoot集成MinIO實(shí)現(xiàn)大文件分片上傳的示例代碼
需求背景:為什么需要分片上傳?
1. 傳統(tǒng)上傳方式的痛點(diǎn)
在文件上傳場(chǎng)景中,當(dāng)用戶嘗試上傳超過(guò) 100MB 的大文件時(shí),傳統(tǒng)單次上傳方式會(huì)面臨三大核心問(wèn)題:
(1)網(wǎng)絡(luò)穩(wěn)定性挑戰(zhàn)
- 弱網(wǎng)環(huán)境下(如移動(dòng)網(wǎng)絡(luò)/跨國(guó)傳輸)易出現(xiàn)傳輸中斷
- 網(wǎng)絡(luò)波動(dòng)可能導(dǎo)致整個(gè)文件重傳(用戶需從0%重新開(kāi)始)
(2)服務(wù)器資源瓶頸
- 單次傳輸大文件占用大量?jī)?nèi)存(如上傳10GB文件需要預(yù)留10GB內(nèi)存)
- 長(zhǎng)時(shí)間占用線程影響服務(wù)器吞吐量
(3)用戶體驗(yàn)缺陷
- 無(wú)法顯示實(shí)時(shí)進(jìn)度條
- 不支持?jǐn)帱c(diǎn)續(xù)傳
- 失敗重試成本極高
2. 分片上傳的核心優(yōu)勢(shì)
技術(shù)價(jià)值
特性 | 說(shuō)明 |
---|---|
可靠性 | 單個(gè)分片失敗不影響整體上傳,支持分片級(jí)重試 |
內(nèi)存控制 | 分片按需加載(如5MB/片),內(nèi)存占用恒定 |
并行加速 | 支持多分片并發(fā)上傳(需配合前端Worker實(shí)現(xiàn)) |
業(yè)務(wù)價(jià)值
- 支持超大文件:可突破GB級(jí)文件上傳限制
- 斷點(diǎn)續(xù)傳:刷新頁(yè)面/切換設(shè)備后繼續(xù)上傳
- 精準(zhǔn)進(jìn)度:實(shí)時(shí)顯示每個(gè)分片的上傳狀態(tài)
- 容災(zāi)能力:分片可跨服務(wù)器分布式存儲(chǔ)
3. 典型應(yīng)用場(chǎng)景
(1)企業(yè)級(jí)網(wǎng)盤(pán)系統(tǒng)
- 用戶上傳設(shè)計(jì)圖紙(平均500MB-2GB)
- 跨國(guó)團(tuán)隊(duì)協(xié)作時(shí)處理4K視頻素材(10GB+)
(2)醫(yī)療影像系統(tǒng)
- 醫(yī)院PACS系統(tǒng)上傳CT掃描文件(單次檢查約3GB)
- 支持醫(yī)生在弱網(wǎng)環(huán)境下暫停/恢復(fù)上傳
(3)在線教育平臺(tái)
- 講師上傳高清課程視頻(1080P視頻約2GB/小時(shí))
- 學(xué)員斷網(wǎng)后自動(dòng)恢復(fù)上傳至95%進(jìn)度
4. 為什么選擇MinIO?
MinIO作為高性能對(duì)象存儲(chǔ)方案,與分片上傳架構(gòu)完美契合:
(1)分布式架構(gòu)
- 自動(dòng)將分片分布到不同存儲(chǔ)節(jié)點(diǎn)
- 支持EC糾刪碼保障數(shù)據(jù)可靠性
(2)高性能合并
// MinIO服務(wù)端合并只需一次API調(diào)用 minioClient.composeObject(ComposeObjectArgs.builder()...);
相比傳統(tǒng)文件IO合并方式,速度提升5-10倍
(3)生命周期管理
- 可配置自動(dòng)清理臨時(shí)分片
- 合并后文件自動(dòng)歸檔至冷存儲(chǔ)
一、環(huán)境準(zhǔn)備與依賴配置
1. 開(kāi)發(fā)環(huán)境要求
- JDK 17+
- Maven 3.6+
- MinIO Server(推薦版本:RELEASE.2023-10-25T06-33-25Z)
2. 項(xiàng)目依賴(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>
二、核心代碼實(shí)現(xiàn)解析
1. MinIO服務(wù)配置(FileUploadService)
(1) 客戶端初始化
private MinioClient createMinioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); }
- 通過(guò)
@Value
注入配置參數(shù) - 支持自定義endpoint和認(rèn)證信息
(2) 分片上傳實(shí)現(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()); // 清理臨時(shí)分片 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
合并分片 - 最終文件存儲(chǔ)在
merged/{fileId}
目錄 - 自動(dòng)清理已合并的分片
2. 控制層設(shè)計(jì)(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. 前端分片上傳實(shí)現(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 }) }); }
三、功能測(cè)試驗(yàn)證
測(cè)試用例1:上傳500MB視頻文件
選擇測(cè)試文件:sample.mp4
(512MB)
觀察分片上傳過(guò)程:
- 總生成103個(gè)分片(5MB/片)
- 上傳進(jìn)度實(shí)時(shí)更新
合并完成后檢查MinIO:
sta-bucket └── merged └── 6ba7b814... └── sample.mp4
下載驗(yàn)證文件完整性
測(cè)試用例2:中斷恢復(fù)測(cè)試
- 上傳過(guò)程中斷網(wǎng)絡(luò)連接
- 重新上傳時(shí):
- 已完成分片跳過(guò)上傳
- 繼續(xù)上傳剩余分片
- 最終合并成功
四、關(guān)鍵配置項(xiàng)說(shuō)明
配置項(xiàng) | 示例值 | 說(shuō)明 |
---|---|---|
minio.endpoint | http://localhost:9991 | MinIO服務(wù)器地址 |
minio.access-key | root | 訪問(wèn)密鑰 |
minio.secret-key | xxxxx | 秘密密鑰 |
minio.bucket-name | minio-xxxx | 默認(rèn)存儲(chǔ)桶名稱 |
server.servlet.port | 8080 | Spring Boot服務(wù)端口 |
附錄:完整源代碼
1. 后端核心類(lèi)
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訪問(wèn)密鑰 @Value("${minio.secret-key:xxxx}") private String secretKey; // MinIO秘密密鑰 @Value("${minio.bucket-name:minio-xxxx}") private String bucketName; // 存儲(chǔ)桶名稱 /** * 創(chuàng)建 MinIO 客戶端 * * @return MinioClient 實(shí)例 */ private MinioClient createMinioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } /** * 如果存儲(chǔ)桶不存在,則創(chuàng)建存儲(chǔ)桶 */ 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)識(shí)符 * @param filePart 文件分片 * @return 分片對(duì)象名稱 */ public String uploadFilePart(String fileId, String fileName, MultipartFile filePart, Integer chunkIndex, Integer totalChunks) throws IOException, NoSuchAlgorithmException, InvalidKeyException { MinioClient minioClient = createMinioClient(); try { // 構(gòu)建分片對(duì)象名稱 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); } } /** * 合并多個(gè)文件分片為一個(gè)完整文件 */ public void mergeFileParts(FileMergeReqVO reqVO) throws IOException, NoSuchAlgorithmException, InvalidKeyException { MinioClient minioClient = createMinioClient(); try { // 構(gòu)建最終文件對(duì)象名稱 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)建存儲(chǔ)桶 * * @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)識(shí)符 * @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)識(shí)ID */ private String fileId; /** * 文件名 */ private String fileName; /** * 合并文件列表 */ @NotEmpty(message = "合并文件列表不允許為空") private List<String> partNames; }
2. 前端HTML頁(yè)面
<!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; // 每個(gè)分片大小為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; // 上傳每個(gè)分片 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開(kāi)始 formData.append('totalChunks', totalChunks); // 發(fā)送分片上傳請(qǐng)求 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>
注意事項(xiàng):
- MinIO服務(wù)需提前啟動(dòng)并創(chuàng)建好存儲(chǔ)桶
- 生產(chǎn)環(huán)境建議增加分片MD5校驗(yàn)
- 前端需處理上傳失敗的重試機(jī)制
- 建議配置Nginx反向代理提高性能
通過(guò)本方案可實(shí)現(xiàn)穩(wěn)定的大文件上傳功能,經(jīng)測(cè)試可支持10GB以上文件傳輸,實(shí)際應(yīng)用時(shí)可根據(jù)業(yè)務(wù)需求調(diào)整分片大小和并發(fā)策略。
到此這篇關(guān)于SpringBoot集成MinIO實(shí)現(xiàn)大文件分片上傳的文章就介紹到這了,更多相關(guān)SpringBoot MinIO大文件分片上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Graceful Response 構(gòu)建 Spring Boot 響應(yīng)
Graceful Response是一個(gè)Spring Boot技術(shù)棧下的優(yōu)雅響應(yīng)處理器,提供一站式統(tǒng)一返回值封裝、全局異常處理、自定義異常錯(cuò)誤碼等功能,本文介紹Graceful Response 構(gòu)建 Spring Boot 下優(yōu)雅的響應(yīng)處理,感興趣的朋友一起看看吧2024-01-01ArrayList詳解和使用示例_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
ArrayList 是一個(gè)數(shù)組隊(duì)列,相當(dāng)于 動(dòng)態(tài)數(shù)組。與Java中的數(shù)組相比,它的容量能動(dòng)態(tài)增長(zhǎng)。接下來(lái)通過(guò)本文給大家介紹arraylist詳解和使用示例代碼,需要的的朋友一起學(xué)習(xí)吧2017-05-05JAVA中通過(guò)Redis實(shí)現(xiàn)延時(shí)任務(wù)demo實(shí)例
Redis在2.0版本時(shí)引入了發(fā)布訂閱(pub/sub)功能,在發(fā)布訂閱中有一個(gè)channel(頻道),與消息隊(duì)列中的topic(主題)類(lèi)似,可以通過(guò)redis的發(fā)布訂閱者模式實(shí)現(xiàn)延時(shí)任務(wù)功能,實(shí)例中會(huì)議室預(yù)約系統(tǒng),用戶預(yù)約管理員審核后生效,如未審批,需要自動(dòng)變超期未處理,使用延時(shí)任務(wù)2024-08-08IDEA 中創(chuàng)建Spring Data Jpa 項(xiàng)目的示例代碼
這篇文章主要介紹了IDEA 中創(chuàng)建Spring Data Jpa 項(xiàng)目的示例代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04解決springboot啟動(dòng)報(bào)錯(cuò)bean找不到的問(wèn)題
這篇文章主要介紹了解決springboot啟動(dòng)報(bào)錯(cuò)bean找不到原因,本文給大家分享完美解決方案,通過(guò)圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03Java的DelayQueue延遲隊(duì)列簡(jiǎn)單使用代碼實(shí)例
這篇文章主要介紹了Java的DelayQueue延遲隊(duì)列簡(jiǎn)單使用代碼實(shí)例,DelayQueue是一個(gè)延遲隊(duì)列,插入隊(duì)列的數(shù)據(jù)只有達(dá)到設(shè)置的延遲時(shí)間時(shí)才能被取出,否則線程會(huì)被阻塞,插入隊(duì)列的對(duì)象必須實(shí)現(xiàn)Delayed接口,需要的朋友可以參考下2023-12-12