文章
问答
冒泡
文件切片上传

一、文件切片上传

业务需求
  • 文件秒传

    如果该文件曾经上传过,或者服务器存在该文件,则立即上传成功,并返回文件地址

  • 断点续传

    如果文件上传的过程中,因某种原因中断,则已上传的内容不在继续上传

核心内容--保存文件
  1. 上传流程

    • 第一步:获取RandomAccessFile,随机访问文件类的对象

    • 第二步:调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel

    • 第三步:获取当前是第几个分块,计算文件的最后偏移量

    • 第四步:获取当前文件分块的字节数组,用于获取文件字节长度

    • 第五步:使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer

    • 第六步:将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b);

    • 第七步:释放缓冲区

  2. 实现过程

    @Resource
    private Semaphore semaphore;

    /**
     * MultipartFileDto multipartFile 参数
     * chunkNumber:当前切片序号
     * chunkSize:切片大小
     * currentChunkSize:当前切片大小
     * totalSize:文件总大小
     * identifier:切片的唯一标识,MD5
     * fileName:文件名
     * relativePath:临时文件名
     * totalChunks:总切片数
     * file:文件
     */
    public void saveFile(MultipartFileDto multipartFile) throws IOException {
       MappedByteBuffer mappedByteBuffer = null;
       FileChannel fileChannel = null;
       RandomAccessFile randomAccessFile = null;
       try {
           // 获取信号量
           semaphore.acquire();
           String filePath = "c:/" + multipartFile.getRelativePath();
           // 第一步 获取RandomAccessFile,随机访问文件类的对象
           randomAccessFile = new RandomAccessFile(filePath, "rw");
           // 第二步 调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
           fileChannel = randomAccessFile.getChannel();
           // 第三步 获取当前是第几个分块,计算文件的最后偏移量
           long offset = (multipartFile.getChunkNumber() - 1) * multipartFile.getChunkSize();
           // 第四步 获取当前文件分块的字节数组,用于获取文件字节长度
           byte[] fileData = multipartFile.getFile().getBytes();
           // 第五步 使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer
           mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
           // 第六步 将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b)
           mappedByteBuffer.put(fileData);
      } catch (InterruptedException e) {
           throw new RuntimeException(e);
      } finally {
           // 第七步 释放缓冲区
           freeMappedByteBuffer(mappedByteBuffer);
           if (fileChannel != null) {
               fileChannel.close();
          }
           if (randomAccessFile != null) {
               randomAccessFile.close();
          }
           // 释放信号量
           semaphore.release();
      }
    }

    /**
     * 用于释放MappedByteBuffer
     */
    private void freeMappedByteBuffer(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();
                  }
                   return null;
              }
          });
      } catch (Exception e) {
           e.printStackTrace();
      }
    }

    信号量的作用:每一次请求都会开辟一个对磁盘空间操作的方法,为防止磁盘压力过大,通过信号量,控制同时对磁盘操作的个数

功能完善

mongodb

判断文件是否上传完成的过程中,会大量的访问数据库,采用mongodb可提高访问速度

mongodb中的集合

  • MultipartFilePo

    @Data
    @Document("multipart_file")
    public class MultipartFilePo implements Serializable {

       // 表示主键
       @Id
       private ObjectId id;

       // 切片的唯一标识,MD5
       @Indexed(unique = true)
       @Field("identifier")
       private String identifier;

       // 当前上传文件大小
       @Field("uploader_size")
       private Long uploaderSize;

       // 切片序号
       @Field("chunk_number")
       private Integer chunkNumber;
    }
  • CompleteFilePo

    @Data
    @Document("complete_file")
    public class CompleteFilePo implements Serializable {

       // 表示主键
       @Id
       private ObjectId id;

       // 切片的唯一标识,MD5
       @Indexed(unique = true)
       @Field("identifier")
       private String identifier;

       // 总文件大小
       @Field("total_size")
       private Long totalSize;

       // 文件切片总数
       @Field("total_chunk")
       private Integer totalChunk;

       // 是否已经全部上传
       @Field("upload_success")
       private Boolean uploadSuccess;

       // 文件地址
       @Field("url")
       private String url;
    }

