vue-simple-uploader结合Spring boot实现文件分块上传

vue-simple-uploader结合Spring boot实现文件分块上传

vue-simple-uploader中文APi地址
前端搭建
引入依赖

npm install vue-simple-uploader --save

在main.js中全局引用

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

页面代码
测试时可以只替换里面的url

<template>
  <div class="hello">
    <uploader :key="uploader_key" :options="options" class="uploader-example"
              @file-success="onFileSuccess">
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <uploader-btn :single="true">选择文件</uploader-btn>
      </uploader-drop>
      <uploader-list></uploader-list>
    </uploader>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data(){
    return{
      uploader_key: new Date().getTime(),//这个用来刷新组件--解决不刷新页面连续上传的缓存上传数据(注:每次上传时,强制这个值进行更改---根据自己的实际情况重新赋值)
      options: {
        target: 'http://localhost:18002/ossserver/api/v1/material/chunkUpload',//SpringBoot后台接收文件夹数据的接口
        testChunks: false//是否测试分片
      }
    }
  },
  props: {
    msg: String
  },
  methods:{
    onFileSuccess: function (rootFile, file, response, chunk) {
      console.log(rootFile)
      console.log(file)
      console.log(response)
      console.log(chunk)
    }
  }
}
</script>

<style>
  .uploader-example {
    width: 90%;
    padding: 15px;
    margin: 40px auto 0;
    font-size: 12px;
    box-shadow: 0 0 10px rgba(0, 0, 0, .4);
  }

  .uploader-example .uploader-btn {
    margin-right: 4px;
  }

  .uploader-example .uploader-list {
    max-height: 440px;
    overflow: auto;
    overflow-x: hidden;
    overflow-y: auto;
  }
</style>

后端代码
首先是Param 实体类,专门对接了vue-simple-uploader的参数

package com.grandtech.oss.domain;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.springframework.web.multipart.MultipartFile;

@ApiModel("大文件分片入参实体")
public class MultipartFileParam {
  @ApiModelProperty("文件传输任务ID")
  private String taskId;

  @ApiModelProperty("当前为第几分片")
  private int chunkNumber;

  @ApiModelProperty("每个分块的大小")
  private long chunkSize;


  @ApiModelProperty("分片总数")
  private int totalChunks;
  @ApiModelProperty("文件唯一标识")
  private String identifier;


  @ApiModelProperty("分块文件传输对象")
  private MultipartFile file;

  public String getTaskId() {
    return taskId;
  }

  public void setTaskId(String taskId) {
    this.taskId = taskId;
  }

  public int getChunkNumber() {
    return chunkNumber;
  }

  public void setChunkNumber(int chunkNumber) {
    this.chunkNumber = chunkNumber;
  }

  public long getChunkSize() {
    return chunkSize;
  }

  public void setChunkSize(long chunkSize) {
    this.chunkSize = chunkSize;
  }

  public int getTotalChunks() {
    return totalChunks;
  }

  public void setTotalChunks(int totalChunks) {
    this.totalChunks = totalChunks;
  }

  public MultipartFile getFile() {
    return file;
  }

  public void setFile(MultipartFile file) {
    this.file = file;
  }

  public String getIdentifier() {
    return identifier;
  }

  public void setIdentifier(String identifier) {
    this.identifier = identifier;
  }
}
Controller 接口
注意返回的状态码,充分利用前端插件的自动重传
 @ApiOperation("大文件分片上传")
    @PostMapping("/chunkUpload")
    public void fileChunkUpload(MultipartFileParam param,  HttpServletRequest request,HttpServletResponse response){
        
        //自己的业务获取存储路径,可以换成自己的
        OSSInformation ossInformation = ossInformationService.queryOne();
        String root = ossInformation.getRoot();
        //验证文件夹规则,不能包含特殊字符
        File file = new File(root);
        createDirectoryQuietly(file);

        String path=file.getAbsolutePath();
        response.setContentType("text/html;charset=UTF-8");
        // response.setStatus对接前端插件
     //        200, 201, 202: 当前块上传成功,不需要重传。
     //        404, 415. 500, 501: 当前块上传失败,会取消整个文件上传。
     //        其他状态码: 出错了,但是会自动重试上传。

        try {
            /**
             * 判断前端Form表单格式是否支持文件上传
             */
            boolean isMultipart = ServletFileUpload.isMultipartContent(request);
            if(!isMultipart){
                //这里是我向前端发送数据的代码,可理解为 return 数据; 具体的就不贴了
                System.out.println("不支持的表单格式");
                response.setStatus(404);
                response.getOutputStream().write("不支持的表单格式".getBytes());
            }else {
            param.setTaskId(param.getIdentifier());
            materialService.chunkUploadByMappedByteBuffer(param,path);//service层
            response.setStatus(200);
            response.getWriter().print("上传成功");
            }
            response.getWriter().flush();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("上传文件失败");
            response.setStatus(415);
        }
    }

Service 实现层
中间的代码最初从网上借鉴的,最后发现很多逻辑都是错误的,而且异常太多。最后自己修改了逻辑了,替换了很多关键代码。

 @Override
    public String chunkUploadByMappedByteBuffer(MultipartFileParam param, String filePath) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        if(param.getTaskId() == null || "".equals(param.getTaskId())){
            param.setTaskId(UUID.randomUUID().toString());
        }
        /**
         *
         * 1:创建临时文件,和源文件一个路径
         * 2:如果文件路径不存在重新创建
         */
        String fileName = param.getFile().getOriginalFilename();
        String tempFileName = param.getTaskId() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
        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 position = (param.getChunkNumber()-1) * param.getChunkSize();
        //第四步
        byte[] fileData = param.getFile().getBytes();
        //第五步
        long end=position+fileData.length-1;
        fileChannel.position(position);
        fileChannel.write(ByteBuffer.wrap(fileData));
        //使用 fileChannel.map的方式速度更快,但是容易产生IO操作,无建议使用
//        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,position,fileData.length);
//        //第六步
//        mappedByteBuffer.put(fileData);
        //第七步
//        freedMappedByteBuffer(mappedByteBuffer);
//        Method method = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
//        method.setAccessible(true);
//        method.invoke(FileChannelImpl.class, mappedByteBuffer);
        fileChannel.force(true);
        fileChannel.close();
        raf.close();
        //第八步
        boolean isComplete = checkUploadStatus(param,fileName,filePath);
        if(isComplete){
            renameFile(tempFile,fileName);
        }
        return param.getTaskId();
    }

    /**
     * 文件重命名
     * @param toBeRenamed   将要修改名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    public void renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的文件是否存在,是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            System.out.println("文件不存在");
            return;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
         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.getTotalChunks());
        //设置起始偏移量
        confAccessFile.seek(param.getChunkNumber()-1);
        //将指定的一个字节写入文件中 127,
        confAccessFile.write(Byte.MAX_VALUE);
        byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
        confAccessFile.close();//不关闭会造成无法占用
        //这一段逻辑有点复杂,看的时候思考了好久,创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127
        for(int i = 0; i<completeStatusList.length; i++){
           if(completeStatusList[i]!=Byte.MAX_VALUE){
               return false;
           }
        }
          //如果全部文件上传完成,删除conf文件
          confFile.delete();
            return true;
    }

    /**
     * 在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) {
                        e.printStackTrace();
                        System.out.println("清理缓存出错!!!"+e.getMessage());
                    }
                    System.out.println("缓存清理完毕!!!");
                    return null;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

你可能感兴趣的:(springboot,java,vue)