切換語言為:簡體

SpringBoot中大量資料匯出方案:使用EasyExcel並行匯出多個excel檔案並壓縮zip後下載

  • 爱糖宝
  • 2024-09-27
  • 2045
  • 0
  • 0

SpringBoot的同步excel匯出方式中,服務會阻塞直到Excel檔案生成完畢,如果匯出資料很多時,效率低體驗差。有效的方案是將匯出資料拆分後利用CompletableFuture,將匯出任務非同步化,並行使用easyExcel匯出多個excel檔案,最後將所有檔案壓縮成ZIP格式以方便下載。

Springboot環境下基於以上方案,下面程式碼的高質量的完成匯出銷售訂單資訊到Excel檔案,並將多個Excel檔案打包成一個ZIP檔案,最後傳送給客戶端:

  1. 控制器層程式碼
@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. 服務層程式碼

負責執行銷售訂單的匯出邏輯:

  • 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檔案中

模板內容:

SpringBoot中大量資料匯出方案:使用EasyExcel並行匯出多個excel檔案並壓縮zip後下載

 /**
   * 匯出單個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);
     }
 }

匯出檔案如下:

SpringBoot中大量資料匯出方案:使用EasyExcel並行匯出多個excel檔案並壓縮zip後下載

程式碼亮點分析
  1. 多執行緒處理

    • 透過CompletableFutureThreadPoolTaskExecutor,將銷售訂單的匯出任務分配給多個執行緒並行執行,顯著提高了處理大量訂單時的效能。
    • 使用Lists.partition方法將訂單列表分割成多個子列表,每個子列表由一個執行緒處理,這裏每5個訂單一個執行緒。
  2. Excel模板匯出

    • 利用EasyExcel的模板功能,可以基於預定義的Excel模板填充資料,從而生成格式統一的銷售訂單Excel檔案。
    • 模板檔案透過類載入器的getResourceAsStream方法載入,便維護。
    • 將多個Excel檔案打包成一個ZIP檔案,方便使用者下載和管理。
  3. 資源清理

    • 方法執行完畢後,及時關閉開啟的檔案流和刪除臨時生成的Excel檔案和目錄,避免了資源洩露。
    • 使用try-with-resourcestry-catch-finally來確保資源的正確關閉和清理。
  4. 錯誤處理

    • 在方法執行過程中,對可能出現的異常進行了捕獲和處理,確保服務的健壯。
    • 對於無法恢復的錯誤,透過丟擲執行時異常的方式通知呼叫者,並記錄了詳細的錯誤日誌。


0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.