文件上传——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 根据程序分析可知,首先获取单词字符数,然后奇数位的字母放在字符数左侧,偶数位…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...
MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
站群服务器的应用场景都有哪些?
站群服务器主要是为了多个网站的托管和管理所设计的,可以通过集中管理和高效资源的分配,来支持多个独立的网站同时运行,让每一个网站都可以分配到独立的IP地址,避免出现IP关联的风险,用户还可以通过控制面板进行管理功…...
go 里面的指针
指针 在 Go 中,指针(pointer)是一个变量的内存地址,就像 C 语言那样: a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10,通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...
