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

news/2024/10/23 14:03:09/

文章目录

  • 前言
  • 一、控制器层代码
  • 二、服务层代码
  • 三、代码亮点分析


前言

SpringBoot的同步excel导出方式中,服务会阻塞直到Excel文件生成完毕,如果导出数据很多时,效率低体验差。有效的方案是将导出数据拆分后利用CompletableFuture,将导出任务异步化,并行使用easyExcel导出多个excel文件,最后将所有文件压缩成ZIP格式以方便下载。

Springboot环境下基于以上方案,下面代码的高质量的完成导出销售订单信息到Excel文件,并将多个Excel文件打包成一个ZIP文件,最后发送给客户端:


一、控制器层代码

@RestController
public class SalesOrderController {@Resourceprivate 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;@Resourceprivate 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文件流到responsezipFileInputStream = 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>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {Files.deleteIfExists(file);return FileVisitResult.CONTINUE;}@Overridepublic 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);}} 
  1. 使用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,将销售订单的导出任务分配给多个线程并行执行,显著提高了处理大量订单时的性能。
  2. 使用Lists.partition方法将订单列表分割成多个子列表,每个子列表由一个线程处理,这里每5个订单一个线程。

Excel模板导出:

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

资源清理:

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

错误处理:

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

http://www.ppmy.cn/news/1541346.html

相关文章

材质变体 PSO学习笔记

学习笔记 参考各路知乎大佬文章 首先是对变体的基本认知 概括就是变体是指根据引擎中上层编写(UnityShaderLab/UE连连看)中的各种defines情况&#xff0c;根据不同平台编译成的底层shader&#xff0c;OpenGL-glsl/DX(9-11)-dxbc DX12-dxil/Vulkan-spirv&#xff0c;是打到游…

案例分享-优秀蓝色系UI界面赏析

蓝色UI设计界面要提升舒适度&#xff0c;关键在于色彩搭配与对比度。选择柔和的蓝色调作为主色&#xff0c;搭配浅灰或白色作为辅助色&#xff0c;能营造清新、宁静的氛围。同时&#xff0c;确保文字与背景之间有足够的对比度&#xff0c;避免视觉疲劳&#xff0c;提升阅读体验…

攻防世界web引导模式 框架梳理

view_source 根据提示&#xff0c;没法右键&#xff0c;想办法右键查看源代码f12&#xff0c;找到flag robots 这个协议&#xff1a;表示网络爬虫可以访问哪些路径和不能访问哪些路径 之后查看路径得到flag backup 根据提示知道要找备份文件&#xff0c;搜索备份文件后缀…

嵌入式入门学习——8基于Protues仿真Arduino+SSD1306液晶显示数字时钟

0 系列文章入口 嵌入式入门学习——0快速入门&#xff0c;Let‘s Do It&#xff01; SSD1306 1 Protues查找SSD1306器件并放置在画布&#xff0c;画好电气连接&#xff08;这里VCC和GND画反了&#xff0c;后面仿真出错我才看见&#xff0c;要是现实硬件估计就烧毁了&#xf…

搞错了,再来!谷歌利用AI重新推出全新的Google Shopping

近年来&#xff0c;随着电子商务的迅猛发展&#xff0c;消费者对个性化和便捷购物体验的需求愈发高涨。谷歌&#xff0c;作为互联网巨头之一&#xff0c;一直在不断探索和创新&#xff0c;它一直在应对这样一个事实&#xff1a;越来越多的消费者首先访问零售商的网站&#xff0…

你了解的spring框架有哪些

列举一些重要的Spring模块&#xff1f; Spring Core&#xff1a; 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IOC 依赖注入功能。**Spring Aspects ** &#xff1a; 该模块为与AspectJ的集成提供支持。Spring AOP &#xff1a;提供了面向方面的编程实现。…

【Docker】Dockerfile 镜像实战

目录 一、构建SSH镜像 二、构建Systemctl镜像 三、nginx镜像 四、tomcat 镜像 五、mysql镜像 一、构建SSH镜像 mkdir /opt/sshd cd /opt/sshdvim Dockerfile #第一行必须指明基于的基础镜像 FROM centos:7 #作者信息 MAINTAINER this is ssh image <hmj> #镜像的操…

仅一行代码,使LLaMA3在知识编辑任务上表现暴涨35%!您确定不来试试嘛?

引言 LLMs常因错误/过时知识产生幻觉&#xff0c;而基于新知识微调耗时且易过拟合、引入额外的知识库或参数模块又会带来不断增加的存储空间压力。因此&#xff0c;基于“Locate-then-Edit”的知识编辑&#xff08;如ROME&#xff09;被提出&#xff0c;用“少时间成本、零空间…