java实现大文件分片上传

背景:

        公司后台管理系统有个需求,需要上传体积比较大的文件:500M-1024M;此时普通的文件上传显然有些吃力了,加上我司服务器配置本就不高,带宽也不大,所以必须考虑多线程异步上传来提速;所以这里就要用到文件分片上传技术了。

技术选型:

        直接问GPT实现大文件分片上传比较好的解决方案,它给的答案是webUploader(链接是官方文档);这是由 Baidu FEX 团队开发的一款以 HTML5 为主,FLASH 为辅的现代文件上传组件。在现代的浏览器里面能充分发挥 HTML5 的优势,同时又不摒弃主流IE浏览器,沿用原来的 FLASH 运行时,兼容 IE6+,iOS 6+, android 4+。采用大文件分片并发上传,极大的提高了文件上传效率;功能强大且齐全,支持对文件内容的Hash计算分片上传,可实现上传进度条等功能。

实现原理:

        文件分片上传比较简单,就不画图了,前端(webUploader)将用户选择的文件根据开发者配置的分片参数进行分片计算,将文件分成N个小文件多次调用后端提供的分片文件上传接口(webUploader插件有默认的一套参数规范,文件ID及分片相关字段,后端将对保存分片临时文件),后端记录并判断当前文件所有分片是否上传完毕,若已上传完则将所有分片合并成完整的文件,完成后建议删除分片临时文件(若考虑做分片下载可以保留)。

前端引入webUploader:

这里推荐去CDN下载静态资源:

记得要先引入JQuery,webUploader依赖JQuery;前端页面引入CSS和JS文件即可,Uploader.swf文件在创建webUploader对象时指定,貌似用来做兼容的。

前端(笔者前端用的layui)核心代码:


        //百度文件上传插件 WebUploader
        let uploader = WebUploader.create({
            // 选完文件后,是否自动上传。
            auto: true,
            // swf文件路径
            swf: contextPath + '/static/plugin/webuploader/Uploader.swf',
            pick: {
                id: '#webUploader',
                multiple: false
            },
            // 文件接收服务端。
            server: contextPath + '/common/file/shard/upload',
            // 文件分片上传相关配置
            chunked: true,
            chunkSize: 5 * 1024 * 1024, // 分片大小为 5MB
            chunkRetry: 3, // 上传失败最大重试次数
            threads: 5, // 同时最大上传线程数
        });


        //文件上传临时对象
        let fileUpload = {
            idPrefix: '' //文件id前缀
            , genIdPrefix: function () {
                this.idPrefix = new Date().getTime() + '_';
            }
            , mergeLoading: null //合并文件加载层
            , lastUploadResponse: null // 最后一次上传返回值
            , chunks: 0 // 文件分片数
            , uploadedChunks: 0 // 已上传文件分片数
            , sumUploadChunk: function () {
                if (this.chunks > 0) {
                    this.uploadedChunks++;
                }
            }
            , checkResult: function () {
                if (this.uploadedChunks < this.chunks) {
                    layer.open({
                        title: '系统提示'
                        , content: '文件上传失败,请重新上传!'
                        , btn: ['我知道了']
                    });
                }
            }
        };

        // 某个文件开始上传前触发,一个文件只会触发一次
        uploader.on('uploadStart', function (file) {
            $('#uploadProgressBar').show();
            // 生成文件id前缀
            fileUpload.genIdPrefix();
        });

        // 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次
        uploader.on('uploadBeforeSend', function (object, data, header) {
            // 重写文件id生成规则
            data.id = fileUpload.idPrefix + data.name;
            fileUpload.chunks = data.chunks != null ? data.chunks : 0;
        });


        uploader.on('uploadProgress', function (file, percentage) {
            // 更新进度条
            let value = Math.round(percentage * 100);
            element.progress('progressBar', value + '%');
            if (value == 100) {
                fileUpload.mergeLoading = layer.load();
            }
        });

        // 获取最后上传成功的文件信息,每个分片文件上传都会回调
        uploader.on('uploadAccept', function (file, response) {
            if (response == null || response.code !== '0000') {
                return;
            }
            fileUpload.sumUploadChunk();
            if (response.data != null && response.data.fileAccessPath != null) {
                fileUpload.lastUploadResponse = response.data;
            }
        });

        // 文件上传成功时触发
        uploader.on('uploadSuccess', function (file, response) {
            console.log('File ' + file.name + ' uploaded successfully.');
            layer.msg('文件上传成功!');
            $('#fileName').val(fileUpload.lastUploadResponse.fileOriginalName);
            $('#fileRelativePath').val(fileUpload.lastUploadResponse.fileRelativePath);
        })

        uploader.on('uploadComplete', function (file) {
            console.log('File' + file.name + 'uploaded complete.');
            console.log('总分片:' + fileUpload.chunks + ' 已上传:' + fileUpload.uploadedChunks);
            fileUpload.checkResult();
            $('#uploadProgressBar').hide();
            layer.close(fileUpload.mergeLoading);
        });

