springboot大文件上传、分片上传、断点续传、秒传的实现

对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。

本文是基于 springboot + vue 实现的文件上传,本文主要介绍服务端实现文件上传的步骤及代码实现
上传分步:
本人分析上传总共分为:

检查文件是否已上传,如已上传可实现秒传
创建临时文件(._tmp)和上传的配置文件(.conf)
使用RandomAccessFile获取临时文件
调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
获取当前是第几个分块,计算文件的最后偏移量
获取当前文件分块的字节数组,用于获取文件字节长度
使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer
将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b)
释放缓冲区
检查文件是否全部完成上传,如上传完成将临时文件名为正式文件名
直接上代码

public class FlieChunkUtils {
 
    /**
     * 分块上传
     * 第一步:获取RandomAccessFile,随机访问文件类的对象
     * 第二步:调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
     * 第三步:获取当前是第几个分块,计算文件的最后偏移量
     * 第四步:获取当前文件分块的字节数组,用于获取文件字节长度
     * 第五步:使用文件通道FileChannel类的 map()方法创建直接字节缓冲器  MappedByteBuffer
     * 第六步:将分块的字节数组放入到当前位置的缓冲区内  mappedByteBuffer.put(byte[] b);
     * 第七步:释放缓冲区
     * 第八步:检查文件是否全部完成上传
     *
     * @param param
     * @return
     * @throws Exception
     */
    public static ApiResult uploadByMappedByteBuffer(MultipartFileParam param) throws Exception {
        if (param.getIdentifier() == null || "".equals(param.getIdentifier())) {
            param.setIdentifier(UUID.randomUUID().toString());
        }
        // 判断是否上传
        if (ObjectUtil.isEmpty(param.getFile())) {
            return checkUploadStatus(param);
        }
        // 文件名称
        String fileName = getFileName(param);
        // 临时文件名称
        String tempFileName = param.getIdentifier() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
        // 获取文件路径
        String filePath = getUploadPath(param);
        // 创建文件夹
        FileUploadUtils.getAbsoluteFile(filePath, fileName);
        // 创建临时文件
        File tempFile = new File(filePath, tempFileName);
        //第一步 获取RandomAccessFile,随机访问文件类的对象
        RandomAccessFile raf = RandomAccessFileUitls.getModelRW(tempFile);
        //第二步 调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
        FileChannel fileChannel = raf.getChannel();
        //第三步 获取当前是第几个分块,计算文件的最后偏移量
        long offset = (param.getChunkNumber() - 1) * param.getChunkSize();
        //第四步 获取当前文件分块的字节数组,用于获取文件字节长度
        byte[] fileData = param.getFile().getBytes();
        //第五步 使用文件通道FileChannel类的 map()方法创建直接字节缓冲器  MappedByteBuffer
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        //第六步 将分块的字节数组放入到当前位置的缓冲区内  mappedByteBuffer.put(byte[] b)
        mappedByteBuffer.put(fileData);
        //第七步 释放缓冲区
        freeMappedByteBuffer(mappedByteBuffer);
        fileChannel.close();
        raf.close();
        //第八步 检查文件是否全部完成上传
        ApiResult result = ApiResult.success();
        boolean isComplete = checkUploadStatus(param, fileName, filePath);
        if (isComplete) {
            // 完成后,临时文件名为正式文件名
            renameFile(tempFile, fileName);
            result.put("endUpload", true);
        }
 
        result.put("filePath", FileUploadUtils.getPathFileName(filePath, fileName));
        result.put("fileName", param.getFile().getOriginalFilename());
        return result;
    }
 
    /**
     * 检查文件是否上传
     *
     * @param param
     * @return
     * @throws Exception
     */
    public static ApiResult checkUploadStatus(MultipartFileParam param) throws Exception {
        String fileName = getFileName(param);
        // 校验conf文件
        File confFile = checkConfFile(fileName, getUploadPath(param));
        // 获取完成列表
        byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
        List<String> uploadeds = new ArrayList<>();
        for (int i = 0; i < completeStatusList.length; i++) {
            if (completeStatusList[i] == Byte.MAX_VALUE) {
                uploadeds.add(i + 1 + "");
            }
        }
        ApiResult<Void> success = ApiResult.success();
        success.put("uploaded", uploadeds);
        success.put("skipUpload", completeStatusList.length > 0 && completeStatusList.length == uploadeds.size());
        // 新文件
        if (ObjectUtil.isEmpty(completeStatusList)) {
            success.put("chunk", false);
            return success;
        }
        if (completeStatusList.length < param.getChunkNumber()) {
            success.put("chunk", false);
            return success;
        }
        byte b = completeStatusList[param.getChunkNumber() - 1];
        if (b != Byte.MAX_VALUE) {
            success.put("chunk", false);
            return success;
        }
        success.put("filePath", FileUploadUtils.getPathFileName(getUploadPath(param), fileName));
        success.put("chunk", true);
        return success;
    }
 
