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模式,在上層做了模型轉換的封裝,讓使用者可以簡單方便;