Spring Boot 分片上傳檔案
背景
最近好幾個專案在執行過程中客戶都提出檔案上傳大小的限制能否設定的大一些,使用者經常需要上傳好幾個G的資料檔案,如圖紙,視訊等,並且需要在上傳大檔案過程中進行優化實時展現進度條,進行技術評估後針對框架檔案上傳進行擴充套件升級,擴充套件介面支援大檔案分片上傳處理,減少伺服器瞬時的記憶體壓力,同一個檔案上傳失敗後可以從成功上傳分片位置進行斷點續傳,檔案上傳成功後再次上傳無需等待達到秒傳的效果,優化使用者互動體驗,具體的實現流程如下圖所示(
java fhadmin.cn
)
檔案MD5計算
對於檔案md5的計算我們使用spark-md5第三方庫,大檔案我們可以分片分別計算再合併節省時間,但是經測試1G檔案計算MD5需要20s左右的時間,所以經過優化我們抽取檔案部分特徵資訊(檔案第一片+檔案最後一片+檔案修改時間),來保證檔案的相對唯一性,只需要2s左右,大大提高前端計算效率,對於前端檔案內容塊的讀取我們需要使用html5的api中fileReader.readAsArrayBuffer方法,因為是非同步觸發,封裝的方法提供一個回撥函式進行使用
檔案分片切割
我們通過定義好檔案分片大小,使用blob物件支援的file.slice方法切割檔案,分片上傳請求需要同步按順序請求,因為使用了同步請求,前端ui會阻塞無法點選,需要開啟worker執行緒進行操作,完成後通過postMessage方法傳遞訊息給主頁面通知ui進度條的更新,需要注意的是,worker執行緒方法不支援window物件,所以儘量不要使用第三方庫,使用原生的XMLHttpRequest物件發起請求,需要的引數通過onmessage方法傳遞獲取
頁面upload請求方法如下
worker.js執行方法如下
檔案分片接收
前端檔案分片處理完畢後,接下來我們詳細介紹下後端檔案接受處理的方案,分片處理需要支援使用者隨時中斷上傳與檔案重複上傳,我們新建表f_attachchunk來記錄檔案分片的詳細資訊,表結構設計如下
- FileMD5:檔案MD5唯一標識檔案
- FileName:檔名稱
- ChunkSize:分片大小
- ChunkCount:分片總數量
- ChunkIndex:分片對應序號
- ChunkFilePath:分片儲存路徑(本地儲存檔案方案使用)
- UploadUserGuid:上傳人主鍵
- UploadUserName:上傳人姓名
- UploadDate:上傳人日期
- UploadOSSID:分片上傳批次ID(雲端儲存方案使用)
- UploadOSSChunkInfo:分片上傳單片資訊(雲端儲存方案使用)
- ChunkType:分片儲存方式(本地儲存,阿里雲,華為雲,Minio標識)
- MergeStatus:分片合併狀態(未合併,已合併)
檔案分片儲存後端一共分為三步,檢查分片=》儲存分片=》合併分片,我們這裡先以本地檔案儲存為例講解,雲端儲存思路一致,後續會提供對應使用的api方法
檢查分片
檢查分片以資料庫檔案分片記錄的FIleMD5與ChunkIndex組合來確定分片的唯一性,因為本地分片temp檔案是作為臨時檔案儲存,可能會出現手動清除施放磁碟空間的問題,所以資料庫存在記錄我們還需要對應的檢查實際檔案情況
儲存分片
儲存分片分為兩塊,檔案儲存到本地,成功後資料庫插入對應分片資訊
合併分片
在上傳分片方法中,如果當前分片是最後一片,上傳完畢後進行檔案合併工作,同時進行資料庫合併狀態的更新,下一次同一個檔案上傳時我們可以直接拷貝之前合併過的檔案作為新附件,減少合併這一步驟的I/O操作,合併檔案我們採用BufferedOutputStream與BufferedInputStream兩個物件,固定緩衝區大小
//java fhadmin.cn public void merageFile(String fileMD5, String targetFilePath) throws Exception { String merageFilePath = frameConfig.getAttachChunkPath()+"/"+fileMD5+"/"+fileMD5+".temp"; File merageFile = new File(merageFilePath); if(!merageFile.exists()){ BufferedOutputStream destOutputStream = new BufferedOutputStream(new FileOutputStream(merageFilePath)); List<AttachChunkDO> attachChunkDOList = attachChunkService.selectListByFileMD5(fileMD5, "Local"); for (AttachChunkDO attachChunkDO : attachChunkDOList) { File file = new File(attachChunkDO.getChunkFilePath()); byte[] fileBuffer = new byte[1024 * 1024 * 5];//檔案讀寫快取 int readBytesLength = 0; //每次讀取位元組數 BufferedInputStream sourceInputStream = new BufferedInputStream(new FileInputStream(file)); while ((readBytesLength = sourceInputStream.read(fileBuffer)) != -1) { destOutputStream.write(fileBuffer, 0, readBytesLength); } sourceInputStream.close(); } destOutputStream.flush(); destOutputStream.close(); } FileUtil.copyFile(merageFilePath,targetFilePath); }
雲檔案分片上傳
雲檔案上傳與本地檔案上傳的區別就是,分片檔案直接上傳到雲端,再呼叫雲端儲存api進行檔案合併與檔案拷貝,資料庫相關記錄與檢查差異不大
阿里雲OSS
上傳分片前需要生成該檔案的分片上傳組標識uploadid
//java fhadmin.cn public String getUplaodOSSID(String key){ key = "chunk/" + key + "/" + key; TenantParams.attach appConfig = getAttach(); OSSClient ossClient = InitOSS(appConfig); String bucketName = appConfig.getBucketname_auth(); InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key); InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request); String uploadId = upresult.getUploadId(); ossClient.shutdown(); return uploadId; }
上傳分片時需要指定uploadid,同時我們要將返回的分片資訊PartETag序列化儲存資料庫,用於後續的檔案合併
//java fhadmin.cn public String uploadChunk(InputStream stream,String key, int chunkIndex, int chunkSize, String uploadId){ key = "chunk/" + key + "/" + key; String result = ""; try{ TenantParams.attach appConfig = getAttach(); OSSClient ossClient = InitOSS(appConfig); String bucketName = appConfig.getBucketname_auth(); UploadPartRequest uploadPartRequest = new UploadPartRequest(); uploadPartRequest.setBucketName(bucketName); uploadPartRequest.setKey(key); uploadPartRequest.setUploadId(uploadId); uploadPartRequest.setInputStream(stream); // 設定分片大小。除了最後一個分片沒有大小限制,其他的分片最小為100 KB。 uploadPartRequest.setPartSize(chunkSize); // 設定分片號。每一個上傳的分片都有一個分片號,取值範圍是1~10000,如果超出此範圍,OSS將返回InvalidArgument錯誤碼。 uploadPartRequest.setPartNumber(chunkIndex+1); // 每個分片不需要按順序上傳,甚至可以在不同客戶端上傳,OSS會按照分片號排序組成完整的檔案。 UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest); PartETag partETag = uploadPartResult.getPartETag(); result = JSON.toJSONString(partETag); ossClient.shutdown(); }catch (Exception e){ logger.error("OSS上傳檔案Chunk失敗:" + e.getMessage()); } return result; }
合併分片時通過傳遞儲存分片的PartETag物件陣列進行操作,為了附件獨立唯一性我們不直接使用合併後的檔案,通過api進行檔案拷貝副本使用
華為雲OBS
華為雲api與阿里雲api大致相同,只有個別引數名稱不同,直接上程式碼
//java fhadmin.cn public String getUplaodOSSID(String key) throws Exception { key = "chunk/" + key + "/" + key; TenantParams.attach appConfig = getAttach(); ObsClient obsClient = InitOBS(appConfig); String bucketName = appConfig.getBucketname_auth(); InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key); InitiateMultipartUploadResult result = obsClient.initiateMultipartUpload(request); String uploadId = result.getUploadId(); obsClient.close(); return uploadId; } public String uploadChunk(InputStream stream, String key, int chunkIndex, int chunkSize, String uploadId) { key = "chunk/" + key + "/" + key; String result = ""; try { TenantParams.attach appConfig = getAttach(); ObsClient obsClient = InitOBS(appConfig); String bucketName = appConfig.getBucketname_auth(); UploadPartRequest uploadPartRequest = new UploadPartRequest(); uploadPartRequest.setBucketName(bucketName); uploadPartRequest.setUploadId(uploadId); uploadPartRequest.setObjectKey(key); uploadPartRequest.setInput(stream); uploadPartRequest.setOffset(chunkIndex * chunkSize); // 設定分片大小。除了最後一個分片沒有大小限制,其他的分片最小為100 KB。 uploadPartRequest.setPartSize((long) chunkSize); // 設定分片號。每一個上傳的分片都有一個分片號,取值範圍是1~10000,如果超出此範圍,OSS將返回InvalidArgument錯誤碼。 uploadPartRequest.setPartNumber(chunkIndex + 1); // 每個分片不需要按順序上傳,甚至可以在不同客戶端上傳,OSS會按照分片號排序組成完整的檔案。 UploadPartResult uploadPartResult = obsClient.uploadPart(uploadPartRequest); PartEtag partETag = new PartEtag(uploadPartResult.getEtag(), uploadPartResult.getPartNumber()); result = JSON.toJSONString(partETag); obsClient.close(); } catch (Exception e) { e.printStackTrace(); logger.error("OBS上傳檔案Chunk:" + e.getMessage()); } return result; } public boolean merageFile(String uploadId, List<PartEtag> chunkInfoList, String key, AttachmentDO attachmentDO, boolean checkMerge) { key = "chunk/" + key + "/" + key; boolean result = true; try { TenantParams.attach appConfig = getAttach(); ObsClient obsClient = InitOBS(appConfig); String bucketName = appConfig.getBucketname_auth(); if (!checkMerge) { CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(bucketName, key, uploadId, chunkInfoList); obsClient.completeMultipartUpload(request); } String attachKey = getKey(attachmentDO); obsClient.copyObject(bucketName, key, bucketName, attachKey); obsClient.close(); } catch (Exception e) { e.printStackTrace(); logger.error("OBS合併檔案失敗:" + e.getMessage()); result = false; } return result; }
Minio
檔案儲存Minio應用比較廣泛,框架也同時支援了自己獨立部署的Minio檔案儲存系統,Minio沒有對應的分片上傳api支援,我們可以在上傳完分片檔案後,使用composeObject方法進行檔案的合併
//java fhadmin.cnpublic boolean uploadChunk(InputStream stream, String key, int chunkIndex) { boolean result = true; try { MinioClient minioClient = InitMinio(); String bucketName = frameConfig.getMinio_bucknetname(); PutObjectOptions option = new PutObjectOptions(stream.available(), -1); key = "chunk/" + key + "/" + key; minioClient.putObject(bucketName, key + "-" + chunkIndex, stream, option); } catch (Exception e) { logger.error("Minio上傳Chunk檔案失敗:" + e.getMessage()); result = false; } return result; } public boolean merageFile(String key, int chunkCount, AttachmentDO attachmentDO, boolean checkMerge) { boolean result = true; try { MinioClient minioClient = InitMinio(); String bucketName = frameConfig.getMinio_bucknetname(); key = "chunk/" + key + "/" + key; if (!checkMerge) { List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>(); for (int i = 0; i < chunkCount; i++) { ComposeSource composeSource = ComposeSource.builder().bucket(bucketName).object(key + "-" + i).build(); sourceObjectList.add(composeSource); } minioClient.composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(key).sources(sourceObjectList).build()); } String attachKey = getKey(attachmentDO); minioClient.copyObject( CopyObjectArgs.builder() .bucket(bucketName) .object(attachKey) .source( CopySource.builder() .bucket(bucketName) .object(key) .build()) .build()); } catch (Exception e) { logger.error("Minio合併檔案失敗:" + e.getMessage()); result = false; } return result; }
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31558068/viewspace-2849651/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Spring Boot的檔案上傳Spring Boot
- Spring Boot(十七):使用 Spring Boot 上傳檔案Spring Boot
- PHP 分片上傳檔案PHP
- 前端大檔案上傳/分片上傳前端
- Java大檔案上傳、分片上傳、多檔案上傳、斷點續傳、上傳檔案minio、分片上傳minio等解決方案Java斷點
- Spring Boot 檔案上傳與下載Spring Boot
- 使用Spring Boot實現檔案上傳功能Spring Boot
- nodeJs + js 大檔案分片上傳NodeJS
- springboot(十七):使用Spring Boot上傳檔案Spring Boot
- Spring Boot + thymeleaf 實現檔案上傳下載Spring Boot
- spring-boot-route(三)實現多檔案上傳Springboot
- Spring上傳檔案Spring
- Spring 對檔案上傳下載的支援(Spring boot實現)Spring Boot
- VUE-多檔案斷點續傳、秒傳、分片上傳Vue斷點
- JAVA實現大檔案分片上傳斷點續傳Java斷點
- .NET Core Web APi大檔案分片上傳研究WebAPI
- JavaScript+PHP實現影片檔案分片上傳JavaScriptPHP
- Spring Boot+Vue 檔案上傳,如何攜帶令牌資訊?Spring BootVue
- Spring Boot 2.x(十六):玩轉vue檔案上傳Spring BootVue
- 大檔案傳輸解決方案:分片上傳 / 下載限速
- 無外掛實現大檔案分片上傳,斷點續傳斷點
- spring boot 圖片上傳Spring Boot
- 使用webuploader元件實現大檔案分片上傳,斷點續傳Web元件斷點
- 使用Spring實現上傳檔案Spring
- Spring mvc檔案上傳實現SpringMVC
- spring webflux檔案上傳下載SpringWebUX
- spring4.3.5檔案上傳功能Spring
- Node分片上傳和OSS上傳
- 在Spring Boot程式中上傳和下載檔案Spring Boot
- php檔案上傳之多檔案上傳PHP
- Spring Boot + Vue 前後端分離,兩種檔案上傳方式總結Spring BootVue後端
- Spring Boot 配置檔案Spring Boot
- spring cloud feign 檔案上傳和檔案下載SpringCloud
- Spring Boot MVC 單張圖片和多張圖片上傳 和通用檔案下載Spring BootMVC
- 單個檔案上傳和批量檔案上傳
- 短影片上傳怎麼做|寫個支援分片上傳/斷點續傳/秒傳功能的檔案服務吧斷點
- 檔案上傳
- SpringMVC 單檔案上傳與多檔案上傳SpringMVC