一、文件切片上传
业务需求
-
文件秒传
如果该文件曾经上传过,或者服务器存在该文件,则立即上传成功,并返回文件地址
-
断点续传
如果文件上传的过程中,因某种原因中断,则已上传的内容不在继续上传
核心内容--保存文件
-
上传流程
-
第一步:获取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);
}