    /**
     * 文件下载
     *
     * @param filePath 文件地址
     * @param request
     * @param response
     * @throws IOException
     */
    public static void download(String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 初始化 response
        response.reset();
        // 获取文件
        File file = new File(getDownloadPath(filePath));
        long fileLength = file.length();
        //获取从那个字节开始读取文件
        String rangeString = request.getHeader("Range");
        long range = 0;
        if (StrUtil.isNotBlank(rangeString)) {
            range = Long.valueOf(rangeString.substring(rangeString.indexOf("=") + 1, rangeString.indexOf("-")));
        }
        if (range >= fileLength) {
            throw new CustomException("文件读取长度过长");
        }
        long byteLength = 1024 * 1024;
        if (range + byteLength > fileLength) {
            byteLength = fileLength;
        }
        // 随机读文件RandomAccessFile
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        try {
            // 移动访问指针到指定位置
            randomAccessFile.seek(range);
            // 每次请求只返回1MB的视频流
            byte[] bytes = new byte[(int) byteLength];
            int len = randomAccessFile.read(bytes);
            //获取响应的输出流
            OutputStream outputStream = response.getOutputStream();
            //返回码需要为206,代表只处理了部分请求,响应了部分数据
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            //设置此次相应返回的数据长度
            response.setContentLength(len);
            //设置此次相应返回的数据范围
            response.setHeader("Content-Range", "bytes " + range + "-" + len + "/" + fileLength);
            // 将这1MB的视频流响应给客户端
            outputStream.write(bytes, 0, len);
            outputStream.close();
            //randomAccessFile.close();
            System.out.println("返回数据区间:【" + range + "-" + (range + len) + "】");
        } finally {
            randomAccessFile.close();
        }
    }
 
    /**
     * 文件重命名
     *
     * @param toBeRenamed   将要修改名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    private static 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
     */
    private static boolean checkUploadStatus(MultipartFileParam param, String fileName, String filePath) throws Exception {
        // 校验conf文件
        File confFile = checkConfFile(fileName, filePath);
        // 读取conf
        RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw");
        //设置文件长度
        if (confAccessFile.length() != param.getTotalChunks()) {
            confAccessFile.setLength(param.getTotalChunks());
        }
        //设置起始偏移量
        confAccessFile.seek(param.getChunkNumber() - 1);
        //将指定的一个字节写入文件中 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++) {
            // 按位与运算,将&两边的数转为二进制进行比较,有一个为0结果为0,全为1结果为1  eg.3&5  即 0000 0011 & 0000 0101 = 0000 0001   因此,3&5的值得1。
            isComplete = (byte) (isComplete & completeStatusList[i]);
        }
        if (isComplete == Byte.MAX_VALUE) {
            //如果全部文件上传完成,删除conf文件
            // FileUtils.deleteFile(confFile.getPath());
            return true;
        }
        return false;
    }
 
 
    /**
     * 在MappedByteBuffer释放后再对它进行读操作的话就会引发jvm crash,在并发情况下很容易发生
     * 正在释放时另一个线程正开始读取,于是crash就发生了。所以为了系统稳定性释放前一般需要检 查是否还有线程在读或写
     *
     * @param mappedByteBuffer
     */
    private static 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) {
                        log.error("clean MappedByteBuffer error!!!", e);
                    }
                    return null;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    private static String getFileName(MultipartFileParam param) {
        String extension;
        if (ObjectUtil.isNotEmpty(param.getFile())) {
            // return param.getFile().getOriginalFilename();
            String filename = param.getFile().getOriginalFilename();
            extension = filename.substring(filename.lastIndexOf("."));
            //return  FileUploadUtils.extractFilename(param.getFile());
        } else {
            extension = param.getFilename().substring(param.getFilename().lastIndexOf("."));
            //return DateUtils.datePath() + "/" + IdUtil.fastUUID() + extension;
        }
        return param.getIdentifier() + extension;
    }
 
    private static String getUploadPath(MultipartFileParam param) {
        return FileUploadUtils.getDefaultBaseDir() + "/" + param.getObjectType();
    }
 
    private static String getDownloadPath(String filePath) {
        // 本地资源路径
        String localPath = WhspConfig.getProfile();
        // 数据库资源地址
        String loadPath = localPath + StrUtil.subAfter(filePath, Constants.RESOURCE_PREFIX, false);
        return loadPath;
    }
 
    private static File checkConfFile(String fileName, String filePath) throws Exception {
        File confFile = FileUploadUtils.getAbsoluteFile(filePath, fileName + ".conf");
        if (!confFile.exists()) {
            confFile.createNewFile();
        }
        return confFile;
    }
}

