java springboot 大文件分片上傳處理


參考自:https://blog.csdn.net/u014150463/article/details/74044467

這里只寫后端的代碼,基本的思想就是,前端將文件分片,然后每次訪問上傳接口的時候,向后端傳入參數:當前為第幾塊文件,和分片總數

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

上傳文件實體類:

/**
 * 文件傳輸對象
 * @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.seek(param.getChunk());
//將指定的一個字節寫入文件中 127,
confAccessFile.write(Byte.MAX_VALUE);
byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
byte isComplete = Byte.MAX_VALUE;
     //這一段邏輯有點復雜,看的時候思考了好久,創建conf文件文件長度為總分片數,每上傳一個分塊即向conf文件中寫入一個127,那么沒上傳的位置就是默認的0,已上傳的就是Byte.MAX_VALUE 127
for(int i = 0; i<completeStatusList.length && isComplete==Byte.MAX_VALUE; i++){
       // 按位與運算,將&兩邊的數轉為二進制進行比較,有一個為0結果為0,全為1結果為1 eg.3&5  即 0000 0011 & 0000 0101 = 0000 0001   因此,3&5的值得1。
isComplete = (byte)(isComplete & completeStatusList[i]);
System.out.println("check part " + i + " complete?:" + completeStatusList[i]);
}
if(isComplete == Byte.MAX_VALUE){
       //如果全部文件上傳完成,刪除conf文件
       confFile.delete();
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();
        }
    }

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

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM