根據不同場景使用不同方案進行實現尤為必要。通常開發過程中,文件較小,直接將文件轉化為位元組流上傳到伺服器,但是文件較大時,用普通的方法上傳,顯然效果不是很好,當文件上傳一半中斷再次上傳時,發現需要重新開始,這種體驗不是很爽,下麵介紹幾種好一點兒的上傳方式。 這裡講講如何在Spring bo... ...
-
上傳說明
文件上傳花樣百出,根據不同場景使用不同方案進行實現尤為必要。通常開發過程中,文件較小,直接將文件轉化為位元組流上傳到伺服器,但是文件較大時,用普通的方法上傳,顯然效果不是很好,當文件上傳一半中斷再次上傳時,發現需要重新開始,這種體驗不是很爽,下麵介紹幾種好一點兒的上傳方式。
這裡講講如何在Spring boot 編寫上傳代碼,如有問題可以在下留言,我併在文章末尾附上Java上傳源碼供大家下載。
-
- 分片上傳
分片上傳,就是將所要上傳的文件,按照一定的大小,將整個文件分
隔成多個數據塊(我們稱之為Part)來進行分別上傳,上傳完之後再
由服務端對所有上傳的文件進行彙總整合成原始的文件。
-
- 斷點續傳
斷點續傳是在下載/上傳時,將下載/上傳任務(一個文件或一個壓縮
包)人為的劃分為幾個部分,每一個部分採用一個線程進行上傳/下載,
如果碰到網路故障,可以從已經上傳/下載的部分開始繼續上傳/下載
未完成的部分,而沒有必要從頭開始上傳/下載。
-
Redis啟動安裝
Redis安裝包分為 Windows 版和 Linux 版:
Windows版下載地址:https://github.com/microsoftarchive/redis/releases
Linux版下載地址: https://download.redis.io/releases/
我當前使用的Windows版本:
-
minio下載啟動
windows版本可以參考我之前的文檔:window10安裝minio_minio windows安裝-CSDN博客
啟動會提示:
以上是密碼設置問題需要修改如下:
set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678
啟動成功後會輸出相應地址
-
上傳後端Java代碼
後端採用Spring boot項目結構,主要代碼如下:
1 /** 2 * 單文件上傳 3 * 直接將傳入的文件通過io流形式直接寫入(伺服器)指定路徑下 4 * 5 * @param file 上傳的文件 6 * @return 7 */ 8 @Override 9 public ResultEntity<Boolean> singleFileUpload(MultipartFile file) { 10 //實際情況下,這些路徑都應該是伺服器上面存儲文件的路徑 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文件輸入位元組流裡面的數據 28 fileOutputStream.write(buf, 0, length);//通過fos文件輸出位元組流寫出去 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 * 多文件上傳 51 * 直接將傳入的多個文件通過io流形式直接寫入(伺服器)指定路徑下 52 * 寫入指定路徑下是通過多線程進行文件寫入的,文件寫入線程執行功能就和上面單文件寫入是一樣的 53 * 54 * @param files 上傳的所有文件 55 * @return 56 */ 57 @Override 58 public ResultEntity<Boolean> multipleFileUpload(MultipartFile[] files) { 59 //實際情況下,這些路徑都應該是伺服器上面存儲文件的路徑 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 //使用多線程來完成對每個文件的寫入 72 for (MultipartFile file : files) { 73 futures.add(partMergeTask.submit(new MultipleFileTaskExecutor(filePath, file))); 74 } 75 76 //這裡主要用於監聽各個文件寫入線程是否執行結束 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 * 單文件分片上傳 98 * 直接將傳入的文件分片通過io流形式寫入(伺服器)指定臨時路徑下 99 * 然後判斷是否分片都上傳完成,如果所有分片都上傳完成的話,就把臨時路徑下的分片文件通過流形式讀入合併並從新寫入到(伺服器)指定文件路徑下 100 * 最後刪除臨時文件和臨時文件夾,臨時文件夾是通過文件的uuid進行命名的 101 * 102 * @param filePart 分片文件 103 * @param partIndex 當前分片值 104 * @param partNum 所有分片數 105 * @param fileName 當前文件名稱 106 * @param fileUid 當前文件uuid 107 * @return 108 */ 109 @Override 110 public ResultEntity<Boolean> singleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) { 111 //實際情況下,這些路徑都應該是伺服器上面存儲文件的路徑 112 String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路徑 113 String tempPath = filePath + "temp\\" + fileUid;//臨時文件存放路徑 114 File dir = new File(tempPath); 115 if (!dir.exists()) dir.mkdirs(); 116 117 //生成一個臨時文件名 118 String tempFileNamePath = tempPath + "\\" + fileName + "_" + partIndex + ".part"; 119 try { 120 //將分片存儲到臨時文件夾中 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 //需要校驗一下,表示已有非同步程式正在合併了;如果是分散式這個校驗可以加入redis的分散式鎖來完成 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 //這裡如果分片很多的情況下,可以採用多線程來執行 137 for (int i = 0; i < partNum; i++) { 138 //讀取分片數據,進行分片合併 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文件輸入位元組流裡面的數據 143 fileOutputStream.write(buf, 0, length);//通過fos文件輸出位元組流寫出去 144 } 145 fileInputStream.close(); 146 } 147 fileOutputStream.flush(); 148 fileOutputStream.close(); 149 150 // 刪除臨時文件夾裡面的分片文件 如果使用流操作且沒有關閉輸入流,可能導致刪除失敗 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 //在刪除對應的臨時文件夾 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 //通過返回成功的分片值,來驗證分片是否有丟失 167 return ResultEntity.success(true, partIndex.toString()); 168 } 169 170 /** 171 * 多文件分片上傳 172 * 先將所有文件分片讀入到(伺服器)指定臨時路徑下,每個文件的分片文件的臨時文件夾都是已文件的uuid進行命名的 173 * 然後判斷對已經上傳所有分片的文件進行合併,此處是通過多線程對每一個文件的分片文件進行合併的 174 * 最後對已經合併完成的分片臨時文件和文件夾進行刪除 175 * 176 * @param filePart 分片文件 177 * @param partIndex 當前分片值 178 * @param partNum 總分片數 179 * @param fileName 當前文件名稱 180 * @param fileUid 當前文件uuid 181 * @return 182 */ 183 @Override 184 public ResultEntity<String> multipleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) { 185 //實際情況下,這些路徑都應該是伺服器上面存儲文件的路徑 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 //如果臨時文件夾中分片數量和實際分片數量一致的時候,就需要進行分片合併 198 one: 199 if (partNum.equals(tempFiles.length)) { 200 //需要校驗一下,表示已有非同步程式正在合併了;如果是分散式這個校驗可以加入redis的分散式鎖來完成 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 //使用多線程來完成對每個文件的合併 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 //通過返回成功的分片值,來驗證分片是否有丟失 217 return ResultEntity.success(partIndex.toString(), fileUid); 218 } 219 220 /** 221 * 多文件(分片)秒傳 222 * 通過對比已有的文件分片md5值和需要上傳文件分片的MD5值, 223 * 在文件分片合併的時候,對已有的文件進行地址索引,對沒有的文件進行臨時文件寫入 224 * 最後合併的時候根據不同的文件分片進行文件讀取寫入 225 * 226 * @param filePart 上傳沒有的分片文件 227 * @param fileInfo 當前分片文件相關信息 228 * @param fileOther 已存在文件分片相關信息 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 //實際情況下,這些路徑都應該是伺服器上面存儲文件的路徑 236 String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路徑 237 //正常情況下,這個臨時文件也應該放入(伺服器)非臨時文件夾中,這樣方便下次其他文件上傳查找是否曾經上傳過類似的 238 //當前demo是單獨存放在臨時文件夾中,文件合併完成之後直接刪除的 239 String tempPath = filePath + "temp\\" + upFileInfo.getFileUid();//臨時文件存放路徑 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 //如果臨時文件夾中分片數量和實際分片數量一致的時候,就需要進行分片合併 254 one: 255 if ((upFileInfo.getPartNum() - notUpFileInfoList.size()) == tempFiles.length) { 256 //需要校驗一下,表示已有非同步程式正在合併了;如果是分散式這個校驗可以加入redis的分散式鎖來完成 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 //使用多線程來完成對每個文件的合併 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 //通過返回成功的分片值,來驗證分片是否有丟失 274 return ResultEntity.success(upFileInfo.getPartIndex().toString(), upFileInfo.getFileUid()); 275 } 276 277 /** 278 * 根據傳入需要上傳的文件片段的md5值來對比伺服器中的文件的md5值,將已有對應的md5值的文件過濾出來, 279 * 通知前端或者自行出來這些文件,即為不需要上傳的文件分片,並將已有的文件分片地址索引返回給前端進行出來 280 * 281 * @param upLoadFileListMd5 原本需要上傳文件的索引分片信息 282 * @return 283 */ 284 @Override 285 public ResultEntity<List<DiskFileIndexVo>> checkDiskFile(List<DiskFileIndexVo> upLoadFileListMd5) { 286 List<DiskFileIndexVo> notUploadFile; 287 try { 288 //後端伺服器已經存在的分片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());//不需要上傳文件的地址索引 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生成的)來判斷此文件在伺服器中是否未上傳完整, 309 * 如果沒上傳完整,則返回相關上傳進度等信息 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 * 單文件(分片)斷點上傳 330 * 331 * @param filePart 需要上傳的分片文件 332 * @param fileInfo 當前需要上傳的分片文件信息,如uuid,文件名,文件總分片數量等 333 * @return 334 */ 335 @Override 336 public ResultEntity<String> singleFilePartPointUpload(MultipartFile filePart, String fileInfo) { 337 PointFileIndexVo pointFileIndexVo = JSONObject.parseObject(fileInfo, PointFileIndexVo.class); 338 //實際情況下,這些路徑都應該是伺服器上面存儲文件的路徑 339 String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路徑 340 String tempPath = filePath + "temp\\" + pointFileIndexVo.getFileMd5();//臨時文件存放路徑 341 File dir = new File(tempPath); 342 if (!dir.exists()) dir.mkdirs(); 343 344 //生成一個臨時文件名 345 String tempFileNamePath = tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + pointFileIndexVo.getPartIndex() + ".part"; 346 try { 347 //將分片存儲到臨時文件夾中 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();