JAVA大文件分片上传

JAVA大文件分片上传

一 、思路

    1. 将大文件拆分成多个小文件多次上传, 全部上传完成后, 将这些分片合并成原始文件
    1. 合并过程中需要按顺序合并, 所以需要 chunkIndex 定位顺序
    1. 需要知道什么时候合并, 所以需要 totalChunks 确定分片总数, 后端判断 上传数量(后端记录) == 分片总数
    1. fileHash作为唯一key来标识文件, 比如: MD5计算
    1. 合并完成后删除分片, 清空缓存
    1. 防止文件占用过多jvm内存, 应使用流的方式进行文件传输, 避免使用file.getBytes()类似的方法导致oom, 本示例使用FileChannel的方式进行文件传输

二、代码示例

java 复制代码
package org.example.controller;


import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@RestController
@RequestMapping("/api/test")
public class TestController {

    private Map<String, AtomicInteger> CHUNK_TOTAL_MAP = new ConcurrentHashMap<>();

    /**
     * 大文件分片上传
     * 思路: 1. 将大文件拆分成多个小文件多次上传, 全部上传完成后, 将这些分片合并成原始文件
     *      2. 合并过程中需要按顺序合并, 所以需要 chunkIndex 定位顺序
     *      3. 需要知道什么时候合并, 所以需要 totalChunks 确定分片总数, 后端判断 上传数量(后端记录) == 分片总数
     *      4. fileHash作为唯一key来标识文件
     *      5. 合并完成后删除分片, 清空缓存
     * @param multipartFile 文件片段
     * @param chunkIndex 分片顺序编号, 从0开始
     * @param totalChunks 分片总数
     * @param fileHash 原文件hash值, 作为唯一key
     * @param fileName 合并后的文件名
     * @param endWith 合并后的文件后缀
     * @return
     */
    @PostMapping("/upload")
    public ResponseEntity<Object> upload(@RequestPart MultipartFile multipartFile,
                                         @RequestParam int chunkIndex,
                                         @RequestParam int totalChunks,
                                         @RequestParam String fileHash,
                                         @RequestParam String fileName,
                                         @RequestParam String endWith){

        //临时存储分片路径
        String chunkDir = "D:/file/upload/"+fileHash;
        String chunkPath = "/chunk_" + chunkIndex;

        //创建目录
        try {
            Files.createDirectories(Path.of(chunkDir));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        //保存分片
        writeToChunk(multipartFile, chunkPath);

        //更新上传分片数量
        CHUNK_TOTAL_MAP.putIfAbsent(fileHash, new AtomicInteger(0));
        int uploadChunks = CHUNK_TOTAL_MAP.get(fileHash).incrementAndGet();

        //判断是否开始合并
        if(uploadChunks == totalChunks){
            mergeChunk(chunkDir, fileName, totalChunks, endWith);
            CHUNK_TOTAL_MAP.remove(fileHash); //清空缓存
        }

        return ResponseEntity.ok(CHUNK_TOTAL_MAP);
    }

    /**
     * 合并分片
     * @param chunkDir 文件保存路径
     * @param fileName 文件名
     * @param totalChunks 分片总数
     * @param endWith 合并后的文件后缀
     */
    private void mergeChunk(String chunkDir, String fileName, int totalChunks, String endWith) {
        String filePath = chunkDir + "/" +fileName + "." + endWith;
        try (FileChannel outChannel = (FileChannel) Files.newByteChannel(Path.of(filePath), StandardOpenOption.CREATE, StandardOpenOption.APPEND)){

            for (int i = 0; i < totalChunks; i++) {
                //读取分片
                String chunkPath = "/chunk_" + i;
                try(FileChannel inChannel = (FileChannel) Files.newByteChannel(Path.of(chunkPath), StandardOpenOption.READ)){
                    //合并分片到主文件
                    outChannel.transferFrom(inChannel,outChannel.size(), inChannel.size());
                    //删除分片
                    Files.deleteIfExists(Path.of(chunkPath));
                }catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }


    }

    /**
     * 保存分片
     * @param multipartFile
     * @param chunkPath
     */
    private void writeToChunk(MultipartFile multipartFile, String chunkPath) {

        try (FileChannel outChannel = (FileChannel) Files.newByteChannel(Path.of(chunkPath), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
             InputStream inputStream = multipartFile.getInputStream();
             ReadableByteChannel inChannel =  Channels.newChannel(inputStream);){

            long chunkSize = multipartFile.getSize();
            outChannel.transferFrom(inChannel,0, chunkSize);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
相关推荐
FYKJ_201029 分钟前
springboot助农产品销售商城05829-计算机课程设计、毕业设计
java·spring boot·python·mysql·spark·django
user_admin_god3 小时前
第 10 篇:拼上下文与生成——预算、边界与防幻觉
java·人工智能·spring boot·语言模型
用户3126874877203 小时前
GC 算法与垃圾收集器演进:从 Serial 到 ZGC
java
天天被压力3 小时前
【别再到处找免费股票数据API了:官方204个接口,32篇一次讲透 #02】涨停跌停与特色股池:5类股池接口一次拉全
java·人工智能·python
斯维赤4 小时前
从 @Tool 到 Agent 流水线:LangChain4j 进阶教学,一个库打全套
java·后端
SimonKing4 小时前
SSE项目`nexus-sse`持续优化,不一样的视觉效果
java·后端·程序员
斑鸠喳喳4 小时前
读写锁模式 Read-Write Lock
java·后端
信誓旦旦的程序猿4 小时前
【Python 量化取数指南 #09】Python 拿港股通数据:港股通成交与港股财报实测
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据api
小程序设计5 小时前
本科毕设避坑|大数据 / 单片机物联网 / 网络工程 / PLC / 机械设计选题与落地心得
java·hive·hadoop·单片机
卓怡学长5 小时前
w206基于SpringBoot的社区团购小程序设计与实现
java·spring boot·spring·intellij-idea