文件上传——springboot大文件分片多线程上传功能,前端显示弹出上传进度框
一、项目搭建
-
创建 Spring Boot 项目: 创建一个新的 Spring Boot 项目,添加 Web 依赖。
-
添加依赖: 在
pom.xml文件中添加以下依赖:
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
<dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version>
</dependency>
二、后端实现
-
配置 MultipartResolver: 在 Spring Boot 配置类中添加以下代码:
@Configuration
public class MyWebAppConfigurer implements WebMvcConfigurer {@Beanpublic MultipartResolver multipartResolver() {CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();multipartResolver.setMaxUploadSize(-1); // 设置最大上传大小,-1 表示无限制return multipartResolver;}
}
-
创建 FileUploadService: 创建一个服务类,用于处理文件上传逻辑:
@Service
public class FileUploadService {private String uploadDir = "upload/"; // 设置上传目录public String initUpload(String fileName, long fileSize, int chunkSize) {// 1. 生成任务 ID (UUID)String fileId = UUID.randomUUID().toString();//这里根据实际情况考虑到断点续传的功能,同一个文件生成的标识要一样,后期可以根据这个判断文件上传进度// 2. 创建临时目录: uploadDir/fileIdFile dir = new File(uploadDir, fileId);if (!dir.exists()) {dir.mkdirs();}// 3. 返回 fileIdreturn fileId;}public String uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile file) throws IOException {String fileUrl="";// 1. 获取分片文件String fileName = file.getOriginalFilename();// 2. 保存分片到临时目录: uploadDir/fileId/chunkIndexFile chunkFile = new File(uploadDir, fileId + "/" + chunkIndex);file.transferTo(chunkFile);// 检查所有分片是否已上传完成if (allChunksUploaded(fileId, totalChunks)) {String fileName=datePath()+"/"+fileId;//这里可以不用文件名,如果需要用,则要在controller上传请求增加一个fileName参数// 合并分片fileUrl = mergeChunks(fileId, fileName);}// 3. 校验分片 MD5 (可选)return fileUrl;}//判断所有的分片是否都上传完毕private boolean allChunksUploaded(String fileId, int totalChunks) {for (int i = 0; i < totalChunks; i++) {File chunkFile = new File(uploadDir + fileName + ".chunk" + i);if (!chunkFile.exists()) {return false;}}return true;}public String mergeChunks(String fileId, String fileName) throws IOException {// 1. 获取所有分片文件File dir = new File(uploadDir, fileId);File[] chunkFiles = dir.listFiles();// 2. 按顺序合并分片File mergedFile = new File(uploadDir, fileName);try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {for (File chunkFile : chunkFiles) {try (FileInputStream fis = new FileInputStream(chunkFile)) {IOUtils.copy(fis, fos);}}}// 3. 删除临时目录FileUtils.deleteDirectory(dir);// 4. 校验文件 MD5 (可选)// 5. 返回文件存储路径return uploadDir + fileName;}/*** 日期路径 即年/月/日 如2018/08/08*/public static final String datePath(){Date now = new Date();return DateFormatUtils.format(now, "yyyy/MM/dd");}
}
-
创建 FileUploadController: 创建一个控制器类,用于处理文件上传请求:
要在分片上传的基础上实现断点续传,需要在服务端记录每个文件的上传进度,并在客户端请求上传时返回已上传的分片信息。
- 使用数据库或其他存储机制记录每个文件的上传进度。
- 可以使用以下信息标识一个上传任务:
- fileId: 全局唯一标识符,例如 UUID
- 同一个文件的标识是一样的,这样保证接这上次的进度继续上传。
@RestController
public class FileUploadController {@Autowiredprivate FileUploadService fileUploadService;private final ExecutorService executorService = Executors.newFixedThreadPool(5); // 线程池大小可配置private final Map<String, Set<Integer>> uploadProgress = new HashMap<>(); // 使用内存存储上传进度,实际应用中建议使用数据库@PostMapping("/upload/init")public ResponseEntity<String> initUpload(@RequestParam("fileName") String fileName,@RequestParam("fileSize") long fileSize,@RequestParam("chunkSize") int chunkSize) {String fileId = fileUploadService.initUpload(fileName, fileSize, chunkSize);return ResponseEntity.ok(fileId);}@PostMapping("/upload/chunk")public ResponseEntity<Void> uploadChunk(@RequestParam("fileId") String fileId,@RequestParam("chunkIndex") int chunkIndex, @RequestParam("totalChunks") int totalChunks,@RequestParam("file") MultipartFile file) throws IOException {String filePath = "";try {// 获取或创建上传进度记录Set<Integer> uploadedChunks = uploadProgress.computeIfAbsent(fileId, k -> new HashSet<>());// 如果分片已上传,则跳过if (uploadedChunks.contains(chunkIndex)) {return ResponseEntity.ok(new UploadResponse(uploadedChunks));}// 使用线程池处理每个分片上传executorService.execute(() -> {try {filePath = fileUploadService.uploadChunk(fileId, chunkIndex, totalChunks, file);} catch (IOException e) {// 处理异常,例如记录日志或返回错误信息e.printStackTrace();}});return ResponseEntity.status(HttpStatus.ACCEPTED).body(new UploadResponse(uploadedChunks, filePath));} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error uploading chunk.");}//return ResponseEntity.ok().build();}}// 用于响应上传请求,携带已上传分片信息
class UploadResponse {Set<Integer> uploadedChunks;String filePath;public UploadResponse(Set<Integer> uploadedChunks, String filePath) {this.uploadedChunks = uploadedChunks;this.filePath = filePath;}public UploadResponse(Set<Integer> uploadedChunks) {this.uploadedChunks = uploadedChunks;}// ... getter setter 方法 ...
}
三、前端实现
-
HTML 页面: 创建一个简单的 HTML 页面,包含文件选择按钮、上传进度条和相关信息展示区域。
-
JavaScript 代码: 使用 JavaScript 实现文件分割、分片上传、合并请求和上传进度展示等功能。
// 选择文件
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {const file = event.target.files[0];// ... 文件分割、上传逻辑
});// 文件分割
const chunkSize = 4 * 1024 * 1024; // 4MB
const chunks = sliceFile(file, chunkSize);const totalChunks = Math.ceil(file.size / chunkSize);// 初始化上传
const fileId = await initUpload(file.name, file.size, chunkSize);// 并发上传分片
const uploadPromises = chunks.map((chunk, index) => uploadChunk(fileId, index, totalChunks, chunk)
);// 上传完成合并得到文件url
await Promise.all(uploadPromises);async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) {// ... 创建 FormData ...const formData = new FormData();formData.append('file', chunk);formData.append('chunkIndex', chunkIndex);formData.append('totalChunks', totalChunks);formData.append('fileId', fileId); // 添加 fileId 参数const response = await fetch('/upload', {method: 'POST',body: formData,});//...处理响应数据
}function sliceFile(file, chunkSize) {let chunks = [];let count = Math.ceil(file.size / chunkSize);for (let i = 0; i < count; i++) {let offset = i * chunkSize;let chunk = file.slice(offset, offset + chunkSize + 1);chunks.push(chunk);}return chunks;
}
四、上传进度展示
-
后端: 在
FileUploadService中添加方法,根据fileId返回已上传分片数量或计算上传进度百分比。 -
前端: 使用
setInterval定时请求后端获取上传进度,并更新进度条。
前端html代码使用示例:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>文件上传</title>
</head>
<body>
<h1>大文件分片上传</h1>
<input type="file" id="fileInput">
<button id="uploadBtn">上传</button>
<div>上传进度:<progress id="progressBar" value="0" max="100"></progress> <span id="progressText">0%</span></div>
<script>const fileInput = document.getElementById('fileInput');const uploadBtn = document.getElementById('uploadBtn');const progressBar = document.getElementById('progressBar');const progressText = document.getElementById('progressText');uploadBtn.addEventListener('click', uploadFile);async function uploadFile() {const file = fileInput.files[0];if (!file) {alert('请选择文件');return;}const chunkSize = 4 * 1024 * 1024; // 4MBconst fileId = await initUpload(file.name, file.size, chunkSize);const chunks = sliceFile(file, chunkSize);let uploadedChunks = 0;const uploadPromises = chunks.map((chunk, index) => {return uploadChunk(fileId, index, chunks.length, chunk).then(() => {uploadedChunks++;updateProgress(uploadedChunks / chunks.length);});});await Promise.all(uploadPromises);await mergeChunks(fileId);alert('上传完成!');}function sliceFile(file, chunkSize) {const chunks = [];let offset = 0;while (offset < file.size) {chunks.push(file.slice(offset, offset + chunkSize));offset += chunkSize;}return chunks;}async function initUpload(fileName, fileSize, chunkSize) {const response = await fetch('/upload/init', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ fileName, fileSize, chunkSize })});return await response.text();}async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) {const formData = new FormData();formData.append('fileId', fileId);formData.append('chunkIndex', chunkIndex);formData.append('totalChunks', totalChunks);formData.append('file', chunk);await fetch('/upload/chunk', {method: 'POST',body: formData});}async function mergeChunks(fileId) {await fetch(`/upload/merge?fileId=${fileId}`);}function updateProgress(progress) {progressBar.value = progress * 100;progressText.textContent = Math.round(progress * 100) + '%';}
</script>
</body>
</html>
五、存储上传进度
使用 Redis 存储上传进度
-
Redis 数据结构:
- 使用 Hash 结构存储每个文件的上传进度,key 为 fileId和chunkIndex,chunkIndex 为分片索引,value 为 true 或 false,表示分片是否已上传。
-
代码实现:
- 注入
RedisTemplate:
@Autowired
private RedisTemplate<String, String> redisTemplate;
- 修改
uploadChunk方法:
private void uploadChunk(String fileId, int chunkIndex, int totalChunks, MultipartFile file) throws IOException {// ... 保存分片文件 ...// 更新上传进度到 RedisString chunkKey = fileId + ":" + chunkIndex;redisTemplate.opsForValue().set(chunkKey, "true");// 检查所有分片是否已上传完成if (redisTemplate.opsForHash().size(fileId) == totalChunks) {// ... 合并分片 ...// 清除上传进度redisTemplate.delete(fileId);}
}
- 添加
/upload/progress接口:
@GetMapping("/upload/progress")
public ResponseEntity<UploadResponse> getUploadProgress(@RequestParam("identifier") String identifier
) {Set<String> uploadedChunks = redisTemplate.keys(identifier + ":*");Set<Integer> uploadedChunkIndices = uploadedChunks.stream().map(s -> Integer.parseInt(s.substring((identifier + ":").length()))).collect(Collectors.toSet());return ResponseEntity.ok(new UploadResponse(uploadedChunkIndices));
}
- 前端js使用
// ... 其他代码 ...
// 选择文件
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {const file = event.target.files[0];// ... 文件分割、上传逻辑
});// 文件分割
const chunkSize = 4 * 1024 * 1024; // 4MB
const chunks = sliceFile(file, chunkSize);
const totalChunks = Math.ceil(file.size / chunkSize);// 初始化上传
const fileId = await initUpload(file.name, file.size, chunkSize);async function uploadFile(chunks) {// 获取已上传的分片信息const uploadedChunks = await getUploadedChunks(fileId, file.name);// ... 根据 uploadedChunks 调整分片上传逻辑 ...if(...){//获取uploadedChunks为分片的索引,表示当前文件上传的进度,根据uploadedChunks的具体数据类型去取for (let i = (uploadedChunks); i < chunks.length; i++) {//const start = i * chunkSize;//const end = Math.min(start + chunkSize, file.size);//const chunk = file.slice(start, end);const chunk = chunks[i];await uploadChunk(fileId, i, totalChunks, chunk);}}else{// 并发上传分片chunks.map((chunk, index) => uploadChunk(fileId, index, totalChunks, chunk));}
}async function getUploadedChunks(fileId, fileName) {const response = await fetch(`/upload/progress?identifier=${fileId}&fileName=${fileName}`);const data = await response.json();return data.uploadedChunks || [];
}async function uploadChunk(fileId, chunkIndex, totalChunks, chunk) {// ... 创建 FormData ...const formData = new FormData();formData.append('file', chunk);formData.append('chunkIndex', chunkIndex);formData.append('totalChunks', totalChunks);formData.append('fileId', fileId); // 添加 fileId 参数const response = await fetch('/upload', {method: 'POST',body: formData,});//...处理响应数据
}function sliceFile(file, chunkSize) {let chunks = [];let count = Math.ceil(file.size / chunkSize);for (let i = 0; i < count; i++) {let offset = i * chunkSize;let chunk = file.slice(offset, offset + chunkSize + 1);chunks.push(chunk);}return chunks;
}
重新上传获取进度:
- 用户重新选择同一个文件上传时,需要生成相同的 f
ileId。 - 在前端上传前,调用
/upload/progress接口,传入 fileId 获取已上传的分片信息。 - 根据返回的已上传分片信息,跳过已上传的分片,继续上传剩余分片。
六、注意事项
- 以上代码示例省略了部分细节,例如异常处理、MD5 校验等,请根据实际情况进行完善。
- 前端代码需要根据您使用的 JavaScript 框架进行调整。
- 建议您先学习 Spring Boot 文件上传、JavaScript 文件操作和 AJAX 等前端相关知识。
希望这些更详细的步骤和代码片段能够帮助您更好地理解和实现 Spring Boot 断点续传、多线程分片上传功能! 如果您还有其他问题,请随时提出。
相关文章:
文件上传——springboot大文件分片多线程上传功能,前端显示弹出上传进度框
一、项目搭建 创建 Spring Boot 项目: 创建一个新的 Spring Boot 项目,添加 Web 依赖。 添加依赖: 在 pom.xml 文件中添加以下依赖: <dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId&…...
每日学术速递8.2
1.A Scalable Quantum Non-local Neural Network for Image Classification 标题: 用于图像分类的可扩展量子非局部神经网络 作者: Sparsh Gupta, Debanjan Konar, Vaneet Aggarwal 文章链接:https://arxiv.org/abs/2407.18906 摘要&#x…...
SAP-PLM创建物料主数据接口
FUNCTION zplm_d_0001_mm01. *"---------------------------------------------------------------------- *"*"本地接口: *" EXPORTING *" VALUE(EX_TOTAL) TYPE CHAR4 *" VALUE(EX_SUCCESSFUL) TYPE CHAR4 *" …...
超声波眼镜清洗机哪个品牌好?四款高性能超声波清洗机测评剖析
对于追求高生活质量的用户来说,眼镜的清洁绝对不能马虎。如果不定期清洁眼镜,时间久了,镜片的缝隙中会积累大量的灰尘和细菌,眼镜靠近眼部,对眼部健康有很大影响。在这种情况下,超声波清洗机显得尤为重要。…...
卸载Windows软件的正确姿势,你做对了吗?
前言 今天有小伙伴突然问我:她把软件都卸载了,但是怎么软件都还在运行? 这个问题估计很多小伙伴都是遇到过的,对于电脑小白来说,卸载Windows软件真的真的真的是一件很难的事情。所以,今天咱们就来讲讲&am…...
WEB前端14-Element UI(学生查询表案例/模糊查询/分页查询)
Vue2-Element UI 1.可重用组件的开发 可重用组件 我们一般将可重复使用的组件放在components目录之下,以便父组件的灵活调用 <!--可重用组件一般与css密切相关,使用可重用组件的目的是,将相似的组件放在一起,方便使用-->…...
使用swiftui自定义圆形进度条实现loading
实现的代码如下: // // LoadingView.swift // SwiftBook // // Created by Song on 2024/8/2. //import SwiftUIstruct LoadingView: View {State var process 0.5var body: some View {VStack(spacing: 20) {ZStack {Circle().stroke(.gray.opacity(0.3), lin…...
C# 设计模式之抽象工厂模式
总目录 前言 工厂方法模式是为了克服简单工厂模式的缺点而设计出来的,简单工厂模式的工厂类随着产品类的增加需要增加额外的代码,而工厂方法模式每个具体工厂类只完成单个实例的创建,所以它具有很好的可扩展性。但是在现实生活中,…...
Javascript前端面试基础(八)
window.onload和$(document).ready区别 window.onload()方法是必须等到页面内包括图片的所有元素加载完毕后才能执行$(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕 window.onload 触发时机:window.onload 事件会在整个页面…...
R 语言学习教程,从入门到精通,R的安装与环境的配置(2)
1、R的安装与环境的配置 R语言是一款完全免费且开源的软件,它的开源许可证是GNU通用公共许可证(GPL),这意味着任何人都可以自由地使用、复制、修改和发布R语言的源代码,甚至可以将其用于商业用途。 和python等其他语言…...
Python批量下载音乐功能
Python批量下载音乐功能 Python批量下载音乐,调用API接口,同时下载歌曲和歌词 先安排一下要用的模块,导入进来。 import re import json import requests目录结构 下载音乐 Awking_Class.pymusic.txt 文件文件写的是音乐名字,使用换行分割 new_music 注意这个 ne…...
用 Bytebase 实现批量、多环境、多租户数据库的丝滑变更
Bytebase 提供了多种功能来简化批量变更管理,适用于多环境或多租户情况。本教程将指导您如何使用 部署配置 和 数据库组 在不同场景下进行数据库批量变更。 默认流水线 vs 部署配置 图片数据库 vs 数据库组 1. 准备 请确保已安装 Docker,如果本地没有重…...
java之方法引用 —— ::
目录 一、简介 二、引用静态方法 1.格式 2.示例 编辑 3.条件解析 三、引用成员方法 1.格式 2.示例 四、引用构造方法 1.格式 2.示例 五、类名引用成员方法 1.格式 2.略微不同的方法引用规则 3.示例 六、引用数组的构造方法 1.格式 2.示例 一、简介 方…...
「测试线排查的一些经验-上篇」 后端工程师
文章目录 端口占用脚本失灵线上部署项目结构模版配置文件生效 一般产品研发过程所使用的环境可分为: 研发环境-dev测试环境-test生产环境-prod 软件开发中,完整测试环境包括:UT、IT、ST、UAT UT Unit Test 单元测试 IT System Integration …...
AOSP12_BatteryStats统计电池数据信息
前言 BatteryStats模块主要用于设备在电池供电是系统对各个模块电量使用的统计,Android提供的Battery Historain工具就是对此模块统计的数据进行解析和展示。 一 BatteryStats模块类图 模块主要类图如下:见根目录的模块类图 BatteryStats:抽象类,本模块的核心类,主要定…...
【Android Studio】UI 布局
文章目录 view布局LinearLayout view 在Android开发中,View是一个非常重要的概念,它是所有用户界面组件的基类。View类及其子类构成了Android应用中的用户界面。每个View都占用屏幕上的一个矩形区域,并可以响应用户输入(如触摸、按…...
虚拟机Windows server忘记密码解决方法
原理 utilman.exe是Windows辅助工具管理器程序,虽然它本身不是一个关键的系统进程,但通过修改这个文件,用户可以访问一些有用的UI设置。在某些情况下,比如忘记密码需要重置时,通过修改utilman.exe文件为c…...
【香橙派系列教程】(六)嵌入式SQLite数据库
【六】嵌入式SQLite数据库 文章目录 【六】嵌入式SQLite数据库1.简介2.SQLite数据库安装3.SQLite命令用法1.创建数据库2.创建和查看表格3.插入查看数据(记录)4.删除更改数据(记录) 4.SQLite编程操作1.打开/创建数据库的C接口2.创建…...
深入探讨PHP8的新特性与性能优化
本文由 ChatMoney团队出品 随着互联网技术的飞速发展,PHP作为后端开发领域的热门语言也在不断演进。近期,PHP8的发布引起了广泛关注。本文将为您详细介绍PHP8的新特性以及性能优化,并通过具体示例帮助您更好地理解和应用这些新特性。 一、PH…...
2024年06月 Scratch 图形化(四级)真题解析#中国电子学会#全国青少年软件编程等级考试
Scratch图形化等级考试(1~4级)全部真题・点这里 一、单选题(共10题,共30分) 第1题 运行下列程序,输入单词“PLAY”,最后角色说?( ) A:LY4AP B:AP4LY C:YA4PL D:PL4AY 答案:B 根据程序分析可知,首先获取单词字符数,然后奇数位的字母放在字符数左侧,偶数位…...
XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...
深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
