基于Java的分片上传功能
起因:最近在工作中接到了一个大文件上传下载的需求,要求将文件上传到share盘中,下载的时候根据前端传的不同条件对单个或多个文件进行打包并设置目录下载。
一开始我想着就还是用老办法直接file.transferTo(newFile)
就算是大文件,我只要慢慢等总会传上去的。
(原谅我的无知。。)后来尝试之后发现真的是异想天开了,如果直接用普通的上传方式基本上就会遇到以下4个问题:
- 文件上传超时:原因是前端请求框架限制最大请求时长,后端设置了接口访问的超时时间,或者是 nginx(或其它代理/网关) 限制了最大请求时长。
- 文件大小超限:原因在于后端对单个请求大小做了限制,一般 nginx 和 server 都会做这个限制。
- 上传时间过久(想想10个g的文件上传,这不得花个几个小时的时间)
- 由于各种网络原因上传失败,且失败之后需要从头开始。
所以我只能寻求切片上传的帮助了。
整体思路
前端根据代码中设置好的分片大小将上传的文件切成若干个小文件,分多次请求依次上传,后端再将文件碎片拼接为一个完整的文件,即使某个碎片上传失败,也不会影响其它文件碎片,只需要重新上传失败的部分就可以了。而且多个请求一起发送文件,提高了传输速度的上限。
(前端切片的核心是利用 Blob.prototype.slice
方法,和数组的 slice
方法相似,文件的 slice
方法可以返回原文件的某个切片)
接下来就是上代码!
前端代码
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><!-- 引入 Vue --><script src="https://cdn.jsdelivr.net/npm/vue@2.6/dist/vue.min.js"></script><!-- 引入样式 --><link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"><!-- 引入组件库 --><script src="https://unpkg.com/element-ui/lib/index.js"></script><title>分片上传测试</title>
</head><body><div id="app"><template><div><input type="file" @change="handleFileChange" /><el-button @click="handleUpload">上传</el-button></div></template></div>
</body></html>
<script>// 切片大小// the chunk sizeconst SIZE = 50 * 1024 * 1024;var app = new Vue({el: '#app',data: {container: {file: null},data: [],fileListLong: '',fileSize:''},methods: {handleFileChange(e) {const [file] = e.target.files;if (!file) return;this.fileSize = file.size;Object.assign(this.$data, this.$options.data());this.container.file = file;},async handleUpload() { },// 生成文件切片createFileChunk(file, size = SIZE) {const fileChunkList = [];let cur = 0;while (cur < file.size) {fileChunkList.push({ file: file.slice(cur, cur + size) });cur += size;}return fileChunkList;},// 上传切片async uploadChunks() {const requestList = this.data.map(({ chunk, hash }) => {const formData = new FormData();formData.append("file", chunk);formData.append("hash", hash);formData.append("filename", this.container.file.name);return { formData };}).map(({ formData }) =>this.request({url: "http://localhost:8080/file/upload",data: formData}));// 并发请求await Promise.all(requestList);console.log(requestList.size);this.fileListLong = requestList.length;// 合并切片await this.mergeRequest();},async mergeRequest() {await this.request({url: "http://localhost:8080/file/merge",headers: {"content-type": "application/json"},data: JSON.stringify({fileSize: this.fileSize,fileNum: this.fileListLong,filename: this.container.file.name})});},async handleUpload() {if (!this.container.file) return;const fileChunkList = this.createFileChunk(this.container.file);this.data = fileChunkList.map(({ file }, index) => ({chunk: file,// 文件名 + 数组下标hash: this.container.file.name + "-" + index}));await this.uploadChunks();},request({url,method = "post",data,headers = {},requestList}) {return new Promise(resolve => {const xhr = new XMLHttpRequest();xhr.open(method, url);Object.keys(headers).forEach(key =>xhr.setRequestHeader(key, headers[key]));xhr.send(data);xhr.onload = e => {resolve({data: e.target.response});};});}}});
</script>
考虑到方便和通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求
当点击上传按钮时,会调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 50MB,也就是说一个 100 MB 的文件会被分成 2 个 50MB 的切片
createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回
在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片
随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 formData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片
后端代码
实体类
@Data
public class FileUploadReq implements Serializable {private static final long serialVersionUID = 4248002065970982984L;//切片的文件private MultipartFile file;//切片的文件名称private String hash;//原文件名称private String filename;
}@Data
public class FileMergeReq implements Serializable {private static final long serialVersionUID = 3667667671957596931L;//文件名private String filename;//切片数量private int fileNum;//文件大小private String fileSize;
}
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/file")
public class FileController {final String folderPath = System.getProperty("user.dir") + "/src/main/resources/static/file";@RequestMapping(value = "upload", method = RequestMethod.POST)public Object upload(FileUploadReq fileUploadEntity) {File temporaryFolder = new File(folderPath);File temporaryFile = new File(folderPath + "/" + fileUploadEntity.getHash());//如果文件夹不存在则创建if (!temporaryFolder.exists()) {temporaryFolder.mkdirs();}//如果文件存在则删除if (temporaryFile.exists()) {temporaryFile.delete();}MultipartFile file = fileUploadEntity.getFile();try {file.transferTo(temporaryFile);} catch (IOException e) {log.error(e.getMessage());e.printStackTrace();}return "success";}@RequestMapping(value = "/merge", method = RequestMethod.POST)public Object merge(@RequestBody FileMergeReq fileMergeEntity) {String finalFilename = fileMergeEntity.getFilename();File folder = new File(folderPath);//获取暂存切片文件的文件夹中的所有文件File[] files = folder.listFiles();//合并的文件File finalFile = new File(folderPath + "/" + finalFilename);String finalFileMainName = finalFilename.split("\\.")[0];InputStream inputStream = null;OutputStream outputStream = null;try {outputStream = new FileOutputStream(finalFile, true);List<File> list = new ArrayList<>();for (File file : files) {String filename = FileNameUtil.mainName(file);//判断是否是所需要的切片文件if (StringUtils.equals(filename, finalFileMainName)) {list.add(file);}}//如果服务器上的切片数量和前端给的数量不匹配if (fileMergeEntity.getFileNum() != list.size()) {return "文件缺失,请重新上传";}//根据切片文件的下标进行排序List<File> fileListCollect = list.parallelStream().sorted(((file1, file2) -> {String filename1 = FileNameUtil.extName(file1);String filename2 = FileNameUtil.extName(file2);return filename1.compareTo(filename2);})).collect(Collectors.toList());//根据排序的顺序依次将文件合并到新的文件中for (File file : fileListCollect) {inputStream = new FileInputStream(file);int temp = 0;byte[] byt = new byte[2 * 1024 * 1024];while ((temp = inputStream.read(byt)) != -1) {outputStream.write(byt, 0, temp);}outputStream.flush();}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}finally {try {if (inputStream != null){inputStream.close();}} catch (IOException e) {e.printStackTrace();}try {if (outputStream != null){outputStream.close();}} catch (IOException e) {e.printStackTrace();}}// 产生的文件大小和前端一开始上传的文件不一致if (finalFile.length() != Long.parseLong(fileMergeEntity.getFileSize())) {return "上传文件大小不一致";}return "上传成功";}
}
为了图方便我就直接return
字符串了 嘿嘿(当然我在这个demo里面写了方法统一结果的封装,所以输出的时候还是restful风格的结果,详细内容可以看我之前的文章《Spring使用AOP完成统一结果封装》)
当前端调用upload接口的时候,后端就会将前端传过来的文件放到一个临时文件夹中
当调用merge接口的时候,后端就会认为分片文件已经全部上传完毕就会进行文件合并的工作
后端主要是根据前端返回的hash值来判断分片文件的顺序
结尾
其实分片上传听起来好像很麻烦,其实只要把思路捋清楚了其实是不难的,是一个比较简单的需求。
当然这个只是一个比较简单一个demo,只是实现的一个较为简单的分片上传功能,像断点上传,上传暂停这些功能暂时还没来得及写到demo里面,之后有时间了会新开一个文章写这些额外的内容。
下篇文章见啦,喜欢博主的可以点点关注点点赞
相关文章:
基于Java的分片上传功能
起因:最近在工作中接到了一个大文件上传下载的需求,要求将文件上传到share盘中,下载的时候根据前端传的不同条件对单个或多个文件进行打包并设置目录下载。 一开始我想着就还是用老办法直接file.transferTo(newFile)就算是大文件,…...
KDS安装步骤
KDS kinetis design studio 软件 第一步官网(https://www.nxp.com/ 注册账号下载set成功下载软件。 随着AI,大数据这些技术的快速发展,与此有关的知识也普及开来。如何在众多网站中寻找最有价值的信息,如何在最短的时间内获得最新的技…...
JavaSE-线程池(1)- 线程池概念
JavaSE-线程池(1)- 线程池概念 前提 使用多线程可以并发处理任务,提高程序执行效率。但同时创建和销毁线程会消耗操作系统资源,虽然java 使用线程的方式有多种,但是在实际使用过程中并不建议使用 new Thread 的方式手…...
开源代码的寿命为何只有1年?
说实话,如果古希腊的西西弗斯是一个在2016年编写开源代码的开发者,那他会有宾至如归的感觉。著名的西西弗斯处罚,是神话流传下来的,他被迫推一块巨大的石头上山,当登顶之后,只能眼睁睁看着它滚下去…...
完善登录功能--过滤器的使用
系列文章目录 Spring Boot读取配置文件内容的三种方式 Spring Boot自动配置–如何切换内置Web服务器 SpringBoot项目部署 上述为该系列部分文章,想了解更多可看我博客主页哦! 文章目录系列文章目录前言一、创建自定义过滤器LoginCheckFilter二、在启动类…...
CSS基础:属性和关系选择器
字体属性 color 文本颜色 div{ color:red;} div{ color:#ff0000;} div{ color:rgb(255,0,0);} div{ color:rgba(255,0,0,.5);}font-size 文本大小 h1 {font-size:40px;} h2 {font-size:30px;} p {font-size:14px;}注意:chrome浏览器接受最小字体是12px font-we…...
设计模式:原型模式解决对象创建成本大问题
一、问题场景 现在有一只猫tom,姓名为: tom, 年龄为:1,颜色为:白色,请编写程序创建和tom猫属性完全相同的10只猫。 二、传统解决方案 public class Cat {private String name;private int age;private String color;…...
驱动开发(二)
一、驱动流程 驱动需要以下几个步骤才能完成对硬件的访问和操作: 模块加载函数 module_init注册主次设备号 <应用程序通过设备号找到设备>驱动设备文件 <应用程序访问驱动的方式> 1、手动创建 (mknod)2、程序自动创建file_oper…...
《狂飙》大结局,这22句经典台词值得细品
最近爆火的热播剧《狂飙》大家都看了吗? 剧情紧凑、演技炸裂、豆瓣评分9.0,可以说是开年评分最高的一部国产剧。 虽然大结局了。 里面有很多经典台词,值得每个人细细品味。 01 这世界不缺梦想 有本事你就去实现它 02 你这么善良 怎么跟坏…...
【计算机网络期末复习】第二章 物理层
✍个人博客:https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 📣专栏定位:为想复习学校计算机网络课程的同学提供重点大纲,帮助大家渡过期末考~ 📚专栏地址: ❤️如果有收获的话,欢迎点…...
多核异构核间通信-mailbox/RPMsg 介绍及实验
1. 多核异构核间通信 由于MP157是一款多核异构的芯片,其中既包含的高性能的A7核及实时性强的M4内核,那么这两种处理器在工作时,怎么互相协调配合呢? 这就涉及到了核间通信的概念了。 IPCC (inter-processor communication contr…...
【Rust日报】2023-02-11 从头开始构建云数据库 RisingWave - 为什么我们从 C++ 转向 Rust...
GTK4发布v0.60gtk4-rs代码库包含GTK4的Rust crates。还有个庞大的GObject库生态系统,其中许多库基于gtk-rs中包含的Rust绑定工具。 特别是:gtk-rs-core,一些核心库的绑定,例如 glib、gio、pango、graphenegstreamer-rs,…...
Linux驱动开发(一)
linux驱动学习记录 一、背景 在开始学习我的linux驱动之旅之前,先提一下题外话,我是一个c语言应用层开发工作人员,在工作当中往往会和硬件直接进行数据的交互,往往遇到数据不通的情况,常常难以定位,而恰巧…...
Spring MVC 之返回数据(静态页面、非静态页面、JSON对象、请求转发与请求重定向)
文章目录1. 默认情况下返回静态页面2. 返回一个非静态页面的数据2.1 ResponseBody 返回页面内容2.2 RestController ResponseBody Controller3. 实现登录功能,返回 JSON 对象3.1 前端使⽤ ajax,后端返回 json 给前端3.2 前端发送 JSON 的标准格式4. 请…...
leetcode-每日一题-2335(简单,贪心)
自己打表看一下过程就可以发现,其实就是每次选两个大的进行--之后秒数加1即可现有一台饮水机,可以制备冷水、温水和热水。每秒钟,可以装满 2 杯 不同 类型的水或者 1 杯任意类型的水。给你一个下标从 0 开始、长度为 3 的整数数组 amount &am…...
Verilog语法之数学函数
Verilog-2005支持一些简单的数学函数,其参数的数据类型只能是integer和real型。 Integer型数学函数 $clog2是一个以2为底的对数函数,其结果向上取整,返回值典型的格式: integer result; result $clog2(n); 最典型的应用就是通过…...
【手撕面试题】JavaScript(高频知识点一)
目录 面试官:请你简述 var、let、const 三者之间的区别? 面试官:请你谈谈对深拷贝与浅拷贝的理解 面试官:输入URL的那一瞬间浏览器做了什么? 面试官:说一说cookie sessionStorage localStorage 区别&am…...
如何用PHP实现消息推送
什么是消息推送 通过服务器自动推送消息到客户端(浏览器,APP,微信)的应用技术。 2. 为什么要使用消息推送技术 通常情况下都是用户发送请求浏览器显示用户需要的信息。推送技术通过自动传送信息给用户,来减少用于网络上搜索的时间。它根据…...
电子学会2020年6月青少年软件编程(图形化)等级考试试卷(四级)答案解析
青少年软件编程(Scratch)等级考试试卷(四级A卷) 分数:100.00 题数:30 一、单选题(共15题,每题2分,共30分) 1. 执行下图程序后,“花名…...
DaVinci:调色版本
调色版本 Grade Version记录着片段的全部调色信息。将一种调色风格或效果,保存为一个调色版本,从而可在多个调色版本之间查看、比较、挑选或者渲染输出。调色版本类型本地版本Local Versions在没有创建新的调色版本之前,片段的调色信息默认记…...
【C++初阶】十二、STL---反向迭代器的实现
目录 一、反向迭代器 二、反向迭代器的实现 一、反向迭代器 之前的模拟实现vector、list 的时候,这些都是实现了正向迭代器,反向迭代器都没有实现,这里就要实现反向迭代器 反向迭代器也是适配器(配接器)的一种&#…...
day 43|● 1049. 最后一块石头的重量 II ● 494. 目标和 ● 474.一和零
1049. 最后一块石头的重量 II 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x < y。那么粉碎的可能结果…...
[SSD固态硬盘技术 0] SSD的结构和原理导论
版权声明: 本文禁止转载机械硬盘的存储系统由于内部结构,其IO访问性能无法进一步提高,CPU与存储器之间的性能差距逐渐扩大。以Nand Flash为存储介质的固态硬盘技术的发展,性能瓶颈得到缓解。1. 什么是SSD固态硬盘(Solid State Drives…...
Vue (3)
文章目录1. 数据代理1.1 回顾1.2 开始2. 事件处理2.1 v-on:click 点击事件2.2 事件修饰符2.3 键盘事件3. 计算属性3.1 插值语法实现3.2 methods实现3.3 计算属性实现4. 监视属性4.1 深度监视4.2 监视属性的简写形式4.3 watch 与 computed 对比1. 数据代理 在学习 数据代理 时 先…...
SQL语句,常用的DDL表操作语句
-- ddl sql 语句 -- 创建表 create table user_t( id int primary key auto_increment, -- 自增主键 name varchar(50) ); -- 查看表结构 desc user_t; desc user_test; -- 重命名表 alter table user_t rename to user_test; -- 查询数据库表 show tables; -- 添…...
C 语言 宏定义 :字符串化 stringify 的应用
字符串化 通过C 语言的宏(MICRO),可以把数值或者一段字符的组合,转换为字符串。 因为 C语言的宏在【预处理】阶段就展开了,所以可以实现一些比较使用的功能,比如一些数据的初始化操作 比如定义一个宏&…...
代替swagger的api接口神器
自动化API文档-APIFOX 文章作者:老杨 一:概述 大家在后端开发开发过程中,最痛恨的两天事情:1.写文档,2.别人不写文档。而我们后端开发,必定经历的事情就是要和前端&测试对接,我们需要把我…...
2月12日,30秒知全网,精选7个热点
///北京首批29家药店开通异地参保直接结算服务试点药店已覆盖北京市东城区、西城区、朝阳区、海淀区、丰台区和石景山区,为来京就医的外省市参保人员提供便利///杭州召开平台经济健康高质量发展座谈会落实更有针对性的政策供给、提供“店小二”“保姆式”服务、建立…...
HTML img和video object-fit 属性
简介 Css中object-fit主要是应用到img标签和Video标签的,来控制显示缩放效果的。 首先我们存在一张图片,原始图片的尺寸是 1080px x 600px, 展示效果如下: 如果我们的css样式中的img大小设定并不能满足图片的原始大小,比如我们的…...
Pascal版本的 - freopen
参数 filename -- 这是包含要打开的文件的名称的字符串。 mode -- 这是包含文件访问模式的字符串。它包括 - 高级编号模式&说明1个 “r” 打开文件进行读取。该文件必须存在。 2个 “w” 创建一个用于写入的空文件。如果已存在同名文件,则删除其内容并将该文件…...
建立企业网站方案/吉林seo刷关键词排名优化
LSW1的配置 <Huawei>system-view [Huawei]undo info-center enable [Huawei]sysname sw1 [sw1]vlan 2将所有接口设置为中继链路允许所有vlan通信 [sw1]port-group 1 #创建接口组1 [sw1-port-group-1]group-member GigabitEthernet 0/0/1 to GigabitEthernet 0/0/3 …...
做一个中文域名购物网站要多少钱/如何建立个人网址
报错信息如上图所示,解决步骤: 1. 首先是检查这个MainActivity.java是不是真的存在,且包名和路径无误; 2. 如果文件存在,且包名和路径没有问题,那么就打开你项目所在的/android/app/build.gradle文件&#…...
常州便宜的做网站服务/外链工具xg
Python 爬虫模拟登录 Python 爬虫模拟登录是使用脚本实现自动登录,部分站点需要逆向加密逻辑,更多的站点会采用验证码进行鉴权验证。 本文为大家带来最简单的登录,其核心用到的是 requests 模拟 POST 请求。 本次练习使用的站点是 https:/…...
网站seo公司/名风seo软件
基本概念: 对于一个类来说一个类应该只负责一项职责。 单一职责要做的事: 降低类的复杂度,一个类只负责一项职责提高类的可读性,可维护性降低变更引起的风险 另外注意: 当逻辑足够简单时,才可以在代码级别…...
北京通州住房和城乡建设部网站/淘宝seo搜索优化
Log4J的配置文件(Configuration File)就是用来设置记录器的级别、存放器和布局的,它可接keyvalue格式的设置或xml格式的设置信息。通过配置,可以创建出Log4J的运行环境。 1. 配置文件Log4J配置文件的基本格式如下: #配置根Logger log4j.root…...