Java大文件(jian)(jian)上傳(chuan)、分片上傳(chuan)、多文件(jian)(jian)上傳(chuan)、斷點續傳(chuan)、上傳(chuan)文件(jian)(jian)minio、分片上傳(chuan)minio等解決(jue)方案
-
上傳說明
文件上傳花樣百出,根據不同場景使用不同方案進行實現尤為必要。通常開發過程中,文件較小,直接將文件轉化為字節流上傳到服務器,但是文件較大時,用普通的方法上傳,顯然效果不是很好,當文件上傳一半中斷再次上傳時,發現需要重新開始,這種體驗不是很爽,下面介紹幾種好一點兒的上傳方式。
這(zhe)里(li)講講如(ru)何在Spring boot 編(bian)寫上傳(chuan)代碼(ma),如(ru)有問題可以在下留言,我并在文章末尾附上Java上傳(chuan)源碼(ma)供(gong)大家下載。
-
- 分片上傳
分片上傳,就是將所要上傳的文件,按照一定的大小,將整個文件分
隔成多個數據塊(我們稱之為Part)來進行分別上傳,上傳完之后再
由服(fu)務端對所有上傳的(de)文件進(jin)行匯總整合成原始的(de)文件。
-
- 斷點續傳
斷點續傳是在下載/上傳時,將下載/上傳任務(一個文件或一個壓縮
包)人為的劃分為幾個部分,每一個部分采用一個線程進行上傳/下載,
如果碰到網絡故障,可以從已經上傳/下載的部分開始繼續上傳/下載
未完成的(de)部分,而沒有必要從(cong)頭(tou)開始上傳/下載。
-
Redis啟動安裝
Redis安裝包分為 Windows 版和 Linux 版:
Windows版下載地址://github.com/microsoftarchive/redis/releases
Linux版下載地址: //download.redis.io/releases/
我當前使用(yong)的Windows版本:

-
minio下載啟動
windows版本可以參考我之前的文檔:window10安(an)(an)裝minio_minio windows安(an)(an)裝-CSDN博客
啟動會提示:

