SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载

文摘   2024-09-23 21:57   广东  

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文件中
模板内容:

 /**
   * 导出单个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);
     }
 }
 

导出文件如下:

代码亮点分析
  1. 多线程处理

  • 通过CompletableFutureThreadPoolTaskExecutor,将销售订单的导出任务分配给多个线程并行执行,显著提高了处理大量订单时的性能。
  • 使用Lists.partition方法将订单列表分割成多个子列表,每个子列表由一个线程处理,这里每5个订单一个线程。
  • Excel模板导出

    • 利用EasyExcel的模板功能,可以基于预定义的Excel模板填充数据,从而生成格式统一的销售订单Excel文件。
    • 模板文件通过类加载器的getResourceAsStream方法加载,便维护。
    • 将多个Excel文件打包成一个ZIP文件,方便用户下载和管理。
  • 资源清理

    • 方法执行完毕后,及时关闭打开的文件流和删除临时生成的Excel文件和目录,避免了资源泄露。
    • 使用try-with-resourcestry-catch-finally来确保资源的正确关闭和清理。
  • 错误处理

    • 在方法执行过程中,对可能出现的异常进行了捕获和处理,确保服务的健壮。
    • 对于无法恢复的错误,通过抛出运行时异常的方式通知调用者,并记录了详细的错误日志。

    太强 ! SpringBoot中出入参增强的5种方法 : 加解密、脱敏、格式转换、时间时区处理

    太强 ! SpringBoot中优化if-else语句的七种绝佳方法实战
    提升编程效率的利器: Google Guava库中双向映射BitMap
    从MySQL行格式原理看:为什么开发规范中不推荐NULL?数据是如何在磁盘上存储的?
    SpringBoot中使用Jackson实现自定义序列化和反序列化控制的5种方式总结
    提升编程效率的利器: Google Guava库之RateLimiter优雅限流
    深入JVM逃逸分析原理:且看其如何提高程序性能和内存利用率
    必知必会!MySQL索引下推:原理与实战

    深入解析JVM内存分配优化技术:TLAB

    SpringBoot中基于JWT的双token(access_token+refresh_token)授权和续期方案
    SpringBoot中Token登录授权、续期和主动终止的方案(Redis+Token)
    微服务中token鉴权设计的4种方式总结

    重温设计模式:桥接模式实现灵活组合,超越单一继承的设计之道

    提升编程效率的API利器:精通Google Guava库区间范围映射RangeMap

    SpringBoot中Jackson控制序列化和反序列化的注解和扩展点总结【收藏版】

    关注『 码到三十五 』,日有所获
                       点赞 和 在看 就是最大的支持

    码到三十五
    主要分享正经的开发技术(原理,架构,实践,源码等),以输出驱动输入;当然偶尔会穿插点生活琐碎,顺便吃个瓜,目的嘛,搞点精准流量,看能不能发发广告。
     最新文章