其中几个关键的节点的事件回调都提供了,使用起来很方便;其中“uploadProgress”事件实现了上传的实时进度条展示。

后端Controller代码:

    /**
     * 文件分片上传
     * 
     * 
     * @param file
     * @param fileUploadInfoDTO
     * @return
     */
    @PostMapping(value = "shard/upload")
    public Layui<FileUploadService.FileBean> uploadFileByShard(@RequestParam("file") MultipartFile file,
            FileUploadInfoDTO fileUploadInfoDTO) {
        if (null == fileUploadInfoDTO) {
            return Layui.error("文件信息为空");
        }
        if (null == file || file.getSize() <= 0) {
            return Layui.error("文件内容为空");
        }
        log.info("fileName=[{}]", file.getName());
        log.info("fileSize=[{}]", file.getSize());
        log.info("fileShardUpload=[{}]", JSONUtil.toJsonStr(fileUploadInfoDTO));
        FileUploadService.FileBean fileBean = fileShardUploadService.uploadFileByShard(fileUploadInfoDTO, file);
        return Layui.success(fileBean);
    }


/**
 * @Author: XiangPeng
 * @Date: 2023/12/22 12:01
 */

@Getter
@Setter
public class FileUploadInfoDTO implements Serializable {

    private static final long serialVersionUID = -1L;

    /**
     * 文件 ID
     */
    private String id;

    /**
     * 文件名
     */
    private String name;

    /**
     * 文件类型
     */
    private String type;

    /**
     * 文件最后修改日期
     */
    private String lastModifiedDate;

    /**
     * 文件大小
     */
    private Long size;

    /**
     * 分片总数
     */
    private int chunks;

    /**
     * 当前分片序号
     */
    private int chunk;
}


@Getter
@Setter
public class FileUploadCacheDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 文件 ID
     */
    private String id;

    /**
     * 文件名
     */
    private String name;

    /**
     * 分片总数
     */
    private int chunks;

    /**
     * 当前已上传分片索引
     */
    private List<Integer> uploadedChunkIndex;

    public FileUploadCacheDTO(FileUploadInfoDTO fileUploadInfoDTO) {
        this.id = fileUploadInfoDTO.getId();
        this.name = fileUploadInfoDTO.getName();
        this.chunks = fileUploadInfoDTO.getChunks();
        this.uploadedChunkIndex = Lists.newArrayList();
    }

    public FileUploadCacheDTO() {

    }
}

