Springboot实现使用断点续传优化同步导入Excel

server/2025/3/26 9:34:22/

springboot实现使用断点续传优化同步导入Excel

  • 需求前言
  • 断点续传
  • 前端实现
  • 后端实现
  • 完结撒花,如有需要收藏的看官,顺便也用发财的小手点点赞哈,如有错漏,也欢迎各位在评论区评论!

需求前言

在跨境电商系统中,其中下单方式之一就是通过Excel记录多个用户该下单哪些商品实现批量下单,就需要实现导入Excel的方案,最简单的就是同步导入Excel,但同步导入大Excel文件时,网络原因抑或误刷新了页面,就需要重新上传,这就造成用户体验感不好,于是引入断点续传来优化同步导入Excel。

断点续传

断点续传就是在文件上传或下载过程中,如果中途中断了,下次可以从中断的地方继续,而不需要重新开始。这对于大文件传输特别有用,节省时间和带宽。
前端部分需要支持分片上传。也就是说,把大文件切成多个小块,每个小块单独上传。这样即使中间断了,只需要传剩下的部分。前端怎么做呢?本文使用JavaScript的File API来读取文件,然后分片。比如用File.slice方法,把文件切成多个Blob。然后每个Blob单独上传,并告诉服务器这是第几个分片,总共有多少分片,文件的唯一标识是什么,比如用MD5哈希之类的。这样服务器就知道怎么合并这些分片了。

然后,后端操作需要接收这些分片,保存起来,并且记录哪些分片已经上传了。当所有分片都上传完后,后端要把这些分片按顺序合并成完整的文件。这里可能涉及到文件存储的问题,每个分片保存为临时文件,最后合并的时候可能需要按顺序读取所有分片然后写入到一个文件中。另外,为了支持断点续传,前端在上传前可能需要先询问服务器,这个文件已经传了哪些分片,然后只传剩下的。所以后端还需要提供一个接口,让前端可以查询某个文件的上传进度。
具体步骤:
1、前端选择文件后,先计算文件的唯一标识(比如MD5),并查询服务器该文件的上传情况,获取已上传的分片列表。
2、根据分片大小(比如1MB),将文件分片。
3、遍历所有分片,对于未上传的分片,逐个上传到服务器。
4、每个分片上传时,携带分片索引、总片数、文件唯一标识等信息。
5、后端接收到分片后,保存分片文件,并记录该分片已上传。
6、当所有分片上传完毕后,前端发送一个合并请求,或者后端检测到所有分片都已上传后自动合并分片为完整文件。
7、合并完成后,删除临时分片文件。

前端实现

<input type="file" id="fileInput" accept=".xlsx,.xls" />
<button onclick="startUpload()">上传Excel</button>
<div id="progress"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script>
<script>
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB分片async function startUpload() {const file = document.getElementById('fileInput').files[0];if (!file || !file.name.match(/\.(xlsx|xls)$/i)) {alert('请选择Excel文件');return;}// 1. 生成文件唯一标识const fileId = await computeFileHash(file);document.getElementById('progress').innerHTML = '正在验证文件...';// 2. 检查文件状态const { exists, uploadedChunks } = await fetch(`/api/upload/check?fileId=${fileId}`).then(res => res.json());if (exists) {document.getElementById('progress').innerHTML = '文件已存在,直接解析';triggerParse(fileId); // 触发后端解析return;}// 3. 分片上传const totalChunks = Math.ceil(file.size / CHUNK_SIZE);let uploaded = uploadedChunks.length;// 上传未完成的分片(带进度显示)for (let i = 0; i < totalChunks; i++) {if (!uploadedChunks.includes(i)) {await uploadChunk(file, i, fileId);uploaded++;document.getElementById('progress').innerHTML = `上传进度:${Math.round((uploaded / totalChunks) * 100)}%`;}}// 4. 合并并解析const { success } = await fetch(`/api/upload/merge?fileId=${fileId}`).then(res => res.json());if (success) {triggerParse(fileId);} else {alert('文件合并失败');}
}// 触发后端解析
async function triggerParse(fileId) {const result = await fetch(`/api/excel/parse?fileId=${fileId}`).then(res => res.json());document.getElementById('progress').innerHTML = `解析完成,成功导入${result.successCount}条数据`;
}// 其他函数保持原有逻辑...
</script>

