java springboot 大檔案分片上傳處理
阿新 • • 發佈:2018-12-17
這裡只寫後端的程式碼,基本的思想就是,前端將檔案分片,然後每次訪問上傳介面的時候,向後端傳入引數:當前為第幾塊問價,和分片總數
下面直接貼程式碼吧,一些難懂的我大部分都加上註釋了:
上傳檔案實體類:
/** * 檔案傳輸物件 * @ApiModel和@ApiModelProperty及Controller中@Api開頭的註解 是swagger中的註解 用於專案Api的自動生成,如果有沒接觸過的同學,可以把他理解為一個註釋 */ @ApiModel("大檔案分片入參實體") public class MultipartFileParam { @ApiModelProperty("檔案傳輸任務ID") private String taskId; @ApiModelProperty("當前為第幾分片") private int chunk; @ApiModelProperty("每個分塊的大小") private long size; @ApiModelProperty("分片總數") private int chunkTotal; @ApiModelProperty("主體型別--這個欄位是我專案中的其他業務邏輯可以忽略") private int objectType; @ApiModelProperty("分塊檔案傳輸物件") private MultipartFile file;
首先是Controller層:
1 @ApiOperation("大檔案分片上傳") 2 @PostMapping("chunkUpload") 3 public void fileChunkUpload(MultipartFileParam param, HttpServletResponse response, HttpServletRequest request){ 4 /** 5 * 判斷前端Form表單格式是否支援檔案上傳6 */ 7 boolean isMultipart = ServletFileUpload.isMultipartContent(request); 8 if(!isMultipart){ 9 //這裡是我向前端傳送資料的程式碼,可理解為 return 資料; 具體的就不貼了 10 resultData = ResultData.buildFailureResult("不支援的表單格式", ResultCodeEnum.NOTFILE.getCode()); 11 printJSONObject(resultData,response); 12 return; 13 } 14 logger.info("上傳檔案 start..."); 15 try { 16 String taskId = fileManage.chunkUploadByMappedByteBuffer(param); 17 } catch (IOException e) { 18 logger.error("檔案上傳失敗。{}", param.toString()); 19 } 20 logger.info("上傳檔案結束"); 21 }
Service層: FileManage 我這裡是使用 ---直接位元組緩衝器 MappedByteBuffer 來實現分塊上傳,還有另外一種方法使用RandomAccessFile 來實現的,使用前者速度較快所以這裡就直說 MappedByteBuffer 的方法
具體步驟如下:
第一步:獲取RandomAccessFile,隨機訪問檔案類的物件 第二步:呼叫RandomAccessFile的getChannel()方法,開啟檔案通道 FileChannel 第三步:獲取當前是第幾個分塊,計算檔案的最後偏移量 第四步:獲取當前檔案分塊的位元組陣列,用於獲取檔案位元組長度 第五步:使用檔案通道FileChannel類的 map()方法建立直接位元組緩衝器 MappedByteBuffer 第六步:將分塊的位元組陣列放入到當前位置的緩衝區內 mappedByteBuffer.put(byte[] b); 第七步:釋放緩衝區 第八步:檢查檔案是否全部完成上傳
如下程式碼:
package com.zcz.service.impl;import com.zcz.bean.dto.MultipartFileParam;import com.zcz.exception.ServiceException;import com.zcz.service.IFileManage;import com.zcz.util.FileUtil;import com.zcz.util.ImageUtil;import org.apache.commons.io.FileUtils;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import org.springframework.web.multipart.MultipartFile;import java.io.*;import java.nio.MappedByteBuffer;import java.nio.channels.FileChannel;import java.util.*;/** * 檔案上傳服務層 */@Service("fileManage")public class FileManageImpl implements IFileManage { @Value("${basePath}") private String basePath; @Value("${file-url}") private String fileUrl; /** * 分塊上傳 * 第一步:獲取RandomAccessFile,隨機訪問檔案類的物件 * 第二步:呼叫RandomAccessFile的getChannel()方法,開啟檔案通道 FileChannel * 第三步:獲取當前是第幾個分塊,計算檔案的最後偏移量 * 第四步:獲取當前檔案分塊的位元組陣列,用於獲取檔案位元組長度 * 第五步:使用檔案通道FileChannel類的 map()方法建立直接位元組緩衝器 MappedByteBuffer * 第六步:將分塊的位元組陣列放入到當前位置的緩衝區內 mappedByteBuffer.put(byte[] b); * 第七步:釋放緩衝區 * 第八步:檢查檔案是否全部完成上傳 * @param param * @return * @throws IOException */ @Override public String chunkUploadByMappedByteBuffer(MultipartFileParam param) throws IOException { if(param.getTaskId() == null || "".equals(param.getTaskId())){ param.setTaskId(UUID.randomUUID().toString()); } /** * basePath是我的路徑,可以替換為你的 * 1:原檔名改為UUID * 2:建立臨時檔案,和原始檔一個路徑 * 3:如果檔案路徑不存在重新建立 */ String fileName = param.getFile().getOriginalFilename(); //fileName.substring(fileName.lastIndexOf(".")) 這個地方可以直接寫死 寫成你的上傳路徑 String tempFileName = param.getTaskId() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp"; String filePath = basePath + getFilePathByType(param.getObjectType()) + "/original"; File fileDir = new File(filePath); if(!fileDir.exists()){ fileDir.mkdirs(); } File tempFile = new File(filePath,tempFileName); //第一步 RandomAccessFile raf = new RandomAccessFile(tempFile,"rw"); //第二步 FileChannel fileChannel = raf.getChannel(); //第三步 long offset = param.getChunk() * param.getSize(); //第四步 byte[] fileData = param.getFile().getBytes(); //第五步 MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,offset,fileData.length); //第六步 mappedByteBuffer.put(fileData); //第七步 FileUtil.freeMappedByteBuffer(mappedByteBuffer); fileChannel.close(); raf.close(); //第八步 boolean isComplete = checkUploadStatus(param,fileName,filePath); if(isComplete){ renameFile(tempFile,fileName); } return ""; } /** * 檔案重新命名 * @param toBeRenamed 將要修改名字的檔案 * @param toFileNewName 新的名字 * @return */ public boolean renameFile(File toBeRenamed, String toFileNewName) { //檢查要重新命名的檔案是否存在,是否是檔案 if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) { return false; } String p = toBeRenamed.getParent(); File newFile = new File(p + File.separatorChar + toFileNewName); //修改檔名 return toBeRenamed.renameTo(newFile); } /** * 檢查檔案上傳進度 * @return */ public boolean checkUploadStatus(MultipartFileParam param,String fileName,String filePath) throws IOException { File confFile = new File(filePath,fileName+".conf"); RandomAccessFile confAccessFile = new RandomAccessFile(confFile,"rw"); //設定檔案長度 confAccessFile.setLength(param.getChunkTotal()); //設定起始偏移量 confAccessFile.setLength(param.getChunk()); //將指定的一個位元組寫入檔案中 127, confAccessFile.write(Byte.MAX_VALUE); byte[] completeStatusList = FileUtils.readFileToByteArray(confFile); byte isComplete = Byte.MAX_VALUE; for(int i = 0; i<completeStatusList.length && isComplete==Byte.MAX_VALUE; i++){ isComplete = completeStatusList[i]; System.out.println("check part " + i + " complete?:" + completeStatusList[i]); } if(isComplete == Byte.MAX_VALUE){ return true; } return false; }
/** * 根據主體型別,獲取每個主題所對應的資料夾路徑 我專案內的需求可以忽略 * @param objectType * @return filePath 檔案路徑 */ private String getFilePathByType(Integer objectType){ //不同主體對應的資料夾 Map<Integer,String> typeMap = new HashMap<>(); typeMap.put(1,"Article"); typeMap.put(2,"Question"); typeMap.put(3,"Answer"); typeMap.put(4,"Courseware"); typeMap.put(5,"Lesson"); String objectPath = typeMap.get(objectType); if(objectPath==null || "".equals(objectPath)){ throw new ServiceException("主體型別不存在"); } return objectPath; }
}
FileUtil:
/** * 在MappedByteBuffer釋放後再對它進行讀操作的話就會引發jvm crash,在併發情況下很容易發生 * 正在釋放時另一個執行緒正開始讀取,於是crash就發生了。所以為了系統穩定性釋放前一般需要檢 查是否還有執行緒在讀或寫 * @param mappedByteBuffer */ public static void freedMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) { try { if (mappedByteBuffer == null) { return; } mappedByteBuffer.force(); AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { try { Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]); //可以訪問private的許可權 getCleanerMethod.setAccessible(true); //在具有指定引數的 方法物件上呼叫此 方法物件表示的底層方法 sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer, new Object[0]); cleaner.clean(); } catch (Exception e) { logger.error("clean MappedByteBuffer error!!!", e); } logger.info("clean MappedByteBuffer completed!!!"); return null; } }); } catch (Exception e) { e.printStackTrace(); } }
好了,到此就全部結束了,如果有疑問或批評,歡迎評論和私信,我們一起成長一起學習。