具体实现过程

  • 返回vo文件

    @Data
    public class MultipartFileVo {

       // 上传记录
       private Boolean needMerge;
       // 已上传的切片
       private List<Integer> uploaded;

       // 是否秒传
       private Boolean skipUpload;
       // 秒传地址
       private String url;
    }
  1. 判断是否上传过文件

    // 判断是否上传过文件
    @Transactional
    public MultipartFileVo isUploadFile(MultipartFileDto multipartFile) {
       MultipartFileVo multipartFileVo = new MultipartFileVo();
       CompleteFilePo completeFilePo = completeRepository.findByIdentifier(multipartFile.getIdentifier());
       if (completeFilePo == null) {
           // 首次上传
           completeFilePo = new CompleteFilePo();
           completeFilePo.setIdentifier(multipartFile.getIdentifier());
           completeFilePo.setTotalSize(multipartFile.getTotalSize());
           completeFilePo.setTotalChunk(multipartFile.getTotalChunks());
           completeFilePo.setUploadSuccess(false);
           completeFilePo.setUrl(filePathDir + multipartFile.getRelativePath());
           completeRepository.insert(completeFilePo);
           multipartFileVo.setNeedMerge(false);
      } else if (!completeFilePo.getUploadSuccess()) {
           // 断点续传
           List<MultipartFilePo> multipartFilePoList = multipartRepository.findByIdentifier(multipartFile.getIdentifier());
           List<Integer> chunkIdList = multipartFilePoList.stream().map(MultipartFilePo::getChunkNumber).sorted().collect(Collectors.toList());
           multipartFileVo.setUploaded(chunkIdList);
           multipartFileVo.setNeedMerge(true);
           multipartFileVo.setSkipUpload(false);
      } else {
           // 秒传
           multipartFileVo.setSkipUpload(true);
           multipartFileVo.setUrl(completeFilePo.getUrl());
      }
       return multipartFileVo;
    }
  2. 判断文件是否上传完成

    // 判断是否上传完成
    private MultipartFileVo isUploadOver(MultipartFileDto multipartFile) {
       MultipartFileVo multipartFileVo = new MultipartFileVo();
       List<MultipartFilePo> multipartFilePoList = multipartRepository.findByIdentifier(multipartFile.getIdentifier());
       long totalChunkSize = multipartFilePoList.stream().mapToLong(MultipartFilePo::getUploaderSize).sum();
       if (totalChunkSize == multipartFile.getTotalSize()) {
           // 更新文件状态
           CompleteFilePo completeFilePo = completeRepository.findByIdentifier(multipartFile.getIdentifier());
           completeFilePo.setUploadSuccess(true);
           completeRepository.save(completeFilePo);
           // 返回前端
           multipartFileVo.setSkipUpload(true);
           multipartFileVo.setUrl(completeFilePo.getUrl());
      } else {
           multipartFileVo.setNeedMerge(true);
           multipartFileVo.setSkipUpload(false);
      }
       return multipartFileVo;
    }
  3. 保存已上传的文件信息

    // 保存上传文件
    @Transactional
    public MultipartFileVo uploadFile(MultipartFileDto multipartFileDto) {
       MultipartFilePo multipartFilePo = new MultipartFilePo();
       multipartFilePo.setIdentifier(multipartFileDto.getIdentifier());
       multipartFilePo.setUploaderSize(multipartFileDto.getCurrentChunkSize());
       multipartFilePo.setChunkNumber(multipartFileDto.getChunkNumber());
       // 将该切片信息保存到mongodb中
       multipartRepository.insert(multipartFilePo);
       return isUploadOver(multipartFileDto);
    }
Java项目总结

关于作者

Kirito
获得点赞
文章被阅读