以上(shang)是密碼(ma)設置問題需要修改(gai)如下:
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
啟動成功后會輸出相應地址
-
上傳后端Java代碼
后(hou)端采用(yong)Spring boot項目結構,主要代碼如下:
1 /** 2 * 單(dan)文件上傳 3 * 直接將傳(chuan)入的文件通過io流形式直接寫入(服務器(qi))指定路徑下(xia) 4 * 5 * @param file 上傳的文件 6 * @return 7 */ 8 @Override 9 public ResultEntity<Boolean> singleFileUpload(MultipartFile file) { 10 //實際情況下,這些(xie)路徑都應該(gai)是服務器上面存儲文件的(de)路徑 11 String filePath = System.getProperty("user.dir") + "\\file\\"; 12 File dir = new File(filePath); 13 if (!dir.exists()) dir.mkdir(); 14 15 if (file == null) { 16 return ResultEntity.error(false, "上傳文件為空!"); 17 } 18 InputStream fileInputStream = null; 19 FileOutputStream fileOutputStream = null; 20 try { 21 String filename = file.getOriginalFilename(); 22 fileOutputStream = new FileOutputStream(filePath + filename); 23 fileInputStream = file.getInputStream(); 24 25 byte[] buf = new byte[1024 * 8]; 26 int length; 27 while ((length = fileInputStream.read(buf)) != -1) {//讀取fis文件輸入字節流里(li)面的數據 28 fileOutputStream.write(buf, 0, length);//通(tong)過fos文(wen)件輸出字(zi)節(jie)流寫出去 29 } 30 log.info("單文件上傳完成!文件路徑:{},文件名:{},文件大小:{}", filePath, filename, file.getSize()); 31 return ResultEntity.success(true, "單文件上傳完成!"); 32 } catch (IOException e) { 33 return ResultEntity.error(true, "單文件上傳失敗!"); 34 } finally { 35 try { 36 if (fileOutputStream != null) { 37 fileOutputStream.close(); 38 fileOutputStream.flush(); 39 } 40 if (fileInputStream != null) { 41 fileInputStream.close(); 42 } 43 } catch (Exception e) { 44 e.printStackTrace(); 45 } 46 } 47 } 48 49 /** 50 * 多文(wen)件上傳 51 * 直接(jie)將傳入的多(duo)個(ge)文件(jian)通過io流形式直接(jie)寫入(服務器)指定路徑下(xia) 52 * 寫(xie)入(ru)指定路(lu)徑(jing)下是(shi)通過多(duo)線程進(jin)行(xing)文件(jian)寫(xie)入(ru)的,文件(jian)寫(xie)入(ru)線程執行(xing)功能就和上(shang)面單文件(jian)寫(xie)入(ru)是(shi)一樣的 53 * 54 * @param files 上(shang)傳的所有文件 55 * @return 56 */ 57 @Override 58 public ResultEntity<Boolean> multipleFileUpload(MultipartFile[] files) { 59 //實際(ji)情況下,這些路徑都(dou)應該(gai)是服務器上面存(cun)儲文件的(de)路徑 60 String filePath = System.getProperty("user.dir") + "\\file\\"; 61 File dir = new File(filePath); 62 if (!dir.exists()) dir.mkdir(); 63 64 if (files.length == 0) { 65 return ResultEntity.error(false, "上傳文件為空!"); 66 } 67 ArrayList<String> uploadFiles = new ArrayList<>(); 68 try { 69 70 ArrayList<Future<String>> futures = new ArrayList<>(); 71 //使用多線(xian)程來(lai)完成對每個文件的寫入 72 for (MultipartFile file : files) { 73 futures.add(partMergeTask.submit(new MultipleFileTaskExecutor(filePath, file))); 74 } 75 76 //這里主要用(yong)于監(jian)聽各個文件寫入(ru)線程是否執行結(jie)束 77 int count = 0; 78 while (count != futures.size()) { 79 for (Future<String> future : futures) { 80 if (future.isDone()) { 81 uploadFiles.add(future.get()); 82 count++; 83 } 84 } 85 Thread.sleep(1); 86 } 87 log.info("多文件上傳完成!文件路徑:{},文件信息:{}", filePath, uploadFiles); 88 return ResultEntity.success(true, "多文件上傳完成!"); 89 } catch (Exception e) { 90 log.error("多文件分片上傳失敗!", e); 91 return ResultEntity.error(true, "多文件上傳失敗!"); 92 } 93 94 } 95 96 /** 97 * 單文件分片上傳(chuan) 98 * 直接將傳入(ru)的文件(jian)分(fen)片通過(guo)io流形式寫入(ru)(服務器(qi))指定臨時(shi)路徑下 99 * 然后判斷(duan)是否分片都(dou)上傳完成(cheng),如果所有分片都(dou)上傳完成(cheng)的話,就把臨時路徑(jing)下的分片文件(jian)通過(guo)流形式讀入合并并從新寫入到(dao)(服務器(qi))指(zhi)定文件(jian)路徑(jing)下 100 * 最后刪除臨(lin)(lin)時文件(jian)(jian)和臨(lin)(lin)時文件(jian)(jian)夾,臨(lin)(lin)時文件(jian)(jian)夾是(shi)通過文件(jian)(jian)的uuid進行(xing)命(ming)名的 101 * 102 * @param filePart 分(fen)片文件 103 * @param partIndex 當前(qian)分片值 104 * @param partNum 所有分片數(shu) 105 * @param fileName 當前(qian)文件名稱 106 * @param fileUid 當(dang)前文件uuid 107 * @return 108 */ 109 @Override 110 public ResultEntity<Boolean> singleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) { 111 //實際情況下,這(zhe)些路徑(jing)都應(ying)該是服務(wu)器上面存儲文件的路徑(jing) 112 String filePath = System.getProperty("user.dir") + "\\file\\";//文(wen)件存(cun)放(fang)路(lu)徑 113 String tempPath = filePath + "temp\\" + fileUid;//臨時文件存放路徑 114 File dir = new File(tempPath); 115 if (!dir.exists()) dir.mkdirs(); 116 117 //生成一個臨時文(wen)件(jian)名(ming) 118 String tempFileNamePath = tempPath + "\\" + fileName + "_" + partIndex + ".part"; 119 try { 120 //將分(fen)片存儲到臨時文(wen)件(jian)夾中 121 filePart.transferTo(new File(tempFileNamePath)); 122 123 File tempDir = new File(tempPath); 124 File[] tempFiles = tempDir.listFiles(); 125 126 one: 127 if (partNum.equals(Objects.requireNonNull(tempFiles).length)) { 128 //需(xu)要校驗一下,表示已(yi)有異步程序正(zheng)在合并了;如(ru)果是(shi)分布式(shi)這個校驗可以加入redis的(de)分布式(shi)鎖來完成(cheng) 129 if (isMergePart.get(fileUid) != null) { 130 break one; 131 } 132 isMergePart.put(fileUid, tempFiles.length); 133 System.out.println("所有分片上傳完成,預計總分片:" + partNum + "; 實際總分片:" + tempFiles.length); 134 135 FileOutputStream fileOutputStream = new FileOutputStream(filePath + fileName); 136 //這里如果分片很(hen)多的情(qing)況(kuang)下,可以采用多線程來執(zhi)行(xing) 137 for (int i = 0; i < partNum; i++) { 138 //讀取(qu)分(fen)片數據,進行分(fen)片合(he)并 139 FileInputStream fileInputStream = new FileInputStream(tempPath + "\\" + fileName + "_" + i + ".part"); 140 byte[] buf = new byte[1024 * 8];//8MB 141 int length; 142 while ((length = fileInputStream.read(buf)) != -1) {//讀取fis文件輸入字節流(liu)里面的數據 143 fileOutputStream.write(buf, 0, length);//通過(guo)fos文(wen)件(jian)輸出字節流寫(xie)出去 144 } 145 fileInputStream.close(); 146 } 147 fileOutputStream.flush(); 148 fileOutputStream.close(); 149 150 // 刪(shan)除臨時(shi)文(wen)件夾里面的分片文(wen)件 如果使用流操作且沒有關(guan)閉輸入流,可能(neng)導致刪(shan)除失敗 151 for (int i = 0; i < partNum; i++) { 152 boolean delete = new File(tempPath + "\\" + fileName + "_" + i + ".part").delete(); 153 File file = new File(tempPath + "\\" + fileName + "_" + i + ".part"); 154 } 155 //在刪除對(dui)應的臨時文件夾 156 if (Objects.requireNonNull(tempDir.listFiles()).length == 0) { 157 tempDir.delete(); 158 } 159 isMergePart.remove(fileUid); 160 } 161 162 } catch (Exception e) { 163 log.error("單文件分片上傳失敗!", e); 164 return ResultEntity.error(false, "單文件分片上傳失敗"); 165 } 166 //通過返(fan)回成功(gong)的(de)分片值(zhi),來驗證(zheng)分片是否(fou)有丟失 167 return ResultEntity.success(true, partIndex.toString()); 168 } 169 170 /** 171 * 多文件分片(pian)上(shang)傳 172 * 先(xian)將所(suo)有文(wen)件(jian)分(fen)片讀入到(服務器)指定臨時路徑下(xia),每個文(wen)件(jian)的(de)(de)分(fen)片文(wen)件(jian)的(de)(de)臨時文(wen)件(jian)夾都是已(yi)文(wen)件(jian)的(de)(de)uuid進行命名(ming)的(de)(de) 173 * 然后判斷對(dui)已(yi)經上傳所有分片的文(wen)件進行(xing)(xing)合并(bing),此(ci)處是通過多線(xian)程對(dui)每一個文(wen)件的分片文(wen)件進行(xing)(xing)合并(bing)的 174 * 最后(hou)對已經(jing)合(he)并完成的(de)分片(pian)臨時文件和文件夾(jia)進行刪除 175 * 176 * @param filePart 分(fen)片(pian)文件 177 * @param partIndex 當前分片(pian)值 178 * @param partNum 總分片數 179 * @param fileName 當(dang)前文件(jian)名稱 180 * @param fileUid 當前文(wen)件uuid 181 * @return 182 */ 183 @Override 184 public ResultEntity<String> multipleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) { 185 //實際情況下(xia),這些路(lu)徑都應該(gai)是服(fu)務器上面存儲文件(jian)的路(lu)徑 186 String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路徑 187 String tempPath = filePath + "temp\\" + fileUid;//臨時文件存放路徑 188 File dir = new File(tempPath); 189 if (!dir.exists()) dir.mkdirs(); 190 //生成一個臨時文件名 191 String tempFileNamePath = tempPath + "\\" + fileName + "_" + partIndex + ".part"; 192 try { 193 filePart.transferTo(new File(tempFileNamePath)); 194 195 File tempDir = new File(tempPath); 196 File[] tempFiles = tempDir.listFiles(); 197 //如果臨時文件(jian)夾中分(fen)(fen)片數(shu)(shu)量和實際分(fen)(fen)片數(shu)(shu)量一致的時候,就需要進(jin)行(xing)分(fen)(fen)片合(he)并(bing) 198 one: 199 if (partNum.equals(tempFiles.length)) { 200 //需(xu)要校(xiao)驗一下,表示已有異步程(cheng)序正在合(he)并了;如果是分(fen)布式這個校(xiao)驗可以(yi)加入(ru)redis的分(fen)布式鎖來完成 201 if (isMergePart.get(fileUid) != null) { 202 break one; 203 } 204 isMergePart.put(fileUid, tempFiles.length); 205 System.out.println(fileName + ":所有分片上傳完成,預計總分片:" + partNum + "; 實際總分片:" + tempFiles.length); 206 207 //使用多線程來完成對(dui)每個文件的合并 208 Future<Integer> submit = partMergeTask.submit(new PartMergeTaskExecutor(filePath, tempPath, fileName, partNum)); 209 System.out.println("上傳文件名:" + fileName + "; 總大小:" + submit.get()); 210 isMergePart.remove(fileUid); 211 } 212 } catch (Exception e) { 213 log.error("{}:多文件分片上傳失敗!", fileName, e); 214 return ResultEntity.error("", "多文件分片上傳失敗"); 215 } 216 //通過返回成功的分片(pian)值(zhi),來驗證分片(pian)是(shi)否有丟失(shi) 217 return ResultEntity.success(partIndex.toString(), fileUid); 218 } 219 220 /** 221 * 多文件(jian)(分片)秒(miao)傳(chuan) 222 * 通過對比已有的文(wen)件分片md5值和需要(yao)上傳(chuan)文(wen)件分片的MD5值, 223 * 在(zai)文(wen)件(jian)分片合并(bing)的(de)時候(hou),對已有的(de)文(wen)件(jian)進行地(di)址索引,對沒(mei)有的(de)文(wen)件(jian)進行臨時文(wen)件(jian)寫入 224 * 最后合并的時候根據不同(tong)的文件(jian)分片進行文件(jian)讀取寫入 225 * 226 * @param filePart 上(shang)傳沒有的分(fen)片文件 227 * @param fileInfo 當前分片文件相關信息 228 * @param fileOther 已(yi)存在文件分片相關信息 229 * @return 230 */ 231 @Override 232 public ResultEntity<String> multipleFilePartFlashUpload(MultipartFile filePart, String fileInfo, String fileOther) { 233 DiskFileIndexVo upFileInfo = JSONObject.parseObject(fileInfo, DiskFileIndexVo.class); 234 List<DiskFileIndexVo> notUpFileInfoList = JSON.parseArray(fileOther, DiskFileIndexVo.class); 235 //實(shi)際情況下,這些路(lu)徑都應該是服務器上面存儲(chu)文件(jian)的(de)路(lu)徑 236 String filePath = System.getProperty("user.dir") + "\\file\\";//文件存(cun)放(fang)路徑 237 //正常情況下(xia)(xia),這(zhe)(zhe)個臨時(shi)文(wen)(wen)件(jian)也應(ying)該放入(服(fu)務(wu)器)非臨時(shi)文(wen)(wen)件(jian)夾中,這(zhe)(zhe)樣方便(bian)下(xia)(xia)次(ci)其他文(wen)(wen)件(jian)上(shang)傳查(cha)找是否曾經(jing)上(shang)傳過類似的 238 //當前demo是單(dan)獨存放在(zai)臨時文件(jian)夾中(zhong),文件(jian)合并(bing)完成之后直接刪(shan)除的 239 String tempPath = filePath + "temp\\" + upFileInfo.getFileUid();//臨時(shi)文(wen)件存放路徑 240 241 File dir = new File(tempPath); 242 if (!dir.exists()) dir.mkdirs(); 243 //生成一個臨時文件名 244 String tempFileNamePath = tempPath + "\\" + upFileInfo.getFileName() + "_" + upFileInfo.getPartIndex() + ".part"; 245 246 try { 247 filePart.transferTo(new File(tempFileNamePath)); 248 249 File tempDir = new File(tempPath); 250 File[] tempFiles = tempDir.listFiles(); 251 notUpFileInfoList = notUpFileInfoList.stream().filter(e -> 252 upFileInfo.getFileUid().equals(e.getFileUid())).collect(Collectors.toList()); 253 //如果臨時文(wen)件夾中(zhong)分(fen)片數量和(he)實際分(fen)片數量一致的時候,就需要(yao)進(jin)行分(fen)片合并 254 one: 255 if ((upFileInfo.getPartNum() - notUpFileInfoList.size()) == tempFiles.length) { 256 //需(xu)要校驗一(yi)下(xia),表示已有異(yi)步程序正在合(he)并了;如果是分布式這個校驗可(ke)以加入redis的(de)分布式鎖來完(wan)成(cheng) 257 if (isMergePart.get(upFileInfo.getFileUid()) != null) { 258 break one; 259 } 260 isMergePart.put(upFileInfo.getFileUid(), tempFiles.length); 261 System.out.println(upFileInfo.getFileName() + ":所有分片上傳完成,預計總分片:" + upFileInfo.getPartNum() 262 + "; 實際總分片:" + tempFiles.length + "; 已存在分片數:" + notUpFileInfoList.size()); 263 264 //使(shi)用多線程(cheng)來完成對每個(ge)文件的合并 265 Future<Integer> submit = partMergeTask.submit( 266 new PartMergeFlashTaskExecutor(filePath, upFileInfo, notUpFileInfoList)); 267 isMergePart.remove(upFileInfo.getFileUid()); 268 } 269 } catch (Exception e) { 270 log.error("{}:多文件(分片)秒傳失敗!", upFileInfo.getFileName(), e); 271 return ResultEntity.error("", "多文件(分片)秒傳失敗!"); 272 } 273 //通過返(fan)回成功的分(fen)片(pian)(pian)值,來驗證分(fen)片(pian)(pian)是否(fou)有丟失 274 return ResultEntity.success(upFileInfo.getPartIndex().toString(), upFileInfo.getFileUid()); 275 } 276 277 /** 278 * 根據傳入需(xu)要上傳的(de)(de)文件片段的(de)(de)md5值(zhi)(zhi)來對比服務器中的(de)(de)文件的(de)(de)md5值(zhi)(zhi),將已有對應的(de)(de)md5值(zhi)(zhi)的(de)(de)文件過濾出(chu)來, 279 * 通(tong)知前端或(huo)者自行出(chu)來(lai)這些文(wen)(wen)件(jian),即(ji)為不(bu)需要上(shang)傳的文(wen)(wen)件(jian)分片,并將已有的文(wen)(wen)件(jian)分片地址索引返回給前端進行出(chu)來(lai) 280 * 281 * @param upLoadFileListMd5 原本需要上(shang)傳文件的索(suo)引分片信息 282 * @return 283 */ 284 @Override 285 public ResultEntity<List<DiskFileIndexVo>> checkDiskFile(List<DiskFileIndexVo> upLoadFileListMd5) { 286 List<DiskFileIndexVo> notUploadFile; 287 try { 288 //后端(duan)服(fu)務(wu)器已經存在的分片md5值集合 289 List<DiskFileIndexVo> diskFileMd5IndexList = diskFileIndexVos; 290 291 notUploadFile = upLoadFileListMd5.stream().filter(uf -> diskFileMd5IndexList.stream().anyMatch( 292 df -> { 293 if (df.getFileMd5().equals(uf.getFileMd5())) { 294 uf.setFileIndex(df.getFileName());//不需(xu)要上傳文件的地址索引 295 return true; 296 } 297 return false; 298 })).collect(Collectors.toList()); 299 log.info("過濾出不需要上傳的文件分片:{}", notUploadFile); 300 } catch (Exception e) { 301 log.error("上傳文件檢測異常!", e); 302 return ResultEntity.error("上傳文件檢測異常!"); 303 } 304 return ResultEntity.success(notUploadFile); 305 } 306 307 /** 308 * 根據文件uuid(md5生成的(de))來判斷此文件在服務(wu)器中是否未(wei)上傳完(wan)整, 309 * 如(ru)果沒(mei)上傳完整,則(ze)返(fan)回(hui)相(xiang)關上傳進度等信息 310 * 311 * @param pointFileIndexVo 312 * @return 313 */ 314 @Override 315 public ResultEntity<PointFileIndexVo> checkUploadFileIndex(PointFileIndexVo pointFileIndexVo) { 316 try { 317 List<String> list = uploadProgress.get(pointFileIndexVo.getFileMd5()); 318 if (list == null) list = new ArrayList<>(); 319 pointFileIndexVo.setParts(list); 320 System.out.println("已上傳部分:" + list); 321 return ResultEntity.success(pointFileIndexVo); 322 } catch (Exception e) { 323 log.error("上傳文件檢測異常!", e); 324 return ResultEntity.error("上傳文件檢測異常!"); 325 } 326 } 327 328 /** 329 * 單(dan)文件(分片(pian))斷點(dian)上傳 330 * 331 * @param filePart 需要上傳的分片文件 332 * @param fileInfo 當前需要上傳的分(fen)片文(wen)件(jian)信息,如uuid,文(wen)件(jian)名,文(wen)件(jian)總分(fen)片數量等 333 * @return 334 */ 335 @Override 336 public ResultEntity<String> singleFilePartPointUpload(MultipartFile filePart, String fileInfo) { 337 PointFileIndexVo pointFileIndexVo = JSONObject.parseObject(fileInfo, PointFileIndexVo.class); 338 //實際情況(kuang)下,這些路徑都(dou)應該是服務器上面存(cun)儲(chu)文(wen)件的路徑 339 String filePath = System.getProperty("user.dir") + "\\file\\";//文件(jian)存放路徑 340 String tempPath = filePath + "temp\\" + pointFileIndexVo.getFileMd5();//臨時文件存放路徑 341 File dir = new File(tempPath); 342 if (!dir.exists()) dir.mkdirs(); 343 344 //生成一個臨時文(wen)件名 345 String tempFileNamePath = tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + pointFileIndexVo.getPartIndex() + ".part"; 346 try { 347 //將分片存儲到(dao)臨時文件夾(jia)中 348 filePart.transferTo(new File(tempFileNamePath)); 349 350 List<String> partIndex = uploadProgress.get(pointFileIndexVo.getFileMd5()); 351 if (Objects.isNull(partIndex)) { 352 partIndex = new ArrayList<>(); 353 } 354 partIndex.add(pointFileIndexVo.getPartIndex().toString()); 355 uploadProgress.put(pointFileIndexVo.getFileMd5(), partIndex); 356 357 File tempDir = new File(tempPath); 358 File[] tempFiles = tempDir.listFiles(); 359 360 one: 361 if (pointFileIndexVo.getPartNum().equals(Objects.requireNonNull(tempFiles).length)) { 362 //需(xu)要校驗一下(xia),表(biao)示已有(you)異步程序正在合并了;如果是分布(bu)式(shi)這(zhe)個校驗可以加入(ru)redis的分布(bu)式(shi)鎖來完成 363 if (isMergePart.get(pointFileIndexVo.getFileMd5()) != null) { 364 break one; 365 } 366 isMergePart.put(pointFileIndexVo.getFileMd5(), tempFiles.length); 367 System.out.println("所有分片上傳完成,預計總分片:" + pointFileIndexVo.getPartNum() + "; 實際總分片:" + tempFiles.length); 368 //讀取(qu)分片數據(ju),進行分片合并 369 FileOutputStream fileOutputStream = new FileOutputStream(filePath + pointFileIndexVo.getFileName()); 370 //這里如果分片很多(duo)的情況下,可以(yi)采用多(duo)線程來執行 371 for (int i = 0; i < pointFileIndexVo.getPartNum(); i++) { 372 FileInputStream fileInputStream = new FileInputStream(tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + i + ".part"); 373 byte[] buf = new byte[1024 * 8];//8MB 374 int length; 375 while ((length = fileInputStream.read(buf)) != -1) {//讀(du)取fis文件(jian)輸入字節(jie)流里(li)面的數據 376 fileOutputStream.write(buf, 0, length);//通(tong)過fos文件輸出字(zi)節流寫出去 377 } 378 fileInputStream.close(); 379 } 380 fileOutputStream.flush(); 381 fileOutputStream.close(); 382 383 // 刪除臨(lin)時文件(jian)夾(jia)里面的分片文件(jian) 如果使用(yong)流操作(zuo)且沒有關閉輸入流,可能(neng)導致刪除失敗 384 for (int i = 0; i < pointFileIndexVo.getPartNum(); i++) { 385 boolean delete = new File(tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + i + ".part").delete(); 386 File file = new File(tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + i + ".part"); 387 } 388 //在刪除(chu)對應的臨時文件夾 389 if (Objects.requireNonNull(tempDir.listFiles()).length == 0) { 390 tempDir.delete(); 391 } 392 isMergePart.remove(pointFileIndexVo.getFileMd5()); 393 uploadProgress.remove(pointFileIndexVo.getFileMd5()); 394 } 395 396 } catch (Exception e) { 397 log.error("單文件分片上傳失敗!", e); 398 return ResultEntity.error(pointFileIndexVo.getFileMd5(), "單文件分片上傳失敗"); 399 } 400 //通過(guo)返回成功的分片值,來(lai)驗證分片是否有丟失 401 return ResultEntity.success(pointFileIndexVo.getFileMd5(), pointFileIndexVo.getPartIndex().toString()); 402 } 403 404 /** 405 * 獲取(服務器)指定文件存(cun)儲(chu)路徑下所有(you)文件MD5值 406 * 實際情(qing)況下,每(mei)一(yi)個文(wen)件(jian)的(de)md5值都(dou)是單獨(du)保(bao)存在數據庫或者其他存儲機制(zhi)中的(de), 407 * 不需(xu)要每次都去讀(du)取(qu)文(wen)件然(ran)后獲取(qu)md5值(zhi),這樣多次io讀(du)取(qu)很耗性能 408 * 409 * @return 410 * @throws Exception 411 */ 412 @Bean 413 private List<DiskFileIndexVo> getDiskFileMd5Index() throws Exception { 414 String filePath = System.getProperty("user.dir") + "\\file\\part\\"; 415 File saveFileDir = new File(filePath); 416 if (!saveFileDir.exists()) saveFileDir.mkdirs(); 417 418 List<DiskFileIndexVo> diskFileIndexVoList = new ArrayList<>(); 419 File[] listFiles = saveFileDir.listFiles(); 420 if (listFiles == null) return diskFileIndexVoList; 421 422 for (File listFile : listFiles) { 423 String fileName = listFile.getName(); 424 FileInputStream fileInputStream = new FileInputStream(filePath + fileName); 425 String md5DigestAsHex = DigestUtils.md5DigestAsHex(fileInputStream); 426 427 DiskFileIndexVo diskFileIndexVo = new DiskFileIndexVo(); 428 diskFileIndexVo.setFileName(fileName); 429 diskFileIndexVo.setFileMd5(md5DigestAsHex); 430 diskFileIndexVoList.add(diskFileIndexVo); 431 fileInputStream.close(); 432 } 433 434 diskFileIndexVos = diskFileIndexVoList; 435 log.info("服務器磁盤所有文件 {}", diskFileIndexVoList); 436 return diskFileIndexVoList; 437 }
代碼結構圖:

-
前端代碼
整體的過程如下
前端將文件按照百分比進行計算,每次上傳文件的百分之一(文件分片),給文件分片做上序號及文件uuid,然后在循環里面對文件片段上傳的時候在將當前分片值一起傳給后端。
后端將前端每次上傳的文件,放入到緩存目錄;
前端將全部的文件內容都上傳完畢后,發送一個合并請求;
后端合并分片的之后對文件進行命名保存;
后端保存(cun)臨時(shi)分片的時(shi)候命(ming)名索引,方便合并(bing)的時(shi)候按照分片索引進(jin)行(xing)合并(bing);
vue模板代碼:
1 <!-- 單文(wen)件分片上傳 --> 2 <div class="fileUploadStyle"> 3 <h3>單文件分片上傳</h3> 4 <el-upload ref="upload" name="files" action="#" :on-change="selectSinglePartFile" 5 :on-remove="removeSingleFilePart" :file-list="singleFilePart.fileList" :auto-upload="false"> 6 <el-button slot="trigger" size="small" type="primary">選取文件</el-button> 7 <el-button style="margin-left: 10px;" size="small" type="success" 8 @click="singleFilePartUpload">點擊進行單文件分片上傳</el-button> 9 <div slot="tip" class="el-upload__tip">主要用于測試單文件分片上傳</div> 10 </el-upload> 11 <el-progress :text-inside="true" class="progress" :stroke-width="26" :percentage="singlePartFileProgress" /> 12 </div> 13 <!-- 多(duo)文(wen)件分(fen)片上傳 --> 14 <div class="fileUploadStyle"> 15 <h3>多文件分片上傳</h3> 16 <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFile" 17 :on-remove="removeMultiplePartFile" :file-list="multipleFilePart.fileList" :auto-upload="false"> 18 <el-button slot="trigger" size="small" type="primary">選取文件</el-button> 19 <el-button style="margin-left: 10px;" size="small" type="success" 20 @click="multipleFilePartUpload">點擊進行多文件分片上傳</el-button> 21 <div slot="tip" class="el-upload__tip">主要用于測試多文件分片上傳</div> 22 </el-upload> 23 </div> 24 <!-- 多文(wen)件(分片)秒傳 --> 25 <div class="fileUploadStyle"> 26 <h3>多文件(分片MD5值)秒傳</h3> 27 <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFlashFile" 28 :on-remove="removeMultiplePartFlashFile" :file-list="multipleFilePartFlash.fileList" :auto-upload="false"> 29 <el-button slot="trigger" size="small" type="primary">選取文件</el-button> 30 <el-button style="margin-left: 10px;" size="small" type="success" 31 @click="multipleFilePartFlashUpload">點擊進行文件秒傳</el-button> 32 <div slot="tip" class="el-upload__tip">主要用于測試多文件(分片MD5值)秒傳</div> 33 </el-upload> 34 </div>
js屬性定義:

上(shang)傳部分代碼:

minio分片上傳:

上傳樣式(shi):

-
功能演示及源碼
部分演示圖: 這里就(jiu)以上(shang)(shang)傳minio為例,測試(shi)上(shang)(shang)傳minio以分片方(fang)式上(shang)(shang)

以8M進行分切(qie) 28M剛好分了四個區,我們使(shi)用redis客戶工具查(cha)看

最后成功上傳到minio中

而且(qie)看(kan)到上(shang)傳文件大小為(wei):28M
文件上(shang)傳(chuan)代(dai)碼其實(shi)功能也簡(jian)單也很明確,先將一個(ge)(ge)大(da)文件分(fen)(fen)成n個(ge)(ge)小文件,然(ran)后給后端檢(jian)測這(zhe)些(xie)分(fen)(fen)片(pian)(pian)(pian)是否曾(ceng)經(jing)上(shang)傳(chuan)中斷過,即(ji)對這(zhe)些(xie)分(fen)(fen)片(pian)(pian)(pian)進行過濾(lv)出來,并(bing)將過濾(lv)出對應的分(fen)(fen)片(pian)(pian)(pian)定位值結果返回(hui)給前端處理出不需要上(shang)傳(chuan)的分(fen)(fen)片(pian)(pian)(pian)和需要上(shang)傳(chuan)的文件分(fen)(fen)片(pian)(pian)(pian),這(zhe)里主要還(huan)是區分(fen)(fen)到確定是這(zhe)個(ge)(ge)文件的分(fen)(fen)區文件。
這(zhe)里,為了(le)方便大家直接能夠使(shi)用(yong)(yong)Java源(yuan)碼,本文所(suo)有都采用(yong)(yong)Spring boot框架模(mo)式,另外使(shi)用(yong)(yong)了(le)第三方插(cha)件(jian),如果大家使(shi)用(yong)(yong)中(zhong)沒有使(shi)用(yong)(yong)到minio可(ke)以不需要引入(ru)并把相(xiang)關(guan)代(dai)碼移除即可(ke),代(dai)碼使(shi)用(yong)(yong)了(le)redis作(zuo)為分(fen)區(qu)數(shu)量緩存(cun),相(xiang)對于Java內(nei)存(cun)更穩(wen)定些。
demo源碼下載gitee地址(代碼包含Java后端工程和vue2前端工程):

根據不同場景使用不同方案進行實現尤為必要。通常開發過程中,文件較小,直接將文件轉化為字節流上傳到服務器,但是文件較大時,用普通的方法上傳,顯然效果不是很好,當文件上傳一半中斷再次上傳時,發現需要重新開始,這種體驗不是很爽,下面介紹幾種好一點兒的上傳方式。
這里講講如何在Spring boot 編寫上傳代碼,如有問題可以在下留言,我并在文章末尾附上Java上傳源碼供大家下載