1. 程式人生 > >java springboot 大檔案分片上傳處理

java springboot 大檔案分片上傳處理

這裡只寫後端的程式碼,基本的思想就是,前端將檔案分片,然後每次訪問上傳介面的時候,向後端傳入引數:當前為第幾塊問價,和分片總數

下面直接貼程式碼吧,一些難懂的我大部分都加上註釋了:

上傳檔案實體類:

/**
 * 檔案傳輸物件
 * @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();
        }
    }

  好了,到此就全部結束了,如果有疑問或批評,歡迎評論和私信,我們一起成長一起學習。