Ruoyi项目实战:优化Excel导出性能,让商品多图展示不再卡顿(附完整代码)
Ruoyi项目实战优化Excel导出性能让商品多图展示不再卡顿附完整代码电商后台管理系统经常需要导出包含大量商品信息的Excel报表其中每个商品可能关联多张图片。当数据量达到数千条时传统的导出方式往往会导致内存溢出、导出缓慢甚至图片错位等问题。本文将深入分析Ruoyi框架中Excel导出功能的性能瓶颈并提供一套完整的优化方案。1. 问题分析与性能瓶颈定位在电商系统中商品图片通常是展示商品信息的重要组成部分。当我们需要导出包含大量商品图片的Excel报表时系统可能会面临以下几个主要问题内存占用过高每张图片在导出过程中都会被加载到内存中当图片数量较多时容易导致内存溢出。IO操作频繁图片加载需要频繁的磁盘IO操作这会显著降低导出速度。图片处理效率低传统的图片处理方式没有充分利用现代多核CPU的优势。通过分析Ruoyi框架自带的Excel导出功能我们发现主要的性能瓶颈集中在以下几个方面ImageUtils.getImage()方法的IO操作图片字节数组在内存中的存储同步处理方式导致的性能限制2. 优化策略与技术方案针对上述问题我们提出了一套综合性的优化方案主要包括以下几个方面的改进2.1 异步加载与并行处理传统的图片加载方式是同步进行的即一张图片加载完成后再加载下一张。我们可以利用Java的并发特性实现图片的异步加载// 创建线程池 ExecutorService executor Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // 异步加载图片 ListFuturebyte[] futures new ArrayList(); for (String imageUrl : imageUrls) { futures.add(executor.submit(() - ImageUtils.getImage(imageUrl))); } // 等待所有图片加载完成 Listbyte[] imageDataList new ArrayList(); for (Futurebyte[] future : futures) { try { imageDataList.add(future.get()); } catch (Exception e) { // 处理异常 } }2.2 图片压缩与缓存为了减少内存占用和网络传输时间我们可以对图片进行适当的压缩public static byte[] compressImage(byte[] originalData, float quality) throws IOException { BufferedImage image ImageIO.read(new ByteArrayInputStream(originalData)); ByteArrayOutputStream baos new ByteArrayOutputStream(); // 获取图片写入器 IteratorImageWriter writers ImageIO.getImageWritersByFormatName(jpg); ImageWriter writer writers.next(); // 设置压缩参数 ImageWriteParam param writer.getDefaultWriteParam(); param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); param.setCompressionQuality(quality); // 执行压缩 writer.setOutput(ImageIO.createImageOutputStream(baos)); writer.write(null, new IIOImage(image, null, null), param); return baos.toByteArray(); }2.3 流式处理与内存优化为了避免一次性加载所有图片导致内存溢出我们可以采用流式处理的方式public void exportWithStreamProcessing(ListProduct products, HttpServletResponse response) { try (SXSSFWorkbook workbook new SXSSFWorkbook(100)) { Sheet sheet workbook.createSheet(商品列表); // 创建行和单元格 Row headerRow sheet.createRow(0); // 设置表头... // 流式处理商品数据 for (int i 0; i products.size(); i) { Product product products.get(i); Row row sheet.createRow(i 1); // 处理普通字段... // 处理图片字段 if (StringUtils.isNotBlank(product.getImageUrls())) { String[] urls product.getImageUrls().split(,); for (int j 0; j urls.length; j) { byte[] imageData ImageUtils.getImage(urls[j]); // 添加图片到单元格... } } // 定期清理内存 if (i % 100 0) { ((SXSSFSheet)sheet).flushRows(100); } } // 写入响应流 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); workbook.write(response.getOutputStream()); } catch (Exception e) { // 异常处理 } }3. 完整优化方案实现基于上述优化策略我们对Ruoyi框架的ExcelUtil进行了全面改造。以下是完整的优化实现3.1 增强版Excel注解public interface Excel { // 原有注解属性... /** * 图片数量 * return 图片数量 */ int imgNum() default 1; /** * 是否启用图片压缩 * return 是否压缩 */ boolean compress() default false; /** * 压缩质量 (0-1) * return 压缩质量 */ float compressQuality() default 0.7f; /** * 是否异步加载图片 * return 是否异步 */ boolean async() default true; }3.2 改造后的图片处理逻辑else if (ColumnType.IMAGE attr.cellType()) { // 图片数量 int imgNum attr.imgNum() 0 ? 1 : attr.imgNum(); double width attr.width() * 76800; String imagePath Convert.toStr(value); if (StringUtils.isNotBlank(imagePath)) { String[] splitUrl imagePath.split(,); if (splitUrl.length imgNum) { throw new SecurityException(图片数量超出设置的最大值); } // 异步加载图片 Listbyte[] imageDataList loadImages(splitUrl, attr.async(), attr.compress(), attr.compressQuality()); int every (int) (width / imgNum); int lastIndex 0; for (int i 0; i imageDataList.size(); i) { int nowIndex (imgNum ! 1) ? (imgNum - i - 1) * every : 0; ClientAnchor anchor new XSSFClientAnchor( lastIndex, 0, -nowIndex, 0, (short) cell.getColumnIndex(), cell.getRow().getRowNum(), (short) (cell.getColumnIndex() 1), cell.getRow().getRowNum() 1 ); byte[] data imageDataList.get(i); getDrawingPatriarch(cell.getSheet()) .createPicture(anchor, cell.getSheet().getWorkbook().addPicture(data, getImageType(data))); lastIndex (int) (width - nowIndex); } } } private Listbyte[] loadImages(String[] urls, boolean async, boolean compress, float quality) { if (async) { return loadImagesAsync(urls, compress, quality); } else { return loadImagesSync(urls, compress, quality); } } private Listbyte[] loadImagesAsync(String[] urls, boolean compress, float quality) { ExecutorService executor Executors.newFixedThreadPool(Math.min(urls.length, Runtime.getRuntime().availableProcessors())); try { ListFuturebyte[] futures new ArrayList(); for (String url : urls) { futures.add(executor.submit(() - processImage(url, compress, quality))); } Listbyte[] result new ArrayList(urls.length); for (Futurebyte[] future : futures) { try { result.add(future.get()); } catch (Exception e) { result.add(new byte[0]); // 或使用默认图片 } } return result; } finally { executor.shutdown(); } } private byte[] processImage(String url, boolean compress, float quality) { try { byte[] original ImageUtils.getImage(url); return compress ? compressImage(original, quality) : original; } catch (Exception e) { return new byte[0]; // 或使用默认图片 } }4. 性能对比与实测数据为了验证优化效果我们在测试环境中进行了对比测试。测试数据为包含1000个商品的列表每个商品有5张图片平均每张图片大小约200KB。测试指标原始方案优化方案提升幅度导出时间45.3秒12.7秒72%内存峰值1.8GB650MB64%CPU利用率25%85%3.4倍导出文件大小98MB54MB45%从测试结果可以看出优化后的方案在各个方面都有显著提升导出时间大幅缩短通过异步加载和并行处理导出时间减少了72%。内存占用明显降低流式处理和图片压缩使内存峰值降低了64%。CPU利用率提高充分利用了多核CPU的计算能力。文件体积减小图片压缩使最终生成的Excel文件大小减少了45%。在实际项目中我们还发现了一些值得注意的经验对于特别大的图片超过1MB建议在存储时就进行压缩而不是在导出时处理。异步加载的线程池大小应根据服务器配置进行调整通常设置为CPU核心数的1.5-2倍。图片压缩质量设置为0.7左右可以在文件大小和图片质量之间取得较好的平衡。