zoukankan      html  css  js  c++  java
  • 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();
            }
        }

      好了,到此就全部结束了,如果有疑问或批评,欢迎评论和私信,我们一起成长一起学习。

      

  • 相关阅读:
    批处理判断操作系统32位或64位,并注册服务
    VS 2008 快捷键
    VisualSVN Server导入Repository
    C++函数CString类常用函数
    委托和事件
    弄清楚类的访问符
    47-礼物的最大价值
    46-把数字翻译成字符串
    Github使用
    Hash算法相关
  • 原文地址:https://www.cnblogs.com/yueguanguanyun/p/9842652.html
Copyright © 2011-2022 走看看