上述代码,前端在上传时还可会展示进度条,分片是在前端决定的,每次上传前都需要查询后端关于该文件的分片上传情况,由前端判断该是否上传某分片,在前端分片后,还可以使用并发发送多个分片

后端实现

@RestController
@RequestMapping("/api")
public class ExcelUploadController {@Value("${file.upload.temp-dir}")private String tempDir;@Value("${file.upload.target-dir}")private String targetDir;// 检查文件状态@GetMapping("/upload/check")public Map<String, Object> checkFile(@RequestParam String fileId) {File targetFile = new File(targetDir, fileId + ".xlsx");Map<String, Object> result = new HashMap<>();result.put("exists", targetFile.exists());File chunkDir = new File(tempDir, fileId);if (chunkDir.exists()) {List<Integer> chunks = Arrays.stream(chunkDir.listFiles()).map(f -> Integer.parseInt(f.getName().split("_")[1])).collect(Collectors.toList());result.put("uploadedChunks", chunks);} else {result.put("uploadedChunks", Collections.emptyList());}return result;}// 分片上传@PostMapping("/upload")public ResponseEntity<?> uploadChunk(@RequestParam("file") MultipartFile file,@RequestParam String fileId,@RequestParam int chunkIndex) throws IOException {File chunkDir = new File(tempDir, fileId);if (!chunkDir.exists()) chunkDir.mkdirs();File chunkFile = new File(chunkDir, "chunk_" + chunkIndex);file.transferTo(chunkFile);return ResponseEntity.ok().build();}// 合并文件@GetMapping("/upload/merge")public Map<String, Object> mergeFile(@RequestParam String fileId) throws IOException {File chunkDir = new File(tempDir, fileId);File targetFile = new File(targetDir, fileId + ".xlsx");try (FileOutputStream fos = new FileOutputStream(targetFile)) {// 按顺序合并分片IntStream.range(0, chunkDir.listFiles().length).sorted().forEach(i -> {File chunk = new File(chunkDir, "chunk_" + i);try (FileInputStream fis = new FileInputStream(chunk)) {byte[] buffer = new byte[1024];int len;while ((len = fis.read(buffer)) != -1) {fos.write(buffer, 0, len);}} catch (IOException e) {throw new RuntimeException("合并失败", e);}});}// 清理临时目录FileUtils.deleteDirectory(chunkDir);return Map.of("success", true);}// Excel解析接口@GetMapping("/excel/parse")public Map<String, Object> parseExcel(@RequestParam String fileId) {File excelFile = new File(targetDir, fileId + ".xlsx");// 使用EasyExcel解析ExcelDataListener listener = new ExcelDataListener();try {EasyExcel.read(excelFile, ExcelData.class, listener).sheet().doRead();} catch (Exception e) {return Map.of("success", false,"error", "解析失败: " + e.getMessage(),"successCount", listener.getSuccessCount());}return Map.of("success", true,"successCount", listener.getSuccessCount(),"errors", listener.getErrors());}
}// Excel数据模型
@Data
public class ExcelData {@ExcelProperty("姓名")private String name;@ExcelProperty("年龄")private Integer age;@ExcelProperty("邮箱")private String email;
}// 数据监听器
public class ExcelDataListener extends AnalysisEventListener<ExcelData> {private final List<ExcelData> cachedData = new ArrayList<>();private final List<String> errors = new ArrayList<>();private int successCount = 0;@Overridepublic void invoke(ExcelData data, AnalysisContext context) {// 数据校验if (data.getName() == null || data.getName().isEmpty()) {errors.add("第" + context.readRowHolder().getRowIndex() + "行: 姓名不能为空");return;}cachedData.add(data);if (cachedData.size() >= 100) {saveToDB();cachedData.clear();}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {if (!cachedData.isEmpty()) {saveToDB();}}private void saveToDB() {// 这里实现实际入库逻辑successCount += cachedData.size();}// Getter省略...
}

使用easyExcel来接收分片,即使前端是并发上传多个分片(当然也最好有个上传限制,比如限制前端控制上传5个分片),也能避免出现OOM和CPU飙升的情况。

Java解析、生成Excel比较有名的框架有apache poi、jxl。但他们都存在一个严重的问题就是非常耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但poi还是有一些缺陷,比如07版本Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyExcel重写了poi对07版本Excel的解析(一个3M的Excel用POI
SAX解析依然需要100M左右内存)改用easyExcel可以降低到几M,并且再打的Excel也不会出现内存溢出;03版本依赖POI的sax模式,在上层做了模型转换的封装,让使用者可以简单方便;

完结撒花,如有需要收藏的看官,顺便也用发财的小手点点赞哈,如有错漏,也欢迎各位在评论区评论!


http://www.ppmy.cn/server/177178.html

相关文章

<C#> 详细介绍.net 三种依赖注入:AddTransient、AddScoped、AddSingleton 的区别

在 .NET 8 里&#xff0c;AddTransient、AddScoped 和 AddSingleton 均为依赖注入容器用于注册服务的方法&#xff0c;不过它们的生命周期管理方式存在差异。下面为你详细介绍这三种方法的区别。 1. AddTransient AddTransient 方法所注册的服务&#xff0c;每次被请求时都会…

CSS 中@media查询的工作原理,如何利用它实现不同设备的样式适配

大白话 CSS 中media查询的工作原理&#xff0c;如何利用它实现不同设备的样式适配 什么是 media 查询 在网页设计里&#xff0c;不同的设备&#xff08;像手机、平板、电脑&#xff09;屏幕尺寸各异。要是只使用一套固定的样式&#xff0c;网页在某些设备上可能就显示得乱七八…

有哪些开源的智慧园区项目?

作为专注于数字孪生技术的开发者团队&#xff0c;我们一直在数字孪生圈分享开源项目。但最近一次与AI助手Deepseek的对话&#xff0c;却让我们“又惊又喜”。 ——当询问“有哪些开源的智慧园区项目”时&#xff0c;Deepseek的回答中竟包含了我们自己的开源项目&#xff01; 这…

高效算法与系统优化:二叉树遍历、HTTPS安全、零拷贝及最大乘积问题解析*

*** 在日常开发和系统优化中&#xff0c;我们经常遇到二叉树的遍历问题、网络通信的安全保障、Linux 下的高效数据传输&#xff0c;以及数组处理的最优解法。本文将系统梳理这些知识点&#xff0c;帮助大家深入理解关键概念&#xff0c;并掌握高效实现方案。 1. 二叉树的三种…

【Azure 架构师学习笔记】- Azure Networking(1) -- Service Endpoint 和 Private Endpoint

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Networking】系列。 前言 最近公司的安全部门在审计云环境安全性时经常提到service endpoint&#xff08;SE&#xff09;和priavate endpoint&#xff08;PE&#xff09;的术语&#xff0c;为此做了一些研究储备。 云…

大数据(1.1)纽约出租车大数据分析实战:从Hadoop到Azkaban的全链路解析与优化

目录 一、背景与数据价值‌ ‌二、技术选型与组件分工‌ ‌三、数据准备与预处理‌ 四、实战步骤详解‌ ‌1. 数据上传至HDFS ‌2. Hive数据建模与清洗‌ 4‌.2.1 建表语句&#xff08;分区表按年份&#xff09;‌&#xff1a; ‌4‌.2.2 数据清洗&#xff08;剔除无效…

【设计模式】三十一、状态模式

系列文章|源码 https://github.com/tyronczt/design-mode-learn 文章目录 系列文章|源码一、模式核心思想二、模式结构三、Java代码示例&#xff1a;订单状态管理1. 定义状态接口2. 实现具体状态类3. 上下文类&#xff08;Context&#xff09;4. 客户端调用5. 运行截图 四、状…

【css酷炫效果】纯CSS实现球形阴影效果

【css酷炫效果】纯CSS实现球形阴影效果 缘创作背景html结构css样式完整代码效果图 想直接拿走的老板&#xff0c;链接放在这里&#xff1a;上传后更新 缘 创作随缘&#xff0c;不定时更新。 创作背景 刚看到csdn出活动了&#xff0c;赶时间&#xff0c;直接上代码。 html结…