java实现大文件分片上传

news2024/11/28 12:31:17

背景:

        公司后台管理系统有个需求,需要上传体积比较大的文件: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;
    }

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flink 维表关联方案

Flink 维表关联方案 1、Flink DataStream 关联维表 1&#xff09;概述 1.分类 实时数据库查找关联&#xff08;Per-Record Reference Data Lookup&#xff09; 预加载维表关联&#xff08;Pre-Loading of Reference Data&#xff09; 维表变更日志关联&#xff08;Refere…

SpringBoot从配置文件中获取属性的方法

方式一&#xff1a;Value 基本类型属性注入&#xff0c;直接在字段上添加Value("\${xxx.xxx}")即可&#xff0e;注意这里用的是$&#xff0c;而不是&#xff03;&#xff0c;Value注入的属性&#xff0c;一般其他属性没有关联关系。 配置文件 user:name: Manaphya…

代码随想录算法训练营day6|242.有效的字母异位词、349.两个数组的交集、202.快乐数

哈希表理论基础 建议&#xff1a;大家要了解哈希表的内部实现原理&#xff0c;哈希函数&#xff0c;哈希碰撞&#xff0c;以及常见哈希表的区别&#xff0c;数组&#xff0c;set 和map。 什么时候想到用哈希法&#xff0c;当我们遇到了要快速判断一个元素是否出现集合里的时…

C#用StringBuilder高效处理字符串

目录 一、背景 二、使用StringBuilder便捷、高效地操作字符串 三、实例 1.源码 2.生成效果 四、实例中知识点 1.StringBuilder类 一、背景 符串是不可改变的对象&#xff0c;字符串在创建以后&#xff0c;就不会被改变&#xff0c;当使用字符串对象的Replace、split或Re…

提供电商Api接口-100种接口,淘宝,1688,抖音商品详情数据安全,稳定,支持高并发

Java是一种高级编程语言&#xff0c;由Sun Microsystems公司于1995年推出&#xff0c;现在属于Oracle公司开发和维护。Java以平台无关性、面向对象、安全性、可移植性和高性能著称&#xff0c;广泛用于桌面应用程序、嵌入式系统、企业级服务、Android移动应用程序等。 接口是Ja…

JS运行机制、Event Loop

1、JS运行机制 JS最大的特点就是单线程&#xff0c;所以他同一时间只能做一件事情。使单线程不阻塞&#xff0c;就是事件循环。 在JS当中分为两种任务&#xff1a; 同步任务&#xff1a;立即执行的任务&#xff0c;一般放在主线程中&#xff08;主执行栈&#xff09;。异步任…

Python消消乐小游戏(PyGame)

文章目录 写在前面喜羊羊与灰太狼PyGame入门消消乐注意事项写在后面 写在前面 本期内容&#xff1a;基于pygame实现喜羊羊与灰太狼版消消乐小游戏 实验环境 python3.11及以上pycharmpygame 安装pygame的命令&#xff1a; pip install -i https://pypi.tuna.tsinghua.edu.c…

前端基础:Vue搞笑白话文(工作之余瞎写)

1、data:{}与data(){return{}}这两个是个什么鬼&#xff1f; vue实例 new Vue({el:#app,data:{name:李四}}) 组件实例 const aaa Vue.extent({data(){return {name:}} }) 为什么Vue实例可以那么写而组件实例就不行了&#xff1f;原因就是因为在底层原理上&#xff0c;组件…

【KingbaseES】实现MySql函数Space

CREATE OR REPLACE FUNCTION SPACE(input_length integer) RETURNS text AS $$ BEGIN RETURN REPEAT( , input_length) AS SPACES; END; $$ LANGUAGE plpgsql;

Windows 下用 C++ 调用 Python

文章目录 Part.I IntroductionChap.I InformationChap.II 预备知识 Part.II 语法Chap.I PyRun_SimpleStringChap.II C / Python 变量之间的相互转换 Part.III 实例Chap.I 文件内容Chap.II 基于 Visual Studio IDEChap.III 基于 cmakeChap.IV 运行结果 Part.IV 可能出现的问题Ch…

