参考自:
这里只写后端的代码, 基本的思想就是, 前端将文件分片, 然后每次访问上传接口的时候, 向后端传入参数: 当前为第几块文件, 和分片总数
下面直接贴代码吧, 一些难懂的我大部分都加上注释了:
上传文件实体类:
- /**
- * 文件传输对象
- * @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 层:
- @ApiOperation("大文件分片上传")
- @PostMapping("chunkUpload")
- public void fileChunkUpload(MultipartFileParam param, HttpServletResponse response, HttpServletRequest request){
- /**
- * 判断前端 Form 表单格式是否支持文件上传
- */
- boolean isMultipart = ServletFileUpload.isMultipartContent(request);
- if(!isMultipart){
- // 这里是我向前端发送数据的代码, 可理解为 return 数据; 具体的就不贴了
- resultData = ResultData.buildFailureResult("不支持的表单格式", ResultCodeEnum.NOTFILE.getCode());
- printJSONObject(resultData,response);
- return;
- }
- logger.info("上传文件 start...");
- try {
- String taskId = fileManage.chunkUploadByMappedByteBuffer(param);
- } catch (IOException e) {
- logger.error("文件上传失败.{}", param.toString());
- }
- logger.info("上传文件结束");
- }
Service 层: FileManage 我这里是使用 --- 直接字节缓冲器 MappedByteBuffer 来实现分块上传, 还有另外一种方法使用 RandomAccessFile 来实现的, 使用前者速度较快所以这里就直说 MappedByteBuffer 的方法
具体步骤如下:
第一步: 获取 RandomAccessFile, 随机访问文件类的对象
第二步: 调用 RandomAccessFile 的 getChannel() 方法, 打开文件通道 FileChannel
第三步: 获取当前是第几个分块, 计算文件的最后偏移量
第四步: 获取当前文件分块的字节数组, 用于获取文件字节长度
第五步: 使用文件通道 FileChannel 类的 map() 方法创建直接字节缓冲器 MappedByteBuffer
第六步: 将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b);
第七步: 释放缓冲区
第八步: 检查文件是否全部完成上传
如下代码:
- package com.zcz.service.impl;
- import com.zcz.bean.dto.MultipartFileParam;
- import com.zcz.exception.ServiceException;
- import com.zcz.service.IFileManage;
- import com.zcz.util.FileUtil;
- import com.zcz.util.ImageUtil;
- import org.apache.commons.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 {
- if(param.getTaskId() == null || "".equals(param.getTaskId())){
- param.setTaskId(UUID.randomUUID().toString());
- }
- /**
- * basePath 是我的路径, 可以替换为你的
- * 1: 原文件名改为 UUID
- * 2: 创建临时文件, 和源文件一个路径
- * 3: 如果文件路径不存在重新创建
- */
- String fileName = param.getFile().getOriginalFilename();
- //fileName.substring(fileName.lastIndexOf(".")) 这个地方可以直接写死 写成你的上传路径
- String tempFileName = param.getTaskId() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
- String filePath = basePath + getFilePathByType(param.getObjectType()) + "/original";
- File fileDir = new File(filePath);
- if(!fileDir.exists()){
- fileDir.mkdirs();
- }
- File tempFile = new File(filePath,tempFileName);
- // 第一步
- RandomAccessFile raf = new RandomAccessFile(tempFile,"rw");
- // 第二步
- FileChannel fileChannel = raf.getChannel();
- // 第三步
- long offset = param.getChunk() * param.getSize();
- // 第四步
- byte[] fileData = param.getFile().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 (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
- return false;
- }
- String p = toBeRenamed.getParent();
- File newFile = new File(p + File.separatorChar + toFileNewName);
- // 修改文件名
- return toBeRenamed.renameTo(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.setLength(param.getChunkTotal());
- // 设置起始偏移量
- confAccessFile.setLength(param.getChunk());
- // 将指定的一个字节写入文件中 127,
- confAccessFile.write(Byte.MAX_VALUE);
- byte[] completeStatusList = FileUtils.readFileToByteArray(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++){
- isComplete = (byte)(isComplete & completeStatusList[i]);
- System.out.println("check part" + i + "complete?:" + completeStatusList[i]);
- }
- if(isComplete == Byte.MAX_VALUE){
- 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 = typeMap.get(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 = 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) {
- logger.error("clean MappedByteBuffer error!!!", e);
- }
- logger.info("clean MappedByteBuffer completed!!!");
- return null;
- }
- });
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
好了, 到此就全部结束了, 如果有疑问或批评, 欢迎评论和私信, 我们一起成长一起学习.
来源: https://www.cnblogs.com/yueguanguanyun/p/9842652.html