java中大文件上传

CodeLee0106 2021-01-14 12:58:46
java 文件 大文件 上传 中大


java中大文件上传

1、什么是秒传
通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了.

2、本文实现的秒传核心逻辑
2.1、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,

2.2、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径
分片上传
1.什么是分片上传
分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。

2.分片上传的场景
1.大文件上传

2.网络环境环境不好,存在需要重传风险的场景

断点续传
1、什么是断点续传
断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。

2、应用场景
断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。

3、实现断点续传的核心逻辑
在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

4、实现流程步骤
a、方案一,常规步骤

将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
初始化一个分片上传任务,返回本次分片上传唯一标识;
按照一定的策略(串行或并行)发送各个分片数据块;
发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。
b、方案二、本文实现的步骤

前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小
服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)
服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。
5、分片上传/断点上传代码实现
a、前端采用百度提供的webuploader的插件,进行分片。因本文主要介绍服务端代码实现,webuploader如何进行分片,具体实现可以查看如下链接:

b、后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接:

另一种是使用MappedByteBuffer,对MappedByteBuffer不熟悉的朋友,可以查看如下链接进行了解:

后端进行写入操作的核心代码
a、RandomAccessFile实现方式

@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)
@Slf4j
public class RandomAccessUploadStrategy extends SliceUploadTemplate {
@Autowired
private FilePathUtil filePathUtil;
@Value("${upload.chunkSize}")
private long defaultChunkSize;
@Override
public boolean upload(FileUploadRequestDTO param) {
RandomAccessFile accessTmpFile = null;
try {
String uploadDirPath = filePathUtil.getPath(param);
File tmpFile = super.createTmpFile(param);
accessTmpFile = new RandomAccessFile(tmpFile, "rw");
//这个必须与前端设定的值一致
long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
: param.getChunkSize();
long offset = chunkSize * param.getChunk();
//定位到该分片的偏移量
accessTmpFile.seek(offset);
//写入该分片数据
accessTmpFile.write(param.getFile().getBytes());
boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
return isOk;
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.close(accessTmpFile);
}
return false;
}
}

b、MappedByteBuffer实现方式

@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)
@Slf4j
public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {
@Autowired
private FilePathUtil filePathUtil;
@Value("${upload.chunkSize}")
private long defaultChunkSize;
@Override
public boolean upload(FileUploadRequestDTO param) {
RandomAccessFile tempRaf = null;
FileChannel fileChannel = null;
MappedByteBuffer mappedByteBuffer = null;
try {
String uploadDirPath = filePathUtil.getPath(param);
File tmpFile = super.createTmpFile(param);
tempRaf = new RandomAccessFile(tmpFile, "rw");
fileChannel = tempRaf.getChannel();
long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
: param.getChunkSize();
//写入该分片数据
long offset = chunkSize * param.getChunk();
byte[] fileData = param.getFile().getBytes();
mappedByteBuffer = fileChannel
.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
mappedByteBuffer.put(fileData);
boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
return isOk;
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.freedMappedByteBuffer(mappedByteBuffer);
FileUtil.close(fileChannel);
FileUtil.close(tempRaf);
}
return false;
}
}

c、文件操作核心模板类代码

@Slf4j
public abstract class SliceUploadTemplate implements SliceUploadStrategy {
public abstract boolean upload(FileUploadRequestDTO param);
protected File createTmpFile(FileUploadRequestDTO param) {
FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);
param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));
String fileName = param.getFile().getOriginalFilename();
String uploadDirPath = filePathUtil.getPath(param);
String tempFileName = fileName + "_tmp";
File tmpDir = new File(uploadDirPath);
File tmpFile = new File(uploadDirPath, tempFileName);
if (!tmpDir.exists()) {
tmpDir.mkdirs();
}
return tmpFile;
}
@Override
public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {
boolean isOk = this.upload(param);
if (isOk) {
File tmpFile = this.createTmpFile(param);
FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);
return fileUploadDTO;
}
String md5 = FileMD5Util.getFileMD5(param.getFile());
Map<Integer, String> map = new HashMap<>();
map.put(param.getChunk(), md5);
return FileUploadDTO.builder().chunkMd5Info(map).build();
}
/**
* 检查并修改文件上传进度
*/
public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {
String fileName = param.getFile().getOriginalFilename();
File confFile = new File(uploadDirPath, fileName + ".conf");
byte isComplete = 0;
RandomAccessFile accessConfFile = null;
try {
accessConfFile = new RandomAccessFile(confFile, "rw");
//把该分段标记为 true 表示完成
System.out.println("set part " + param.getChunk() + " complete");
//创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127
accessConfFile.setLength(param.getChunks());
accessConfFile.seek(param.getChunk());
accessConfFile.write(Byte.MAX_VALUE);
//completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)
byte[] completeList = FileUtils.readFileToByteArray(confFile);
isComplete = Byte.MAX_VALUE;
for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
//与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
isComplete = (byte) (isComplete & completeList[i]);
System.out.println("check part " + i + " complete?:" + completeList[i]);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.close(accessConfFile);
}
boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);
return isOk;
}
/**
* 把上传进度信息存进redis
*/
private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,
String fileName, File confFile, byte isComplete) {
RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);
if (isComplete == Byte.MAX_VALUE) {
redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");
redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());
confFile.delete();
return true;
} else {
if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {
redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");
redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),
uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");
}
return false;
}
}
/**
* 保存文件操作
*/
public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {
FileUploadDTO fileUploadDTO = null;
try {
fileUploadDTO = renameFile(tmpFile, fileName);
if (fileUploadDTO.isUploadComplete()) {
System.out
.println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);
//TODO 保存文件信息到数据库
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
}
return fileUploadDTO;
}
/**
* 文件重命名
*
* @param toBeRenamed 将要修改名字的文件
* @param toFileNewName 新的名字
*/
private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {
//检查要重命名的文件是否存在,是否是文件
FileUploadDTO fileUploadDTO = new FileUploadDTO();
if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
log.info("File does not exist: {}", toBeRenamed.getName());
fileUploadDTO.setUploadComplete(false);
return fileUploadDTO;
}
String ext = FileUtil.getExtension(toFileNewName);
String p = toBeRenamed.getParent();
String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;
File newFile = new File(filePath);
//修改文件名
boolean uploadFlag = toBeRenamed.renameTo(newFile);
fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());
fileUploadDTO.setUploadComplete(uploadFlag);
fileUploadDTO.setPath(filePath);
fileUploadDTO.setSize(newFile.length());
fileUploadDTO.setFileExt(ext);
fileUploadDTO.setFileId(toFileNewName);
return fileUploadDTO;
}
}

