一、文件切片上传
前端使用vue-simple-uploader组件
业务需求
-
文件秒传
如果该文件曾经上传过,或者服务器存在该文件,则立即上传成功,并返回文件地址
-
断点续传
如果文件上传的过程中,因某种原因中断,则已上传的内容不在继续上传
核心内容–保存文件
-
上传流程
- 第一步:获取RandomAccessFile,随机访问文件类的对象
- 第二步:调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
- 第三步:获取当前是第几个分块,计算文件的最后偏移量
- 第四步:获取当前文件分块的字节数组,用于获取文件字节长度
- 第五步:使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer
- 第六步:将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b);
- 第七步:释放缓冲区
-
实现过程
@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; }
-
判断是否上传过文件
// 判断是否上传过文件 @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; }
-
判断文件是否上传完成
// 判断是否上传完成 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; }
-
保存已上传的文件信息
// 保存上传文件 @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); }