后端Service层代码:

    /**
     * 文件分片上传
     * 
     * @param fileUploadInfoDTO
     * @param file
     * @return
     */
    public FileBean uploadFileByShard(FileUploadInfoDTO fileUploadInfoDTO, MultipartFile file) {
        if (fileUploadInfoDTO == null || file == null) {
            throw new ServiceException("文件上传失败!");
        }
        // 无需分片的小文件直接上传
        if (fileUploadInfoDTO.getChunks() <= 0) {
            return super.commonUpload(file);
        }
        String fileId = fileUploadInfoDTO.getId();
        // 生成分片临时文件,文件名格式:文件id_分片序号
        FileBean fileBean = super.commonUpload(fileId + StrUtil.UNDERLINE + fileUploadInfoDTO.getChunk(), file);
        // redis缓存数据
        FileUploadCacheDTO fileUploadInfo = null;
        synchronized (this) {
            // 查询文件id是否存在,不存在则创建,存在则更新已上传分片数
            fileUploadInfo = (FileUploadCacheDTO) redisService.get(genRedisKey(fileId));
            // 第一个分片文件上传
            if (fileUploadInfo == null) {
                fileUploadInfo = new FileUploadCacheDTO(fileUploadInfoDTO);
            }
            fileUploadInfo.getUploadedChunkIndex().add(fileUploadInfoDTO.getChunk());
            redisService.set(genRedisKey(fileId), fileUploadInfo);
            // 判断所有分片文件是否上传完成
            if ((fileUploadInfo.getUploadedChunkIndex().size()) < fileUploadInfo.getChunks()) {
                return fileBean;
            }
        }
        // 合并文件
        return mergeChunks(fileUploadInfo);
    }

    /**
     * 分片文件全部上传完成则合并文件,清除缓存并返回文件地址
     * 
     * @param fileUploadCache
     * @return
     */
    private FileBean mergeChunks(FileUploadCacheDTO fileUploadCache) {
        String mergeFileRelativePath = super.getCommonPath().getFileRelativePath() + fileUploadCache.getId();
        String mergeFilePath = super.getCommonPath().getBasePath() + mergeFileRelativePath;
        RandomAccessFile mergedFile = null;
        File chunkTempFile = null;
        RandomAccessFile chunkFile = null;
        try {
            mergedFile = new RandomAccessFile(mergeFilePath, "rw");
            for (int i = 0; i < fileUploadCache.getChunks(); i++) {
                // 读取分片文件
                chunkTempFile = new File(
                        super.getCommonPath().getFileFullPath() + fileUploadCache.getId() + StrUtil.UNDERLINE + i);
                byte[] buffer = new byte[1024 * 1024];
                int bytesRead;
                chunkFile = new RandomAccessFile(chunkTempFile, "r");
                // 合并分片文件
                while ((bytesRead = chunkFile.read(buffer)) != -1) {
                    mergedFile.write(buffer, 0, bytesRead);
                }
                chunkFile.close();
            }
        } catch (IOException e) {
            log.error("merge file chunk error, fileId=[{}]", fileUploadCache.getId(), e);
        } finally {
            try {
                if (mergedFile != null) {
                    mergedFile.close();
                }
            } catch (IOException e) {

            }
            redisService.remove(genRedisKey(fileUploadCache.getId()));
            // 删除分片文件
            removeChunkFiles(super.getCommonPath().getFileFullPath(), fileUploadCache);
        }
        return FileBean.builder().fileOriginalName(fileUploadCache.getName()).fileRelativePath(mergeFileRelativePath)
                .fileAccessPath(super.getNginxPath() + mergeFileRelativePath).build();
    }


    private void removeChunkFiles(String fileFullPathPrefix, FileUploadCacheDTO fileUploadCache) {

        taskExecutor.execute(() -> {
            try {
                // 延迟1秒删除
                TimeUnit.SECONDS.sleep(1);
                String fileFullPath;
                for (int i = 0; i < fileUploadCache.getChunks(); i++) {
                    try {
                        fileFullPath = fileFullPathPrefix + fileUploadCache.getId() + StrUtil.UNDERLINE + i;
                        FileUtil.del(fileFullPath);
                        log.info("file[{}] delete success.", fileFullPath);
                    } catch (Exception e) {
                        log.error("delete temp file error.", e);
                    }
                }
            } catch (Exception e) {
                log.error("delete temp chunk file error.", e);
            }
        });

    }

    private String genRedisKey(String id) {
        return FILE_SHARD_UPLOAD_KEY + id;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/287186.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构与算法python版本之线性结构之队列Quene

什么是队列&#xff1f; 队列是一种有次序的数据集合&#xff0c;其特征是&#xff1a;新数据项的添加总发生在一端&#xff08;通常称为“尾rear”端&#xff09;&#xff0c;而现存数据项的移除总发生在另一端&#xff08;通常称为“首front”端&#xff09;&#xff1b;当数…

缓存数据一致性策略如何分类?

一、概述 数据库与缓存数据一致性问题&#xff0c;一直以来都是大家比较关注的问题。针对一致性的解决方案也是非常多&#xff0c;以下主要针对方案的梳理与分类&#xff1a; 数据库数据与缓存数据一致性的方案&#xff0c;可以从不同的角度来分类&#xff0c;比如&#xff1…

稳定币记录

稳定币&#xff1a; 稳定币&#xff08;Stablecoin&#xff09;是一种加密货币&#xff0c;其设计目的是维持相对稳定的价值&#xff0c;通常与某种法定货币&#xff08;如美元、欧元&#xff09;或其他资产&#xff08;如黄金&#xff09;挂钩。稳定币通过将加密货币与相应的…

Flink-【时间语义、窗口、水位线】

1. 时间语义 1.1 事件时间&#xff1a;数据产生的事件&#xff08;机器时间&#xff09;&#xff1b; 1.2 处理时间&#xff1a;数据处理的时间&#xff08;系统时间&#xff09;。 &#x1f330;&#xff1a;可乐 可乐的生产日期 事件时间&#xff08;可乐产生的时间&…

计算机毕业设计 SpringBoot的停车场管理系统 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

逻辑回归算法到底能做什么

逻辑回归&#xff08;Logistic Regression&#xff09;是一种广义的线性回归分析模型&#xff0c;常用于数据挖掘、疾病自动诊断、经济预测等领域。它根据给定的自变量数据集来估计事件的发生概率。变量的范围在0和1之间&#xff0c;通常用于二分类问题&#xff0c;最终输出的预…

Opencv(C++)学习之cv::calcHist 任意bin数量进行直方图计算

**背景&#xff1a;**当前网上常见的直方图使用方法都是默认使用256的范围&#xff0c;而对于使用特定范围的直方图方法讲的不够清楚。仔细研究后总结如下&#xff1a; 1、常见使用方法&#xff0c;直接对灰度图按256个Bin进行计算。 Mat mHistUn; int channels[1] { 0 }; {…

键盘数字键打不出来怎么解锁?收藏好这4个简单方法!

“我在使用电脑进行办公时&#xff0c;突然发现我电脑键盘的数字键无法输入&#xff0c;这该怎么办呢&#xff1f;我应该如何解锁呢&#xff1f;请给我出出主意吧&#xff01;” 在日常使用电脑时&#xff0c;很多用户都需要使用键盘输入文字。但有时候部分用户也会遇到键盘数字…

你知道vue中key的原理吗?说说你对它的理解

一、Key是什么 开始之前&#xff0c;我们先还原两个实际工作场景 当我们在使用v-for时&#xff0c;需要给单元加上key <ul><li v-for"item in items" :key"item.id">...</li> </ul>用new Date()生成的时间戳作为key&#xff0c…

Docker 网络管理

一、Docker网络简介 Docker网络是容器化应用程序的重要组成部分&#xff0c;它使得容器之间可以互相通信和连接&#xff0c;同时也提供了容器与外部环境之间的隔离和连接。 二、Docker网络网络模式 Docker 提供了多种网络模式&#xff0c;可以通过docker network ls 命令查看…

springboot实现ChatGPT式调用(一次调用,持续返回)

下边实现了一个持续返回100以内随机数的接口&#xff0c;在接口超时之前会每隔1秒返回一个随机数 GetMapping(value "/getRandomNum", produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter getRandomNum() {SseEmitter emitter new SseEmitter();Th…

五、Spring AOP面向切面编程(基于注解方式实现和细节)

本章概要 Spring AOP底层技术组成初步实现获取通知细节信息切点表达式语法重用&#xff08;提取&#xff09;切点表达式环绕通知切面优先级设置CGLib动态代理生效注解实现小结 5.5.1 Spring AOP 底层技术组成 动态代理&#xff08;InvocationHandler&#xff09;&#xff1a;…

SQL Server 权限管理

CSDN 成就一亿技术人&#xff01; 2024年 第一篇 难度指数&#xff1a;* * CSDN 成就一亿技术人&#xff01; 目录 1. 权限管理 什么是权限管理&#xff1f; SQL server的安全机制 服务器级角色 数据库级角色 对象级角色 2. 创建用户 赋予权限 最重要的一步骤 1. 权限…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《计及源荷不确定性的综合能源系统日前-日内协调优化调度》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主的专栏栏目《论文与完整程序》 这个标题指的是一个综合能源系统&#xff08;包括多种能源资源和负荷需求&#xff09;&#xff0c;在考虑到源&#xff08;能源供给&#xff09;和荷&#…

代码随想录算法训练营Day16 | 654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

LeetCode 654 最大二叉树 本题思路&#xff1a;我们可以看到每次其实这个找最大值&#xff0c;然后创建节点的过程就是一个二叉树的前序遍历的过程。所以&#xff0c;我们可以递归来完成它。 先创找到数组中&#xff0c;最大的值的下标&#xff0c;然后创建根节点然后根据下标…

【零基础入门TypeScript】TypeScript - 基本语法

目录 你的第一个 TypeScript 代码 编译并执行 TypeScript 程序 编译器标志 TypeScript 中的标识符 TypeScript ─ 关键字 空格和换行符 TypeScript 区分大小写 分号是可选的 TypeScript 中的注释 TypeScript 和面向对象 语法定义了一组编写程序的规则。每种语言规范都…

Linux系统:引导过程与服务控制

目录 一、linux系统引导过程 1、引导过程介绍 1.1 引导过程总览图 1.2 引导过程详解 1.3 系统初始化进程 1.4 Ststemd单元类型 1.5 运行级别所对应的Systemd目标 二、排除启动类故障 1、修复MBR扇区故障 1.1 故障原因 1.2 故障现象 1.3 解决思路 1.4 详细操作步骤…

密码学:带密钥的消息摘要算法一数字签名算法

文章目录 前言手写签名和数字签名前置知识点&#xff1a;消息摘要算法数字签名算法数字签名算法的由来数字签名算法在实际运用的过程附加&#xff1a;签名和摘要值的解释 数字签名算法的家谱数字签名算法的消息传递模型经典数字签名算法-RSA实现 数字签名标准算法-DSA实现 圆曲…

IPC之十二:使用libdbus在D-Bus上异步发送/接收信号的实例

IPC 是 Linux 编程中一个重要的概念&#xff0c;IPC 有多种方式&#xff0c;本 IPC 系列文章的前十篇介绍了几乎所有的常用的 IPC 方法&#xff0c;每种方法都给出了具体实例&#xff0c;前面的文章里介绍了 D-Bus 的基本概念以及调用远程方法的实例&#xff0c;本文介绍 D-Bus…

【VTK-Rendering::Core】第二期 vtkTextActor

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ&#xff1a;870202403 前言 本文以vtkTextActor为起点&#xff0c;分享VTK中Text相关的内容&#xff0c;希望对各位小伙伴有所帮助&#xff01; 感谢各位小伙伴的点赞关注&#xff0c;小易会继续努力分享&#xff0c;一起进步&a…