Java的分片上傳功能的實現
起因:最近在工作中接到了一個大文件上傳下載的需求,要求將文件上傳到share盤中,下載的時候根據前端傳的不同條件對單個或多個文件進行打包并設置目錄下載。
一開始我想著就還是用老辦法直接file.transferTo(newFile)就算是大文件,我只要慢慢等總會傳上去的。
(原諒我的無知。。)后來嘗試之后發(fā)現真的是異想天開了,如果直接用普通的上傳方式基本上就會遇到以下4個問題:
- 文件上傳超時:原因是前端請求框架限制最大請求時長,后端設置了接口訪問的超時時間,或者是 nginx(或其它代理/網關) 限制了最大請求時長。
- 文件大小超限:原因在于后端對單個請求大小做了限制,一般 nginx 和 server 都會做這個限制。
- 上傳時間過久(想想10個g的文件上傳,這不得花個幾個小時的時間)
- 由于各種網絡原因上傳失敗,且失敗之后需要從頭開始。
所以我只能尋求切片上傳的幫助了。
整體思路
前端根據代碼中設置好的分片大小將上傳的文件切成若干個小文件,分多次請求依次上傳,后端再將文件碎片拼接為一個完整的文件,即使某個碎片上傳失敗,也不會影響其它文件碎片,只需要重新上傳失敗的部分就可以了。而且多個請求一起發(fā)送文件,提高了傳輸速度的上限。
(前端切片的核心是利用 Blob.prototype.slice 方法,和數組的 slice 方法相似,文件的 slice 方法可以返回原文件的某個切片)
接下來就是上代碼!
前端代碼
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- 引入 Vue --> <script src="https://cdn.jsdelivr.net/npm/vue@2.6/dist/vue.min.js"></script> <!-- 引入樣式 --> <link rel="stylesheet" rel="external nofollow" > <!-- 引入組件庫 --> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <title>分片上傳測試</title> </head> <body> <div id="app"> <template> <div> <input type="file" @change="handleFileChange" /> <el-button @click="handleUpload">上傳</el-button> </div> </template> </div> </body> </html> <script> // 切片大小 // the chunk size const SIZE = 50 * 1024 * 1024; var app = new Vue({ el: '#app', data: { container: { file: null }, data: [], fileListLong: '', fileSize:'' }, methods: { handleFileChange(e) { const [file] = e.target.files; if (!file) return; this.fileSize = file.size; Object.assign(this.$data, this.$options.data()); this.container.file = file; }, async handleUpload() { }, // 生成文件切片 createFileChunk(file, size = SIZE) { const fileChunkList = []; let cur = 0; while (cur < file.size) { fileChunkList.push({ file: file.slice(cur, cur + size) }); cur += size; } return fileChunkList; }, // 上傳切片 async uploadChunks() { const requestList = this.data .map(({ chunk, hash }) => { const formData = new FormData(); formData.append("file", chunk); formData.append("hash", hash); formData.append("filename", this.container.file.name); return { formData }; }) .map(({ formData }) => this.request({ url: "http://localhost:8080/file/upload", data: formData }) ); // 并發(fā)請求 await Promise.all(requestList); console.log(requestList.size); this.fileListLong = requestList.length; // 合并切片 await this.mergeRequest(); }, async mergeRequest() { await this.request({ url: "http://localhost:8080/file/merge", headers: { "content-type": "application/json" }, data: JSON.stringify({ fileSize: this.fileSize, fileNum: this.fileListLong, filename: this.container.file.name }) }); }, async handleUpload() { if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.data = fileChunkList.map(({ file }, index) => ({ chunk: file, // 文件名 + 數組下標 hash: this.container.file.name + "-" + index })); await this.uploadChunks(); }, request({ url, method = "post", data, headers = {}, requestList }) { return new Promise(resolve => { const xhr = new XMLHttpRequest(); xhr.open(method, url); Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]) ); xhr.send(data); xhr.onload = e => { resolve({ data: e.target.response }); }; }); } } }); </script>
考慮到方便和通用性,這里沒有用第三方的請求庫,而是用原生 XMLHttpRequest 做一層簡單的封裝來發(fā)請求
當點擊上傳按鈕時,會調用 createFileChunk 將文件切片,切片數量通過文件大小控制,這里設置 50MB,也就是說一個 100 MB 的文件會被分成 2 個 50MB 的切片
createFileChunk 內使用 while 循環(huán)和 slice 方法將切片放入 fileChunkList 數組中返回
在生成文件切片時,需要給每個切片一個標識作為 hash,這里暫時使用文件名 + 下標,這樣后端可以知道當前切片是第幾個切片,用于之后的合并切片
隨后調用 uploadChunks 上傳所有的文件切片,將文件切片,切片 hash,以及文件名放入 formData 中,再調用上一步的 request 函數返回一個 proimise,最后調用 Promise.all 并發(fā)上傳所有的切片
后端代碼
實體類
@Data public class FileUploadReq implements Serializable { ? ? private static final long serialVersionUID = 4248002065970982984L; ? ?? ?? ?//切片的文件 ? ? private MultipartFile file; ? ?? ?? ?//切片的文件名稱 ? ? private String hash; ? ?? ?? ?//原文件名稱 ? ? private ?String filename; } @Data public class FileMergeReq implements Serializable { ? ? private static final long serialVersionUID = 3667667671957596931L; ?? ? ?? ?//文件名 ? ? private String filename; ?? ?//切片數量 ? ? private int fileNum; ?? ?//文件大小 ? ? private String fileSize; }
@Slf4j @CrossOrigin @RestController @RequestMapping("/file") public class FileController { ? ? final String folderPath = System.getProperty("user.dir") + "/src/main/resources/static/file"; ? ? @RequestMapping(value = "upload", method = RequestMethod.POST) ? ? public Object upload(FileUploadReq fileUploadEntity) { ? ? ? ? File temporaryFolder = new File(folderPath); ? ? ? ? File temporaryFile = new File(folderPath + "/" + fileUploadEntity.getHash()); ? ? ? ? //如果文件夾不存在則創(chuàng)建 ? ? ? ? if (!temporaryFolder.exists()) { ? ? ? ? ? ? temporaryFolder.mkdirs(); ? ? ? ? } ? ? ? ? //如果文件存在則刪除 ? ? ? ? if (temporaryFile.exists()) { ? ? ? ? ? ? temporaryFile.delete(); ? ? ? ? } ? ? ? ? MultipartFile file = fileUploadEntity.getFile(); ? ? ? ? try { ? ? ? ? ? ? file.transferTo(temporaryFile); ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? log.error(e.getMessage()); ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? } ? ? ? ? return "success"; ? ? } ? ? @RequestMapping(value = "/merge", method = RequestMethod.POST) ? ? public Object merge(@RequestBody FileMergeReq fileMergeEntity) { ? ? ? ? String finalFilename = fileMergeEntity.getFilename(); ? ? ? ? File folder = new File(folderPath); ? ? ? ? //獲取暫存切片文件的文件夾中的所有文件 ? ? ? ? File[] files = folder.listFiles(); ? ? ? ? //合并的文件 ? ? ? ? File finalFile = new File(folderPath + "/" + finalFilename); ? ? ? ? String finalFileMainName = finalFilename.split("\\.")[0]; ? ? ? ? InputStream inputStream = null; ? ? ? ? OutputStream outputStream = null; ? ? ? ? try { ? ? ? ? ? ? outputStream = new FileOutputStream(finalFile, true); ? ? ? ? ? ? List<File> list = new ArrayList<>(); ? ? ? ? ? ? for (File file : files) { ? ? ? ? ? ? ? ? String filename = FileNameUtil.mainName(file); ? ? ? ? ? ? ? ? //判斷是否是所需要的切片文件 ? ? ? ? ? ? ? ? if (StringUtils.equals(filename, finalFileMainName)) { ? ? ? ? ? ? ? ? ? ? list.add(file); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? //如果服務器上的切片數量和前端給的數量不匹配 ? ? ? ? ? ? if (fileMergeEntity.getFileNum() != list.size()) { ? ? ? ? ? ? ? ? return "文件缺失,請重新上傳"; ? ? ? ? ? ? } ? ? ? ? ? ? //根據切片文件的下標進行排序 ? ? ? ? ? ? List<File> fileListCollect = list.parallelStream().sorted(((file1, file2) -> { ? ? ? ? ? ? ? ? String filename1 = FileNameUtil.extName(file1); ? ? ? ? ? ? ? ? String filename2 = FileNameUtil.extName(file2); ? ? ? ? ? ? ? ? return filename1.compareTo(filename2); ? ? ? ? ? ? })).collect(Collectors.toList()); ? ? ? ? ? ? //根據排序的順序依次將文件合并到新的文件中 ? ? ? ? ? ? for (File file : fileListCollect) { ? ? ? ? ? ? ? ? inputStream = new FileInputStream(file); ? ? ? ? ? ? ? ? int temp = 0; ? ? ? ? ? ? ? ? byte[] byt = new byte[2 * 1024 * 1024]; ? ? ? ? ? ? ? ? while ((temp = inputStream.read(byt)) != -1) { ? ? ? ? ? ? ? ? ? ? outputStream.write(byt, 0, temp); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? outputStream.flush(); ? ? ? ? ? ? } ? ? ? ? } catch (FileNotFoundException e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? }finally { ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? if (inputStream != null){ ? ? ? ? ? ? ? ? ? ? inputStream.close(); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? } ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? if (outputStream != null){ ? ? ? ? ? ? ? ? ? ? outputStream.close(); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? } ? ? ? ? } ? ? ? ? // 產生的文件大小和前端一開始上傳的文件不一致 ? ? ? ? if (finalFile.length() != Long.parseLong(fileMergeEntity.getFileSize())) { ? ? ? ? ? ? return "上傳文件大小不一致"; ? ? ? ? } ? ? ? ? return "上傳成功"; ? ? } }
為了圖方便我就直接return 字符串了 嘿嘿(當然我在這個demo里面寫了方法統(tǒng)一結果的封裝,所以輸出的時候還是restful風格的結果,詳細內容可以看我之前的文章《Spring使用AOP完成統(tǒng)一結果封裝》)
當前端調用upload接口的時候,后端就會將前端傳過來的文件放到一個臨時文件夾中
當調用merge接口的時候,后端就會認為分片文件已經全部上傳完畢就會進行文件合并的工作
后端主要是根據前端返回的hash值來判斷分片文件的順序
結尾
其實分片上傳聽起來好像很麻煩,其實只要把思路捋清楚了其實是不難的,是一個比較簡單的需求。
當然這個只是一個比較簡單一個demo,只是實現的一個較為簡單的分片上傳功能,像斷點上傳,上傳暫停這些功能暫時還沒來得及寫到demo里面,之后有時間了會新開一個文章寫這些額外的內容。
到此這篇關于Java的分片上傳功能的實現的文章就介紹到這了,更多相關Java 分片上傳內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringBoot創(chuàng)建RSocket服務器的全過程記錄
RSocket應用層協議支持 Reactive Streams語義, 例如:用RSocket作為HTTP的一種替代方案。這篇文章主要給大家介紹了關于SpringBoot創(chuàng)建RSocket服務器的相關資料,需要的朋友可以參考下2021-05-05elasticsearch節(jié)點間通信的基礎transport啟動過程
這篇文章主要為大家介紹了elasticsearch節(jié)點間通信的基礎transport啟動過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04IntelliJ IDEA 設置代碼提示或自動補全的快捷鍵功能
這篇文章主要介紹了IntelliJ IDEA 設置代碼提示或自動補全的快捷鍵功能,需要的朋友可以參考下2018-03-03