到此这篇关于springboot大文件上传、分片上传、断点续传、秒传的实现的文章就介绍到这了

参考文章:http://blog.ncmem.com/wordpress/2023/09/26/springboot%e5%a4%a7%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e3%80%81%e5%88%86%e7%89%87%e4%b8%8a%e4%bc%a0%e3%80%81%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e3%80%81%e7%a7%92%e4%bc%a0%e7%9a%84%e5%ae%9e-2/
欢迎入群一起讨论

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot实现MinIO文件服务器的分片上传断点续传,可以使用MinIO Java SDK提供的API和Spring Boot提供的Multipart File上传功能。具体实现步骤如下: 1. 引入MinIO Java SDK和Spring Boot的依赖 ```xml <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.0.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` 2. 配置MinIO客户端 在Spring Boot的配置文件中,配置MinIO客户端的连接信息。 ```yaml minio: endpoint: minio.example.com access-key: ACCESS_KEY secret-key: SECRET_KEY secure: false ``` 3. 初始化Multipart上传上传文件之前,使用 `InitiateMultipartUpload` API 方法初始化一个Multipart上传会话,并获取一个上传ID。上传ID用于标识一个Multipart上传会话。 ```java import io.minio.MinioClient; import io.minio.errors.MinioException; import io.minio.messages.Part; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.ArrayList; import java.util.List; @Service public class MinioService { @Autowired private MinioClient minioClient; @Value("${minio.bucket-name}") private String bucketName; public String initiateMultipartUpload(String objectName) throws MinioException { String uploadId = minioClient.initiateMultipartUpload(bucketName, objectName).uploadId(); return uploadId; } } ``` 4. 上传文件块 将大文件分成多个小块,每个小块的大小可以根据需求自定义。使用 `UploadPart` API 方法将每个小块独立上传上传时需要指定上传文件名、块编号、块大小以及上传ID等信息。 ```java public class MinioService { // ... public List<Part> uploadParts(String objectName, String uploadId, MultipartFile file, int partSize) throws IOException, MinioException { List<Part> parts = new ArrayList<>(); int partNumber = 1; byte[] buffer = new byte[partSize]; while (true) { int bytesRead = file.getInputStream().read(buffer); if (bytesRead < 0) { break; } byte[] partData = new byte[bytesRead]; System.arraycopy(buffer, 0, partData, 0, bytesRead); Part part = minioClient.uploadPart( bucketName, objectName, partNumber, uploadId, partData, bytesRead); parts.add(part); partNumber++; } return parts; } } ``` 5. 完成Multipart上传 上传所有文件块后,使用 `CompleteMultipartUpload` API 方法将它们合并成一个完整的文件,最终得到上传文件。 ```java public class MinioService { // ... public void completeMultipartUpload(String objectName, String uploadId, List<Part> parts) throws MinioException { minioClient.completeMultipartUpload(bucketName, objectName, uploadId, parts); } } ``` 6. 断点续传 如果上传中断,可以使用 `ListParts` API 方法获取已上传文件块信息,然后从中断处继续上传。 ```java public class MinioService { // ... public List<Part> listParts(String objectName, String uploadId) throws MinioException { return minioClient.listParts(bucketName, objectName, uploadId).getParts(); } } ``` 使用Spring Boot的Multipart File上传功能上传文件时,需要在Controller中添加 `@RequestParam("file") MultipartFile file` 注解,自动将文件转换为MultipartFile类型。 ```java @RestController @RequestMapping("/file") public class FileController { @Autowired private MinioService minioService; @PostMapping("/upload") public void uploadFile(@RequestParam("file") MultipartFile file) throws IOException, MinioException { // 获取文件名和文件大小 String fileName = file.getOriginalFilename(); long fileSize = file.getSize(); // 定义块大小(5MB) int partSize = 5 * 1024 * 1024; // 初始化Multipart上传会话 String uploadId = minioService.initiateMultipartUpload(fileName); // 上传文件块 List<Part> parts = minioService.uploadParts(fileName, uploadId, file, partSize); // 完成Multipart上传 minioService.completeMultipartUpload(fileName, uploadId, parts); } @PostMapping("/resume") public void resumeUpload(@RequestParam("file") MultipartFile file, String objectName, String uploadId) throws IOException, MinioException { // 获取已上传文件块信息 List<Part> parts = minioService.listParts(objectName, uploadId); // 上传文件块 parts = minioService.uploadParts(objectName, uploadId, file, partSize); // 完成Multipart上传 minioService.completeMultipartUpload(objectName, uploadId, parts); } } ``` 以上是使用Spring Boot实现MinIO文件服务器的分片上传断点续传的基本步骤,具体实现还需要根据实际需求进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值