从零开始使用Konva,画图并绑定节点。

实战可行&#xff0c;vue3vitets实现 实现电子地图&#xff0c;左侧列表可拖拽绑定 地图可绑定点设备坐标 安装 npm install konva 插件引入 import Konva from konva import Konva from konva import { getImgUrl } from /utils export class konvaManager {public stage…

视频怎么配上音乐?视频软件轻松配乐

视频怎么配上音乐&#xff1f;视频配乐已经成为了一种重要的表达方式。它能够为视频增添情感&#xff0c;营造氛围&#xff0c;让观众更加深入地理解视频的内容。那么&#xff0c;哪些软件可以给视频配上音乐呢&#xff1f;本文将为你介绍几款优秀软件。 一、清爽视频编辑 清爽…

福利来袭,.NET Core开发5大案例,30w字PDF文档大放送!!!

千里之行&#xff0c;始于足下&#xff0c;若想提高软件编程能力&#xff0c;最最重要的是实践&#xff0c;所谓纸上得来终觉浅&#xff0c;绝知此事要躬行。根据相关【艾宾浩斯遗忘曲线】研究表明&#xff0c;如果不动手实践&#xff0c;记住的东西会很快忘记。 为了便于大家查…

虚幻UE 增强输入-第三人称模板增强输入分析与扩展

本篇是增强输入模块&#xff0c;作为UE5.0新增加的模块。 其展现出来的功能异常地强大&#xff01; 让我们先来学习学习一下第三人称模板里面的增强输入吧&#xff01; 文章目录 前言一、增强输入四大概念二、使用步骤1、打开增强输入模块2、添加IA输入动作2、添加IMC输入映射内…

SAFe大规模敏捷企业级实训

课程简介 SAFe – Scaled Agile Framework是目前全球运用最广泛的大规模敏捷框架&#xff0c;也是成长最快、最被认可、最有价值的规模化敏捷框架&#xff0c;目前全球SAFe认证专业人士已达80万人&#xff0c;福布斯100强的70%都在实施SAFe。本课程是一个2天的 SAFe权威培训课…

线程的深入学习(二)

前言 上一篇讲了线程池的相关知识&#xff0c;这篇文章主要讲解一个 1.并发工具类如CountDownLatch、CyclicBarrier等。 2.线程安全和并发集合&#xff1a; 3.学习如何使用Java提供的线程安全的集合类&#xff0c;如ConcurrentHashMap、CopyOnWriteArrayList等。 并发工具类 …

java练习题之String方法运用

应用知识点&#xff1a;​​​​​​String类 1&#xff1a;(String 类)仔细阅读以下代码段&#xff1a; String s "hello"; String t"hello"; char[] c {h,e,l,l,o}; 2&#xff1a;下列选项输出结果为false 的语句是() System.out.println( s.euqals( t…

线段树基础(下)

线段树二分 对序列进行二分的操作&#xff0c;可能使用线段树二分进行优化。 一些序列上最左/最右位置问题可以二分解决&#xff0c;同时需要使用线段树进行查询。时间复杂度通常是 O ( n log ⁡ 2 n ) O(n\log^2n) O(nlog2n)&#xff0c;可以尝试使用线段树二分的技巧将其优…

机器学习(四) -- 模型评估(2)

系列文章目录 机器学习&#xff08;一&#xff09; -- 概述 机器学习&#xff08;二&#xff09; -- 数据预处理&#xff08;1-3&#xff09; 机器学习&#xff08;三&#xff09; -- 特征工程&#xff08;1-2&#xff09; 机器学习&#xff08;四&#xff09; -- 模型评估…

B端产品经理学习-对用户进行需求挖掘

目录&#xff1a; 用户需求挖掘的方法 举例&#xff1a;汽车销售系统的用户访谈-前期准备 用户调研提纲 预约用户做访谈 用户访谈注意点 我们对于干系人做完调研之后需要对用户进行调研&#xff1b;在C端产品常见的用户调研方式外&#xff0c;对B端产品仍然适用的 用户需…