Springboot怎么优雅实现大文件的上传
前言
在软件工程里,在处理“大”的时候一直是一个难点和难点,如并发大、数据量大、文件大,对硬件进行升级可以解决一些问题,但这并不最聪明的办法,而对于老板来说,这也不是成本最小的办法。作为开发人员来说,在面对类似极端的问题时,只可智取,不可硬刚,最大化利用好现有的资源,以更加优雅的办法来满足用户多样化的需求。今天的主题也是一个“大”的问题,就是大文件如何上传和下载?其实在解决这个问题之前,有一个问题是绕不过去的:什么才是大文件?几兆?几十兆?几百兆?这其实是一个极具争议的问题,在不同的业务场景下,对于大文件的“大”的理解和定义肯定是不同的。但这并不是本文的重点,这篇文章的重点是想和大家分享一下大文件如何优雅实现大文件的上传和下载。
文章示例环境配置信息
jdk版本:1.8
开发工具:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE
实现思路
对于各种“大”的问题,一般都是采用是分而治之的理念进行设计,而对于大文件上传来说,从这一理念出发,具体的方法就是大文件分片和分片的合并。什么是大文件分片呢?什么又是合并分片呢?如果分片上传过程中,个别分片上传失败了,需要重新分片上传吗?文件上传本身是一个很简单的需求,当量级达到一定程度时,就变得异常复杂了,需要考虑的问题点很多。
大文件分片:将要上传的大文件,按照一定的规则大小,将整个文件进行切片,即把一个大的数据文件切分成小的数据块,然后再按照一定的策略(串行或并发)进行上传;

合并分片:大文件分片合并是大文件分片的后续,当一个大文件被切分成小的数据块上传到服务器时,需要把这些小的数据块按照原来的顺序再进行合并,还原成原来的大文件;
分片和合并其实很好理解,类似于现实生活中,如果想把一架大飞机藏在洞口比较小的山洞里,要怎么做呢?肯定是先把飞机拆成小的零件,然后运输到山洞里面,再根据飞机的装配图纸把所有的零件再装好。原理就这么简单。

断点续传:大文件分片上传的时候,各个分片的上传有可能会碰到网络故障,而导致一些分片文件上传失败,如果网络恢复后,可以从上传失败的分片开始上传而直接跳过上传成功的分片文件部分,可以节约时间,提高上传效率,这就是断点上传;如下图:一个大文件被切分成了n个分片,分片上传过程中,分片2和分片3因为某种原因上传失败了,再次上传的时候,会直接跳过所有上传成功的分片,直接从失败的分片2和分片3开始上传;

实现原理
1、前端使用百度的WebUploader组件,选中待上传的大文件,然后计算出文件的md5值;
2、WebUploader组件开启分片上传后,选中的待上传文件会会按照配置好的分片规则进行分片,分片文件上传前会计算出分片文件的md5值;
3、webuploader组件api计算出分片的md5值后,会携带文件md5值和分片文件的md5值,调用后台接口检验当前分片是否已经上传过;若上传过,则直接跳过;若未上传过,则开始分片上传;
3、前端往后端传输分片文件的过程可以是并发执行,这里一定注意传递到后台的分片并不是按照分片的顺序来的,后端收到分片文件后,会保存分片文件到硬盘、网盘等存储介质上,同是持久化分片文件md5值、文件md5值;
4、待所有的分片上传成功后,会触发WebUploader的uploadSuccess事件,然后再发起合并分片请求;
5、后端再次检查所有的分片是否上传完整,若上传完整,则开始合并所有分片;
md5消息摘要算法,属Hash算法一类,主要特点是不可逆,相同数据的md5值肯定一样,不同数据的md5值不一样;对于数据文件,不管文件名字是否相同,如果数据文件内容相同,则文件的md值是相同的;
webuploader组件提供了文件的md5值的计算方法,其计算过程是异步的;