在实现分片上传的过程,需要前端和后端配合,比如前后端的上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用fastdfs、hdfs等。

本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的md5值计算,后端写入的速度还是比较快。如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网:

阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。

文末提供一个oss表单上传的链接demo,通过oss表单上传,可以直接从前端把文件上传到oss服务器,把上传的压力都推给oss服务器:

版权声明
本文为[CodeLee0106]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/codeLee0106/p/14276588.html

  1. 基于Jmeter实现Rocketmq消息发送
  2. Java advanced (29) -- HashMap set
  3. Weblogic 2017-3248 analysis of Java Security
  4. 基于Jmeter实现Rocketmq消息发送
  5. Kubernetes official java client 8: fluent style
  6. Explain the function of thread pool and how to use it in Java
  7. Java reflection & dynamic agent
  8. 我的程序员之路:自学Java篇
  9. Comparative analysis of common sorting algorithms of data structure
  10. Application of linear table of data structure
  11. JavaScript
  12. Fast integration of imsdk and Huawei offline push
  13. Fast integration of imsdk and Huawei offline push
  14. 我的程序员之路:自学Java篇
  15. 一网打尽,一文讲通虚拟机VirtualBox及Linux使用
  16. The architecture of MySQL
  17. . net cloud native architect training camp
  18. 一网打尽,一文讲通虚拟机VirtualBox及Linux使用
  19. Remember to use it once Asp.Net The development process of core webapi 5.0 + dapper + MySQL + redis + docker
  20. Java Concurrent Programming points
  21. RBAC authorization mode of k8s
  22. Description of dependency problem after javacv is updated to 1.5. X and how to reduce the size of dependency package
  23. How does serverless deal with the resource supply demand of k8s in the offline scenario
  24. Detailed explanation of HBase basic principle
  25. 【Java练手项目】Java身份证号码识别系统
  26. Spring security oauth2.0 authentication and authorization 4: distributed system authentication and authorization
  27. Serverless 如何應對 K8s 在離線場景下的資源供給訴求
  28. HBase 底層原理詳解(深度好文,建議收藏)
  29. 詳解執行緒池的作用及Java中如何使用執行緒池
  30. Kubernetes官方java客戶端之八:fluent style
  31. Three main problems and solutions of redis
  32. Springboot test redis connection, after starting a variety of error solutions
  33. Installing and configuring redis under Linux
  34. Implementation of fastdfs upload file demo -- springboot
  35. Collation of MySQL Cluster Knowledge Points
  36. Installation of SVN under Linux
  37. Weblogic 2017-3248 analysis of Java Security
  38. Spring Festival in the year of the ox, the most useless year
  39. Crawler series | 2, HTTP request and response, write crawler these contents must master
  40. JQuery table plug-in DataTables
  41. Spring boot static resource configuration principle (step by step source analysis, detailed and easy to understand)
  42. 400万Docker镜像中,51%的镜像存在高危漏洞
  43. Head first design pattern -- 10. Iterator pattern
  44. A few pictures, take down the HTTPS
  45. Simple use of pyecharts module
  46. [azure redis cache] discussion on the functionality of azure redis
  47. Installation of SVN under Linux
  48. Sorting out knowledge points of MySQL Cluster
  49. rocketmq-cpp-client Visual Studio 2019 编译
  50. rocketmq-cpp-client Visual Studio 2019 编译
  51. RBAC authorization mode of k8s
  52. Remember to use it once Asp.Net The development process of core webapi 5.0 + dapper + MySQL + redis + docker
  53. Java Concurrent Programming points
  54. Explain Java I / O flow in detail
  55. Linux system builds springboot project environment and deploys it
  56. Easy to understand JS object-oriented, by the way understand prototype and__ proto__
  57. Summary of java basic knowledge
  58. . net cloud native architect training camp (module 2 basic consolidation rabbitmq mastransit detailed explanation) - learning notes
  59. The architecture of MySQL
  60. MySQL security management, database maintenance and performance improvement