javaspringboot⼤⽂件分⽚上传处理
这⾥只写后端的代码,基本的思想就是,前端将⽂件分⽚,然后每次访问上传接⼝的时候,向后端传⼊参数:当前为第⼏块⽂件,和分⽚总数
下⾯直接贴代码吧,⼀些难懂的我⼤部分都加上注释了:
上传⽂件实体类:
/**
* ⽂件传输对象
* @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")
3public void fileChunkUpload(MultipartFileParam param, HttpServletResponse response, HttpServletRequest request){
4/**
5 * 判断前端Form表单格式是否⽀持⽂件上传
6*/
7boolean isMultipart = ServletFileUpload.isMultipartContent(request);
8if(!isMultipart){
9//这⾥是我向前端发送数据的代码,可理解为 return 数据; 具体的就不贴了
10 resultData = ResultData.buildFailureResult("不⽀持的表单格式", Code());
11 printJSONObject(resultData,response);
12return;
13 }
14 logger.info("上传⽂件 ");
15try {
16 String taskId = fileManage.chunkUploadByMappedByteBuffer(param);
17 } catch (IOException e) {
18 ("⽂件上传失败。{}", String());
19 }
20 logger.info("上传⽂件结束");
21 }
Service层: FileManage 我这⾥是使⽤ ---直接字节缓冲器 MappedByteBuffer 来实现分块上传,还有另外⼀种⽅法使⽤RandomAccessFile 来实现的,使⽤前者速度较快所以这⾥就直说 MappedByteBuffer 的⽅法
具体步骤如下:
第⼀步:获取RandomAccessFile,随机访问⽂件类的对象
第⼆步:调⽤RandomAccessFile的getChannel()⽅法,打开⽂件通道 FileChannel
第三步:获取当前是第⼏个分块,计算⽂件的最后偏移量
第四步:获取当前⽂件分块的字节数组,⽤于获取⽂件字节长度
第五步:使⽤⽂件通道FileChannel类的 map()⽅法创建直接字节缓冲器 MappedByteBuffer
第六步:将分块的字节数组放⼊到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b);
第七步:释放缓冲区
第⼋步:检查⽂件是否全部完成上传
如下代码:
service.impl;
bean.dto.MultipartFileParam;
ception.ServiceException;
service.IFileManage;
util.FileUtil;
util.ImageUtil;
import org.apachemons.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 {
TaskId() == null || "".TaskId())){
param.setTaskId(UUID.randomUUID().toString());
}
/
**
* basePath是我的路径,可以替换为你的
* 1:原⽂件名改为UUID
* 2:创建临时⽂件,和源⽂件⼀个路径
* 3:如果⽂件路径不存在重新创建
*/
String fileName = File().getOriginalFilename();
//fileName.substring(fileName.lastIndexOf(".")) 这个地⽅可以直接写死写成你的上传路径
String tempFileName = TaskId() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
String filePath = basePath + ObjectType()) + "/original";
File fileDir = new File(filePath);
if(!ists()){
fileDir.mkdirs();
}
File tempFile = new File(filePath,tempFileName);
//第⼀步
RandomAccessFile raf = new RandomAccessFile(tempFile,"rw");
//第⼆步
FileChannel fileChannel = Channel();
//第三步
long offset = Chunk() * Size();
//第四步
byte[] fileData = File().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 (!ists() || toBeRenamed.isDirectory()) {
return false;
}
String p = Parent();
File newFile = new File(p + File.separatorChar + toFileNewName);
//修改⽂件名
ameTo(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.ChunkTotal());
//设置起始偏移量
confAccessFile.Chunk());
//将指定的⼀个字节写⼊⽂件中 127,
confAccessFile.write(Byte.MAX_VALUE);
byte[] completeStatusList = adFileToByteArray(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 = (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 = Class().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) {
<("clean MappedByteBuffer error", e);
}
logger.info("clean MappedByteBuffer completed");
return null;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
好了,到此就全部结束了,如果有疑问或批评,欢迎评论和私信,我们⼀起成长⼀起学习。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论