代码实现
大文件分片上传的实现原理,实际上比较简单和清晰的,那么落到代码实现上,还有几个问题需要解决:
1、webuploader分片上传怎么开启?
2、文件的md5值如何计算?
3、文件如合分片?
4、检验分片是否上传的业务逻辑是什么?
5、分片上传的的请求在哪里触发?
6、合并文件分片的请求在哪里触发?
1、webuploader分片上传怎么开启?
webuploader的分片上传开启实际上很简单,在创建webuploader对象时,设置chunked为true,即表示开启分片上传;chunkSize可以设置分片大小,即以多大的体积进行分片;chunkRetry可以设置重传次数,有的时候由于网络原因,分片上传的会失败,这里即是失败允许重的次数;threads可以设置允许最大由几个进程发起上传请求;
uploader = WebUploader.create({// swf文件路径swf: 'http://localhost:8080/lib/Uploader.swf',// 分片文件上传接口server: 'http://localhost:8080/file/upload',// 选择文件的按钮。可选。pick: '#picker',fileVal: 'multipartFile',//后端用来接收上传文件的参数名称chunked: true,//开启分片上传chunkSize: 1024 * 1024 * 10,//设置分片大小chunkRetry: 2,//设置重传次数,有的时候由于网络原因,分片上传的会失败,这里即是失败允许重的次数threads: 3//允许同时最大上传进程数
});2、文件的md5值如何计算?
文件的md5计算可以引用spark-md5.js,据传言是javascript里md5加密计算速度最快的,当然在webuploader.js里也有具体的api可以使用,引入webuploader.js后,调用 WebUploader.Uploader.md5File(...)即可计算文件的md5值,这里需要注意的是md5File(...),有三个参数,分别是file,数据起始索引位置、数据结束索引位置,返回的是一个promise对象,要想拿到具体的值还要再调用then(function(val){}),具体逻辑如下:
1、当添加完文件后,webuploader的fileQueued事件被触发;
2、fileQueued事件触发的业务逻辑主要是计算出文件的的md5值,这里注意是计算出整个文件的md5值,而不是一部分,关键是md5File(...)方法的后两个参数数据起始索引位置、数据结束索引位置;md5的计算过程是异步操作,并且文件越大,计算用时越长;
3、deferred的作用就是监控异步计算文件md5值这个异步操作的执行状态;
4、文件md5值计算完成后,更新状态为已完成,这时 deferred.done()会触发;
5、文件md5值计算完成,更新md5计算标志位为true,这时再点击开始上传按钮时,就会再有弹窗提示:md5计算中...,请稍侯;
webuploader内部有很多种command,其中有一个叫before-send-file,也可以在文件上传前会触发,此时还没有开始分片,可以用来做文件整体的md5计算,但是不建议用这个,因为before-send-file的触发时机是要晚于fileQueued事件的;before-send-file是在点击开始上传按钮执行uploader.upload()后才会触发;而fileQueued事件是在选择文件后,立刻触发,不用等到点击开始上传按钮;
/*** 当有文件被添加进队列后触发* 主要逻辑:1、文件被添加到队列后,开始计算文件的md5值;* 2、md5的计算过程是异步操作,并且文件越大,计算用时越长;* 3、变量md5FlagMap是文件md5值计算的标志位,计算完成后,设置当前文件的md5Flag为true*///md5FlagMap用于存储文件md5计算完成的标志位;多个文件时,分别设置标志位,key是文件名,value是true或false;
var md5FlagMap = new Map();
uploader.on('fileQueued', function (file) {md5FlagMap.set(file.name, false);//文件md5值计算的标志位默认为falsevar deferred = WebUploader.Deferred();//deferred用于监控异步计算文件md5值这个异步操作的执行状态uploader.md5File(file, 0, file.size - 1).then(function (fileMd5) {file.wholeMd5 = fileMd5;file_md5 = fileMd5;deferred.resolve(file.name);//文件md5值计算完成后,更新状态为已完成,这时 deferred.done()会触发})//文件越大,文件的md5值计算用时越长,因此md5的计算搞成异步执行是合理的;如果异步执行比较慢的话,会顺序执行到这里$('#thelist').append('<div id="' + file.id + '" class="item">' +'<h4 class="info">' + file.name + '</h4>' +'<p class="state">开始计算大文件的md5......<br/></p>' +'</div>')//文件的md5计算完成,会触发这里的回调函数,deferred.done(function (name) {md5FlagMap.set(name, true);//更新md5计算标志位为true$('#' + file.id).find('p.state').append('大文件的md5计算完成<br/>');})return deferred.promise();
})3、文件如何分片?
webuploader对象中配置好相关的开启分片设置参数后,当有文件被选中添加后,webuploader会帮你对文件按配置参数进行分片;在分片文件发送到后台之前,webuploader内部另一个command(before-send)会触发,before-send的触发时机上分片上传之前,可以用作在分片发送到之前,计算出分片文件的md5,调用后台接口做分片是否已经上传的验证:如果分片已经上传成功了,直接跳过,不会再调用分片的上传接口;如果分片未上传,则会把分片的md5值赋给分片block上,用于下次上传时分片是否已经上传的校验;
WebUploader.Uploader.register({"add-file": "addFile","before-send-file": "beforeSendFile","before-send": "beforeSend","after-send-file": "afterSendFile"
}, {addFile: function (file) {console.log('1', file)},beforeSendFile: function (file) {console.log('2', file)},beforeSend: function (block) {console.log(3)var file = block.file;var deferred = WebUploader.Base.Deferred();(new WebUploader.Uploader()).md5File(file, block.start, block.end).then(function (value) {$.ajax({url: 'http://localhost:8080/file/check',//检查当前分片是否已经上传method: 'post',data: {chunkMd5: value, fileMd5: file_md5,chunk:block.chunk},success: function (res) {if (res) {deferred.reject();} else {deferred.resolve(value);}}});})deferred.done(function (value) {console.log('分片md5:', value)block.chunkMd5 = value;})return deferred;},afterSendFile: function (file) {console.log('4', file)}
})4、检验分片是否上传的业务逻辑是什么?
在webuploader内部的一个command(before-send)已经完成了分片文件的md5计算以及请求后台接口来校验当前分片文件是否已经上传,如果已上传,那么会直接跳过当前分片上传接口的调用,uploadBeforeSend事件也不会再触发(当某个分片文件在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次);

如果未上传,则uploadBeforeSend事件会触发,携带一些分片的参数信息发起分片上传请求;
// 分片模式下,当文件的分块在发送前触发
uploader.on('uploadBeforeSend', function (block, data) {var file = block.file;console.log('uploadBeforeSend:', block.chunkMd5)var file = block.file;data.originalFilename = file.originalFilename;data.md5Value = file.wholeMd5;data.start = block.start;data.end = block.end;data.chunk = block.chunk;data.chunks = block.chunks;data.chunkMd5 = block.chunkMd5;
});
在后台检验分片是否上传的逻辑很简单:
1、在分片文件上传接口中,这里使用redis缓存了分片md5和文件md5以及其他相关的一些分片信息,用到hash数据结构,key为文件的md5,hashkey是“chunk_md5_”+分片索引,value就是分片文件的md5值;(这里也可以使用数据库来存储)
2、接口被调用的时候,根据前端传过来的当前分片的索引位置取出分片的md5与前端传过来的分片文件md5进行比较,如果相同,则说明当前分片已经上传成功;如果不相同,则说明未上传过;
@PostMapping("/check")
public boolean check(String fileMd5,String chunk,String chunkMd5) {Object o = redisTemplate.opsForHash().get(fileMd5, "chunk_md5_"+chunk);if (chunkMd5.equals(o)) {return true;}return false;
}
/*** 分片上传接口* @param request* @param multipartFile* @return* @throws IOException*/
@PostMapping("/upload")
public String upload(HttpServletRequest request, MultipartFile multipartFile) throws IOException {log.info("分片上传....");Map<String, String> requestParam = this.doRequestParam(request);String md5Value = requestParam.get("md5Value");//整体文件的md5值String chunkIndex = requestParam.get("chunk");//分片在所有分片文件中索引位置Object chunkLocation = redisTemplate.opsForHash().get(md5Value, "chunk_md5_" + chunkIndex);if (chunkLocation != null) {log.info("分片已上传:"+md5Value+"_"+chunkIndex);return "success";}String end = requestParam.get("end");//当前分片在整个数据文件中的结束位置String chunks = requestParam.get("chunks");//文件总共被分了多少片String fileSize = requestParam.get("size");//文件大小String chunkMd5 = requestParam.get("chunkMd5");//分片文件的md5值String userDir = System.getProperty("user.dir");String chunkFilePath = userDir + File.separator + chunkMd5 + "_" + chunkIndex;File file = new File(chunkFilePath);multipartFile.transferTo(file);Map<String,String> map=new HashMap<>();map.put("chunk_location_"+chunkIndex,chunkFilePath);//分片存储路径map.put( "chunk_end_" + chunkIndex, end);map.put("file_size",fileSize);map.put("file_chunks",chunks);map.put("chunk_md5_"+chunkIndex,chunkMd5);redisTemplate.opsForHash().putAll(md5Value,map);return "success";
}5、分片上传的的请求在哪里触发?
当选择文件后,就开始计算整体文件的md5值了,未计算完成前,点击开始上传按钮,会直接弹出“md5计算中...,请稍侯”;考虑到多文件上传的情况,这里使用了map对象md5FlagMap来存储每个文件的标志,key是文件名称,value是true或false,表示分片文件md5文件是否计算完成;如果不想用按钮来触发上传,WebUploader有一个参数是auto,默认是false,可以改为true,选中文件后自动开始上传;
//开始上传按钮被点击时触发
$('#ctlBtn').click(function () {console.log('ctlBtn')//md5FlagMap存储有文件md5计算的标志位;// 同时上传多个文件时,上传前要判断一下文件的md5是否计算完成,// 如果有未计算完成的,则继续等待计算结果;//文件上传标志位,如果多个文件有一个没有完成md5计算则不能开始上传;这里在实际业务中可以更换成其他交互样式,酌情优化为哪个文件的md5计算完成,则开始哪个文件的上传;var uploadFloag = true;md5FlagMap.forEach(function (value, key) {if (!value) {uploadFloag = false;alert('md5计算中...,请稍侯')//文件md5计算未完成,会弹出弹窗提示;}})if (uploadFloag) {uploader.upload();//文件md5计算完成后,开始分片上传;}
})
相关文章:
Springboot怎么优雅实现大文件的上传
前言在软件工程里,在处理“大”的时候一直是一个难点和难点,如并发大、数据量大、文件大,对硬件进行升级可以解决一些问题,但这并不最聪明的办法,而对于老板来说,这也不是成本最小的办法。作为开发人员来说…...
2月编程语言排行榜新鲜出炉,谁又摘得桂冠?
近日,TIOBE公布了2023年2月编程语言排行榜,本月各个语言表现如何?谁又摘得桂冠?一起来看看吧! TIOBE 2月Top15编程语言: 详细榜单查看TIOBE官网 https://www.tiobe.com/tiobe-index/ 关注IT行业的小伙伴…...
机器学习中的数学原理——模型评估与交叉验证
惭愧惭愧!机器学习中的数学原理这个专栏已经很久没有更新了!前段时间一直在学习深度学习,paddlepaddle,刷题专栏跟新了,这个专栏就被打入冷宫了。这个专栏名为白话机器学习中数学学习笔记,主要是用来分享一…...
JAVA开发(JSP的9大内置对象和4大作用域)
背景: 在springboot横行的javaweb开发中,现在的后端开发工程师基本不需要写前端JSP页面。但是作为web开发工程师,不懂JSP的原理和作用,几乎是不行的。 JSP技术介绍: JSP(全称Java Server Pagesÿ…...
(4)EKF失控保护
文章目录 前言 4.1 什么时候会触发? 4.2 当失控保护触发时,会发生什么?...
数论----质数的求解(C/C++)
CSDN的uu,你们好呀,今天我们要学习的内容是数论哦!这也是算法题中的一类题目吧。记好安全带,准备发车咯!🚀学习数论的意义📢算法导论说:“数论曾经被视为一种虽然优美但却没什么用处…...
【电赛MSP430系列】GPIO、LED、按键、时钟、中断、串口、定时器、PWM、ADC
文章目录MSP430一、GPIO二、点亮LED三、按键控制LED四、更改主时钟五、串口通信六、串口中断七、外部中断八、定时器九、定时器中断十、PWM十一、ADCMSP430 MSP430 是德州仪器(TI)一款性能卓越的超低功耗 16 位单片机,自问世以来,…...
【Linux】进程理解与学习(Ⅱ)
环境:centos7.6,腾讯云服务器Linux文章都放在了专栏:【Linux】欢迎支持订阅🌹相关文章推荐:【Linux】冯.诺依曼体系结构与操作系统【Linux】进程理解与学习(Ⅰ)浅谈Linux下的shell--BASH前言章节…...
vscode 爽到起飞的快捷键
这里写目录标题1. 窗口操作2. 代码编辑3. 批量操作4. 错误处理1. 窗口操作 文件之间切换: CtrlTab 切出一个新的编辑器窗口(最多3个): Ctrl\ 切换左中右3个编辑器窗口的快捷键: Ctrl1 Ctrl2 Ctrl3 2. 代码编辑 代码格式化: ShiftAltF 向上或向下移动一行: Alt…...
vs +qt 打包.cpp和.h为DLL文件
文章目录一 编译成库1 创建一个Qt library 项目2,将已有的文件拷贝到项目目录下3 在项目中添加现有项4,拷贝头文件到需要暴露给外面使用的类的头文件中5 拷贝xxx_EXPORT的宏到需要被暴露的类的名前面6 然后点击编译 就完成了。得到的dll文件在debug里面二…...
echarts有滑块
vue下使用echarts折线图及其横坐标拖拽功能 drawLine() {let that this,lineDate [],dispatchCount [],finishCount [],newCount [];let param {// 参数};axios.post(url, param).then(function(response) {let rs response.data.data;if (rs ! undefined && rs…...
MATLAB绘制ROC曲线
ROC曲线(Receiver Operating Characteristic Curve) 1 简介 ROC曲线是用于评估二元分类模型(如Logistic回归)表现优劣的一种工具,其横轴表示假阳性率(false positive rate,FPR),即实际为负例但…...
ChatGPT前传
文章目录前言GPT概述GPT-1代GPT-1 学习目标和概念介绍GPT-1 训练数据集GPT-1 模型结构和应用细节GPT-1 效果性能和总结GPT-2代GPT-2 学习目标和概念介绍GPT-2 训练数据集GPT-2 模型结构和应用细节GPT-2 性能效果和总结GPT-3代GPT-3 学习目标和概念介绍GPT-3 训练数据集GPT-3 模…...
我的十年编程路 2020年篇
我出生在1990年,2020年到来的时候,我完成了一项成就:奔三。同时,也开启了新的征程:奔四。 2020年的春节是在广州的丈母娘家度过的,春节后大概是初五,或者是初六,我和媳妇就返回天津…...
力扣-SQL【入门】
https://leetcode.cn/study-plan/sql/?progressxhqm4sjh 目录选择595. 大的国家1757. 可回收且低脂的产品584. 寻找用户推荐人183. 从不订购的客户排序 & 修改1873. 计算特殊奖金627. 变更性别196. 删除重复的电子邮箱选择 595. 大的国家 # Write your MySQL query state…...
Vue中组件到底是什么
1.先说结论: Vue中组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。 2.我们使用组件时发生了什么? 比如定义了一个school,然后在页面上使用它 我们只需要写 < school/ > 或< school &…...
不同时间间隔数据对统计结果的影响
目录摘要1. 实测数据来源2. 数据分析方法3 结果分析3.1 波况分析摘要 采用不同的波浪观测方法所获得的波浪数据的时间间隔不一致,其数据的准确性须进行分析。基于大埕湾逐时周年波浪观测数据,截取不同时间间隔的波浪数据,采用统计和相关分析…...
hudi系列-数据写入方式及使用场景
hudi支持多种数据写入方式:insert、bulk_insert、upsert、boostrap,我们可以根据数据本身属性(append-only或upsert)来选择insert和upsert方式,同时也支持对历史数据的高效同步并嫁接到实时流程。 这里的使用技术组合为flink + hudi-0.11 upsert 这是hudi默认的写入方式,…...
C # FileStream文件流
本章讲述:FileStream类的基本功能,以及简单示例; 1、引用命名空间:using System.IO; 2、注意:使用IO操作文件时,要注意流关闭和释放问题! 强力推荐:将创建文件流对象的过程写在usi…...
Go语言中的保留字和运算符详解
前言 🏠个人主页:我是沐风晓月 🧑个人简介:大家好,我是沐风晓月,双一流院校计算机专业,阿里云博客专家 😉😉 💕 座右铭: 先努力成长自己ÿ…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
【Python】 -- 趣味代码 - 小恐龙游戏
文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
基于IDIG-GAN的小样本电机轴承故障诊断
目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) 梯度归一化(Gradient Normalization) (2) 判别器梯度间隙正则化(Discriminator Gradient Gap Regularization) (3) 自注意力机制(Self-Attention) 3. 完整损失函数 二…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
WebRTC调研
WebRTC是什么,为什么,如何使用 WebRTC有什么优势 WebRTC Architecture Amazon KVS WebRTC 其它厂商WebRTC 海康门禁WebRTC 海康门禁其他界面整理 威视通WebRTC 局域网 Google浏览器 Microsoft Edge 公网 RTSP RTMP NVR ONVIF SIP SRT WebRTC协…...
Mac flutter环境搭建
一、下载flutter sdk 制作 Android 应用 | Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 1、查看mac电脑处理器选择sdk 2、解压 unzip ~/Downloads/flutter_macos_arm64_3.32.2-stable.zip \ -d ~/development/ 3、添加环境变量 命令行打开配置环境变量文件 ope…...
