SpringBoot的同步excel匯出方式中,服務會阻塞直到Excel檔案生成完畢,如果匯出資料很多時,效率低體驗差。有效的方案是將匯出資料拆分後利用CompletableFuture,將匯出任務非同步化,並行使用easyExcel匯出多個excel檔案,最後將所有檔案壓縮成ZIP格式以方便下載。
Springboot環境下基於以上方案,下面程式碼的高質量的完成匯出銷售訂單資訊到Excel檔案,並將多個Excel檔案打包成一個ZIP檔案,最後傳送給客戶端:
-
控制器層程式碼:
@RestController public class SalesOrderController { @Resource private SalesOrderExportService salesOrderExportService; @PostMapping(value = "/salesOrder/export") public void salesOrderExport(@RequestBody @Validated RequestDto req, HttpServletResponse response) { salesOrderExportService.salesOrderExport(req, response); } }
-
服務層程式碼:
負責執行銷售訂單的匯出邏輯:
-
1. 將多個Excel檔案打包成ZIP檔案 -
2. 多執行緒ThreadPoolTaskExecutor並行處理銷售訂單的匯出
@Slf4j @Service public class SalesOrderExportService { @Autowired @Qualifier("threadPoolTask") private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Resource private OrderManager OrderManager; public void salesOrderExport(RequestDto req, HttpServletResponse response) { // 獲取匯出資料,每個SalesOrder例項需要分別匯出到一個excel檔案 List<SalesOrder> orderDataList = OrderManager.getOrder(req.getUserCode()); // 略...校驗資料 InputStream zipFileInputStream = null; Path tempZipFilePath = null; Path tempDir = null; // 獲取匯出模板 try (InputStream templateInputStream = this.getClass().getClassLoader().getResourceAsStream("template/order_template.xlsx"); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) { if (Objects.isNull(templateInputStream)) { throw new RuntimeException("獲取模版檔案異常"); } // 多執行緒服用一個檔案流 IOUtils.copy(templateInputStream, outputStream); // 建立臨時excel檔案匯出目錄,用於將多個excel匯出到此目錄下 Path tmpDirRef = (tempDir = Files.createTempDirectory(req.userCode() + "dir_prefix")); // 每5個salesOrder一個執行緒並行匯出到excel檔案中 CompletableFuture[] salesOrderCf = Lists.partition(orderDataList, 5).stream() .map(orderDataSubList -> CompletableFuture .supplyAsync(() -> orderDataSubList.stream() .map(orderData -> this.exportExcelToFile(tmpDirRef, outputStream, orderData)) .collect(Collectors.toList()), threadPoolTaskExecutor) .exceptionally(e -> {throw new RuntimeException(e);})) .toArray(CompletableFuture[]::new); // 等待所有excel檔案匯出完成 CompletableFuture.allOf(salesOrderCf).get(3, TimeUnit.MINUTES); // 建立臨時zip檔案 tempZipFilePath = Files.createTempFile(req.userCode() + TMP_ZIP_DIR_PRE, ".zip"); // 將excel目錄下的所有檔案壓縮到zip檔案中,zipUtil有很多工具包都有 ZipUtil.zip(tempDir.toString(), tempZipFilePath.toString()); response.setContentType("application/octet-stream;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(tempZipFilePath.toFile().getName(), "utf-8")); // 寫zip檔案流到response zipFileInputStream = Files.newInputStream(tempZipFilePath); IOUtils.copy(zipFileInputStream, response.getOutputStream()); } catch (Exception e) { log.error("salesOrderExport,異常:", e); throw new RuntimeException("匯出異常,請稍後重拾"); } finally { try { // 關閉流 if (Objects.nonNull(zipFileInputStream)) { zipFileInputStream.close(); } // 刪除臨時檔案及目錄 if (Objects.nonNull(tempDir)) { Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.deleteIfExists(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.deleteIfExists(dir); return FileVisitResult.CONTINUE; } }); } if (Objects.nonNull(tempZipFilePath)) { Files.deleteIfExists(tempZipFilePath); } } catch (Exception e) { log.error("salesOrderExport, 關閉檔案流失敗:", e); } }
-
使用EasyExcel庫基於模板匯出每個銷售訂單到單獨的Excel檔案中
/** * 匯出單個excle檔案,上面的多執行緒程式碼呼叫 **/ private Path exportExcelToFile(Path temporaryDir, ByteArrayOutputStream templateOutputStream, SalesOrder data) { Path temproaryFilePath = null; try { // 建立臨時檔案 temproaryFilePath = Files.createTempFile(temporaryDir, data.getOrderNo(), ExcelTypeEnum.XLSX.getValue()); } catch (IOException e) { throw new RuntimeException("exportExcelToFile,建立excel臨時檔案失敗:" + data.getOrderNo()); } try (InputStream templateInputStream = new ByteArrayInputStream(templateOutputStream.toByteArray()); OutputStream temporaryFileOs = Files.newOutputStream(temproaryFilePath); BufferedOutputStream tempOutStream = new BufferedOutputStream(temporaryFileOs)) { // 使用easyExcel的模板功能匯出訂單資料到臨時檔案中 ExcelWriter excelWriter = EasyExcel.write(tempOutStream, SalesOrder.class) .withTemplate(templateInputStream).excelType(ExcelTypeEnum.XLSX).build(); // 填充模板資料 WriteSheet writeSheet = EasyExcel.writerSheet().build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); excelWriter.fill(new FillWrapper("goods", data.getGoodsList()), fillConfig, writeSheet); excelWriter.fill(data, writeSheet); excelWriter.finish(); // temproaryFilePath.toFile().deleteOnExit(); return temproaryFilePath; } catch (Exception e) { throw new RuntimeException("exportExcelToFile,匯出excel檔案失敗:" + data.getOrderNo(), e); } }
匯出檔案如下:
程式碼亮點分析
-
多執行緒處理:
-
透過 CompletableFuture
和ThreadPoolTaskExecutor
,將銷售訂單的匯出任務分配給多個執行緒並行執行,顯著提高了處理大量訂單時的效能。 -
使用 Lists.partition
方法將訂單列表分割成多個子列表,每個子列表由一個執行緒處理,這裏每5個訂單一個執行緒。 -
Excel模板匯出:
-
利用EasyExcel的模板功能,可以基於預定義的Excel模板填充資料,從而生成格式統一的銷售訂單Excel檔案。 -
模板檔案透過類載入器的 getResourceAsStream
方法載入,便維護。 -
將多個Excel檔案打包成一個ZIP檔案,方便使用者下載和管理。 -
資源清理:
-
方法執行完畢後,及時關閉開啟的檔案流和刪除臨時生成的Excel檔案和目錄,避免了資源洩露。 -
使用 try-with-resources
和try-catch-finally
來確保資源的正確關閉和清理。 -
錯誤處理:
-
在方法執行過程中,對可能出現的異常進行了捕獲和處理,確保服務的健壯。 -
對於無法恢復的錯誤,透過丟擲執行時異常的方式通知呼叫者,並記錄了詳細的錯誤日誌。