导航:
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
目录
5.2.2 测试分块与合并,RandomAccessFile随机流
报错、Tomcat默认上传文件大小限制为1M,yml配置文件上传限制
5 上传视频
5.1 媒资管理页面上传视频流程预览
1、教学机构人员进入媒资管理列表查询自己上传的媒资文件。
点击“媒资管理”
进入媒资管理列表页面查询本机构上传的媒资文件。
2、教育机构用户在"媒资管理"页面中点击 "上传视频" 按钮。
点击“上传视频”打开上传页面
3、选择要上传的文件,自动执行文件上传。
4、视频上传成功会自动处理,处理完成可以预览视频。
5.2 断点续传技术
5.2.1 什么是断点续传
如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差。
断点续传:
在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以节省操作时间。
流程如下:
1、前端上传前先把文件分成块
2、一块一块的上传,上传中断后重新上传,已上传的分块则不用再上传
3、各分块上传完成最后在服务端合并文件
5.2.2 测试分块与合并,RandomAccessFile随机流
文件分块的流程如下:
- 1、获取源文件长度
- 2、根据设定的分块文件的大小计算出块数
- 3、从源文件读数据依次向每一个块文件写数据。
测试代码如下:
随机流RandomAccessFile:
是Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。与普通的输入/输出流不同的是,RandomAccessFile支持"随机访问"的方式,程序可以直接跳转到文件的任意地方来读写数据。
package com.xuecheng.media; /** * @description 大文件处理测试 */ public class BigFileTest { //分块测试,将视频按每块5m进行分块 @Test public void testChunk() throws IOException { //源文件 File sourceFile = new File("D:\\develop\\upload\\1.项目背景.mp4"); //分块文件存储路径。这个路径得是真实存在的,否则会报错找不到路径 String chunkFilePath = "D:\\develop\\upload\\chunk\\"; //分块文件大小。这里设置成5M int chunkSize = 1024 * 1024 * 5; //分块文件个数。Math.ceil是向上取整 int chunkNum = (int) Math.ceil(sourceFile.length() * 1.0 / chunkSize); //使用随机流从源文件读数据,向分块文件中写数据 RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r"); //缓存区 byte[] bytes = new byte[1024]; //遍历所有块 for (int i = 0; i < chunkNum; i++) { //“D:\develop\upload\chunk\1”、“D:\develop\upload\chunk\2”... File chunkFile = new File(chunkFilePath + i); //分块文件写入流 RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw"); int len = -1; //每次写满一个字节数组 while ((len=raf_r.read(bytes))!=-1){ raf_rw.write(bytes,0,len); //当分块大小超过5m时停止在这一块写数据。不加这句的话会出现第一块大小和源文件一样,其余块大小都为0 if(chunkFile.length()>=chunkSize){ break; } } raf_rw.close(); } raf_r.close(); } }
运行测试:
文件合并流程:
1、找到要合并的文件并按文件合并的先后进行排序。
2、创建合并文件
3、依次从合并的文件中读取数据向合并文件写入数
文件合并的测试代码 :
//将分块进行合并 @Test public void testMerge() throws IOException { //块文件目录 File chunkFolder = new File("D:\\develop\\upload\\chunk"); //源文件 File sourceFile = new File("D:\\develop\\upload\\1.项目背景.mp4"); //合并后的文件 File mergeFile = new File("D:\\develop\\upload\\1.项目背景_2.mp4"); //1.取出所有分块文件 File[] files = chunkFolder.listFiles(); //2.将数组转成list,以便于排序 List<File> filesList = Arrays.asList(files); //3.对分块文件排序 Collections.sort(filesList, new Comparator<File>() { @Override public int compare(File o1, File o2) { return Integer.parseInt(o1.getName())-Integer.parseInt(o2.getName()); } }); //向合并文件写的流 RandomAccessFile raf_rw = new RandomAccessFile(mergeFile, "rw"); //缓存区 byte[] bytes = new byte[1024]; //4.遍历每个分块,向合并的目标文件写 for (File file : filesList) { //读分块的流 RandomAccessFile raf_r = new RandomAccessFile(file, "r"); int len = -1; while ((len=raf_r.read(bytes))!=-1){ raf_rw.write(bytes,0,len); } raf_r.close(); } raf_rw.close(); //合并文件完成后对合并的文件md5校验 FileInputStream fileInputStream_merge = new FileInputStream(mergeFile); FileInputStream fileInputStream_source = new FileInputStream(sourceFile); String md5_merge = DigestUtils.md5Hex(fileInputStream_merge); String md5_source = DigestUtils.md5Hex(fileInputStream_source); if(md5_merge.equals(md5_source)){ System.out.println("文件合并成功"); } }
5.2.3 视频上传流程
下图是上传视频的整体流程:
1、前端对文件进行分块。
2、前端上传分块文件前请求媒资服务检查原文件和分块文件是否存在,如果已经存在则不需要再上传。
检查文件存在依据:是媒资主键为文件的md5值,两个文件md5值相等,则是一个文件。
3、如果分块文件不存在则前端开始上传
4、前端请求媒资服务上传分块。
5、媒资服务将分块上传至MinIO。
注意:minio文件和文件的分块存储路径都应该尽量避免存在根目录下,这里将文件名前两位设成路径。
6、前端将分块上传完毕请求媒资服务合并分块。
7、媒资服务判断分块上传完成则请求MinIO合并文件。
8、合并完成校验合并后的文件是否完整,如果完整则上传完成并删除分块,否则删除文件。
5.2.4 测试minio合并文件
1、将分块文件上传至minio
//将分块文件上传至minio @Test public void uploadChunk(){ String chunkFolderPath = "D:\\develop\\upload\\chunk\\"; File chunkFolder = new File(chunkFolderPath); //获取所有分块文件。listFiles()方法返回该文件路径下所有文件数组 File[] files = chunkFolder.listFiles(); //将分块文件上传至minio for (int i = 0; i < files.length; i++) { try { UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket("testbucket").object("chunk/" + i).filename(files[i].getAbsolutePath()).build(); minioClient.uploadObject(uploadObjectArgs); System.out.println("上传分块成功"+i); } catch (Exception e) { e.printStackTrace(); } } }
2、通过minio的合并文件
//合并文件,要求分块文件最小5M @Test public void test_merge() throws Exception { List<ComposeSource> sources = Stream.iterate(0, i -> ++i) .limit(6) .map(i -> ComposeSource.builder() .bucket("testbucket") .object("chunk/".concat(Integer.toString(i))) .build()) .collect(Collectors.toList()); ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder() .bucket("testbucket").object("merge01.mp4") .sources(sources).build(); minioClient.composeObject(composeObjectArgs); } //清除分块文件 @Test public void test_removeObjects(){ //合并分块完成将分块文件清除 List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i) .limit(6) .map(i -> new DeleteObject("chunk/".concat(Integer.toString(i)))) .collect(Collectors.toList()); RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("testbucket").objects(deleteObjects).build(); Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs); results.forEach(r->{ DeleteError deleteError = null; try { deleteError = r.get(); } catch (Exception e) { e.printStackTrace(); } }); }
使用minio合并文件报错:java.lang.IllegalArgumentException: source testbucket/chunk/0: size 1048576 must be greater than 5242880
minio合并文件默认分块最小5M,我们将分块改为5M再次测试。
5.3 接口定义,检查文件/分块、上传分块、合并分块
与前端的约定是操作成功返回{code:0}否则返回{code:-1}
定义接口如下:
package com.xuecheng.media.api; /** * @description 大文件上传接口 */ @Api(value = "大文件上传接口", tags = "大文件上传接口") @RestController public class BigFilesController { @ApiOperation(value = "文件上传前检查文件") @PostMapping("/upload/checkfile") public RestResponse<Boolean> checkfile( @RequestParam("fileMd5") String fileMd5 ) throws Exception { return null; } //chunk是分块序号 @ApiOperation(value = "分块文件上传前的检测") @PostMapping("/upload/checkchunk") public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception { return null; } @ApiOperation(value = "上传分块文件") @PostMapping("/upload/uploadchunk") public RestResponse uploadchunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception { return null; } @ApiOperation(value = "合并文件") @PostMapping("/upload/mergechunks") public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) throws Exception { return null; } }
5.4 上传分块Service
5.4.1 检查文件和分块
接口完成进行接口实现,首先实现检查文件方法和检查分块方法。
@Override public RestResponse<Boolean> checkFile(String fileMd5) { //查询文件信息 MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5); if (mediaFiles != null) { //桶 String bucket = mediaFiles.getBucket(); //存储目录 String filePath = mediaFiles.getFilePath(); //文件流 InputStream stream = null; try { stream = minioClient.getObject( GetObjectArgs.builder() .bucket(bucket) .object(filePath) .build()); if (stream != null) { //文件已存在 return RestResponse.success(true); } } catch (Exception e) { } } //文件不存在 return RestResponse.success(false); } @Override public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) { //得到分块文件目录 String chunkFileFolderPath = getChunkFileFolderPath(fileMd5); //得到分块文件的路径 String chunkFilePath = chunkFileFolderPath + chunkIndex; //文件流 InputStream fileInputStream = null; try { fileInputStream = minioClient.getObject( GetObjectArgs.builder() .bucket(bucket_videoFiles) .object(chunkFilePath) .build()); if (fileInputStream != null) { //分块已存在 return RestResponse.success(true); } } catch (Exception e) { } //分块未存在 return RestResponse.success(false); } //得到分块文件的目录 private String getChunkFileFolderPath(String fileMd5) { return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/"; }
5.4.2 上传分块
@Override public RestResponse uploadChunk(String fileMd5, int chunk, String localChunkFilePath) { //得到分块文件的目录路径。“abcde”->“a/b/abcde” String chunkFileFolderPath = getChunkFileFolderPath(fileMd5); //得到分块文件的路径 String chunkFilePath = chunkFileFolderPath + chunk; //获取文件类型mimeType String mimeType = getMimeType(null); //将文件存储至minIO boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_videoFiles, chunkFilePath); if (!b) { log.debug("上传分块文件失败:{}", chunkFilePath); return RestResponse.validfail(false, "上传分块失败"); } log.debug("上传分块文件成功:{}",chunkFilePath); return RestResponse.success(true); }
//根据扩展名获取mimeType private String getMimeType(String extension) { if (extension == null) { extension = ""; } //根据扩展名取出mimeType ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension); String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流 if (extensionMatch != null) { mimeType = extensionMatch.getMimeType(); } return mimeType; }
5.4.3 完善接口层
@ApiOperation(value = "文件上传前检查文件") @PostMapping("/upload/checkfile") public RestResponse<Boolean> checkfile( @RequestParam("fileMd5") String fileMd5 ) throws Exception { return mediaFileService.checkFile(fileMd5); } @ApiOperation(value = "分块文件上传前的检测") @PostMapping("/upload/checkchunk") public RestResponse<Boolean> checkchunk(@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception { return mediaFileService.checkChunk(fileMd5,chunk); } @ApiOperation(value = "上传分块文件") @PostMapping("/upload/uploadchunk") public RestResponse uploadchunk(@RequestParam("file") MultipartFile file, @RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") int chunk) throws Exception { //创建临时文件 File tempFile = File.createTempFile("minio", "temp"); //上传的文件拷贝到临时文件 file.transferTo(tempFile); //文件路径 String absolutePath = tempFile.getAbsolutePath(); return mediaFileService.uploadChunk(fileMd5,chunk,absolutePath); }
启动前端工程,进入上传视频界面进行前后端联调测试。
报错、Tomcat默认上传文件大小限制为1M,yml配置文件上传限制
minio合并的分块小于5M时会报错:
解决:
前端对文件分块的大小为5MB,SpringBoot web默认上传文件的大小限制为1MB,这里需要在nacos里media-api工程yml配置如下:
spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB
max-file-size:单个文件的大小限制
Max-request-size: 单次请求的大小限制
5.5 合并分块开发
5.5.1 service开发
业务流程 :
1.获取分块文件路径
2.合并
3.验证md5合并后的文件和源文件是否一致,从而判断是否上传成功
4.文件信息入数据库
5.清除分块文件
代码实现:
@Override public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) { //=====1.获取分块文件路径===== String chunkFileFolderPath = getChunkFileFolderPath(fileMd5); //组成将分块文件路径组成 List<ComposeSource> List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i) .limit(chunkTotal) .map(i -> ComposeSource.builder() .bucket(bucket_videoFiles) .object(chunkFileFolderPath.concat(Integer.toString(i))) .build()) .collect(Collectors.toList()); //=====2.合并===== //文件名称 String fileName = uploadFileParamsDto.getFilename(); //文件扩展名 String extName = fileName.substring(fileName.lastIndexOf(".")); //合并文件路径 String mergeFilePath = getFilePathByMd5(fileMd5, extName); try { //合并文件 ObjectWriteResponse response = minioClient.composeObject( ComposeObjectArgs.builder() .bucket(bucket_videoFiles) .object(mergeFilePath) .sources(sourceObjectList) .build()); log.debug("合并文件成功:{}",mergeFilePath); } catch (Exception e) { log.debug("合并文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e); return RestResponse.validfail(false, "合并文件失败。"); } // ====3.验证md5合并后的文件和源文件是否一致,从而判断是否上传成功==== //下载合并后的文件 File minioFile = downloadFileFromMinIO(bucket_videoFiles,mergeFilePath); if(minioFile == null){ log.debug("下载合并后文件失败,mergeFilePath:{}",mergeFilePath); return RestResponse.validfail(false, "下载合并后文件失败。"); } try (InputStream newFileInputStream = new FileInputStream(minioFile)) { //minio上文件的md5值 String md5Hex = DigestUtils.md5Hex(newFileInputStream); //比较md5值,不一致则说明文件不完整 if(!fileMd5.equals(md5Hex)){ return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。"); } //文件大小 uploadFileParamsDto.setFileSize(minioFile.length()); }catch (Exception e){ log.debug("校验文件失败,fileMd5:{},异常:{}",fileMd5,e.getMessage(),e); return RestResponse.validfail(false, "文件合并校验失败,最终上传失败。"); }finally { if(minioFile!=null){ minioFile.delete(); } } //====4.文件信息入数据库。注入自己这个bean,加“currentProxy.”主要为了让组成事务。非事务方法调用事务方法必须用代理对象调用===== // @Autowired // MediaFileService currentProxy; currentProxy.addMediaFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_videoFiles,mergeFilePath); //=====5.清除分块文件===== clearChunkFiles(chunkFileFolderPath,chunkTotal); return RestResponse.success(true); } /** * 从minio下载文件 * @param bucket 桶 * @param objectName 对象名称 * @return 下载后的文件 */ public File downloadFileFromMinIO(String bucket,String objectName){ //临时文件 File minioFile = null; FileOutputStream outputStream = null; try{ InputStream stream = minioClient.getObject(GetObjectArgs.builder() .bucket(bucket) .object(objectName) .build()); //创建临时文件 minioFile=File.createTempFile("minio", ".merge"); outputStream = new FileOutputStream(minioFile); IOUtils.copy(stream,outputStream); return minioFile; } catch (Exception e) { e.printStackTrace(); }finally { if(outputStream!=null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } /** * 得到合并后的文件的地址 * @param fileMd5 文件id即md5值 * @param fileExt 文件扩展名 * @return */ private String getFilePathByMd5(String fileMd5,String fileExt){ return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt; } /** * 清除分块文件 * @param chunkFileFolderPath 分块文件路径 * @param chunkTotal 分块文件总数 */ private void clearChunkFiles(String chunkFileFolderPath,int chunkTotal){ try { List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i) .limit(chunkTotal) .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i)))) .collect(Collectors.toList()); RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build(); Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs); results.forEach(r->{ DeleteError deleteError = null; try { deleteError = r.get(); } catch (Exception e) { e.printStackTrace(); log.error("清楚分块文件失败,objectname:{}",deleteError.objectName(),e); } }); } catch (Exception e) { e.printStackTrace(); log.error("清楚分块文件失败,chunkFileFolderPath:{}",chunkFileFolderPath,e); } }
注意:
非事务方法调用事务方法必须用代理对象调用。
所以文件信息入数据库时,要注入自己这个bean,加“currentProxy.”,而不能加“this.”,主要为了让组成事务。
5.5.2 接口层完善
@ApiOperation(value = "合并文件") @PostMapping("/upload/mergechunks") public RestResponse mergechunks(@RequestParam("fileMd5") String fileMd5, @RequestParam("fileName") String fileName, @RequestParam("chunkTotal") int chunkTotal) throws Exception { Long companyId = 1232141425L; UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto(); uploadFileParamsDto.setFileType("001002"); uploadFileParamsDto.setTags("课程视频"); uploadFileParamsDto.setRemark(""); uploadFileParamsDto.setFilename(fileName); return mediaFileService.mergechunks(companyId,fileMd5,chunkTotal,uploadFileParamsDto); }
5.5.2 合并分块测试
下边进行前后端联调:
1、上传一个视频测试合并分块的执行逻辑
进入service方法逐行跟踪。
2、断点续传测试
上传一部分后,停止刷新浏览器再重新上传,通过浏览器日志发现已经上传过的分块不再重新上传