1. 项目背景与核心需求
最近在开发一个数据报表导出功能时,遇到了两个典型问题:一是当数据量过大时直接导出会导致内存溢出,二是业务部门要求导出的Excel文件需要带有公司水印。经过一番折腾,最终基于EasyExcel实现了稳定可靠的分批导出与水印适配方案。
这个方案特别适合需要处理以下场景的开发者:
- 导出数据量在10万行以上的报表
- 需要动态添加水印的Excel文件生成
- 对服务器内存占用敏感的生产环境
2. 技术方案设计思路
2.1 为什么选择EasyExcel
相比传统的POI,EasyExcel在内存控制方面有显著优势:
- 采用逐行写入的SAX模式解析
- 默认缓存仅保留100行数据
- 支持自定义分批写入策略
实测导出50万行数据时:
- POI方式内存峰值:1.2GB
- EasyExcel方式内存峰值:150MB
2.2 水印实现方案对比
常见的Excel水印实现方式有:
- 页眉页脚文字水印(兼容性好但效果差)
- 背景图片水印(效果佳但兼容性问题)
- 浮动Shape对象(折中方案)
最终选择第三种方案,因为:
- 支持透明度和旋转角度
- 不影响单元格数据操作
- 在各版本Excel中显示稳定
3. 核心实现细节
3.1 分批导出实现
// 分页查询参数 @Data public class PageQuery { private int pageSize = 5000; private int currentPage = 1; } // 分批写入处理器 public class BatchWriteHandler implements WriteHandler { @Override public void sheet(int sheetNo, Sheet sheet) { // 设置批处理大小 sheet.setAutoTrim(true); } } // 导出执行方法 public void exportBigData(HttpServletResponse response) { // 1. 初始化导出参数 ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new BatchWriteHandler()) .build(); // 2. 循环分页写入 PageQuery query = new PageQuery(); while(true) { List<Data> batchData = dataService.getBatchData(query); if(CollectionUtils.isEmpty(batchData)) break; WriteSheet writeSheet = EasyExcel.writerSheet("第"+query.getCurrentPage()+"页") .head(Data.class) .build(); excelWriter.write(batchData, writeSheet); query.setCurrentPage(query.getCurrentPage()+1); } // 3. 关闭资源 excelWriter.finish(); }关键参数说明:
- pageSize:根据数据行宽建议设置在3000-10000之间
- response.getOutputStream():必须提前设置好response的ContentType
- autoTrim:设置为true可自动清理空行内存
3.2 动态水印实现
public class WatermarkHandler extends AbstractSheetWriteHandler { private final String watermarkText; public WatermarkHandler(String text) { this.watermarkText = text; } @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { // 获取Sheet对象 Sheet sheet = writeSheetHolder.getSheet(); Drawing<?> drawing = sheet.createDrawingPatriarch(); // 创建文字水印 XSSFSimpleShape shape = (XSSFSimpleShape) drawing.createSimpleShape( new XSSFClientAnchor(0, 0, 0, 0, 0, 0, 10, 10)); // 水印样式设置 shape.setText(watermarkText); shape.setFillColor(0, 0, 0, 0); shape.setLineStyleColor(0, 0, 0, 0); shape.setRotation(315); // 旋转角度 shape.setFontSize(48); shape.setFontColor(200, 200, 200); // 浅灰色 } }水印优化技巧:
- 透明度设置:RGB最后一个参数控制透明度(0-255)
- 位置计算:ClientAnchor参数控制水印位置和大小
- 多水印:创建多个Shape实现满屏水印效果
4. 性能优化与问题排查
4.1 内存泄漏预防
常见内存问题:
- 未关闭ExcelWriter导致句柄泄漏
- 大对象未及时GC回收
- 分页查询参数未重置
解决方案:
// 使用try-with-resources确保关闭 try (ExcelWriter excelWriter = EasyExcel.write(...)) { // 写入操作 } // 定期清理缓存 if(query.getCurrentPage() % 20 == 0) { System.gc(); }4.2 水印显示异常处理
常见问题:
- WPS打开水印位置偏移
- 打印时水印消失
- 导出PDF格式异常
应对方案:
- 添加多水印交叉覆盖
- 同时设置页眉文字水印作为备用
- 测试不同办公软件的兼容性
5. 完整实现示例
结合两种功能的完整方案:
public void exportWithWatermark(HttpServletResponse response) { // 1. 响应头设置 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=export.xlsx"); // 2. 初始化导出器 try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new WatermarkHandler("CONFIDENTIAL")) .registerWriteHandler(new BatchWriteHandler()) .build()) { // 3. 分批写入数据 PageQuery query = new PageQuery(); while(true) { List<Data> batchData = fetchBatchData(query); if(batchData.isEmpty()) break; WriteSheet writeSheet = EasyExcel.writerSheet("Page_"+query.getCurrentPage()) .head(Data.class) .build(); excelWriter.write(batchData, writeSheet); query.nextPage(); } } }实际使用中发现几个值得注意的点:
- 水印文字建议控制在20个字符以内
- 分页大小需要根据数据列数动态调整
- 响应输出流需要在所有操作前设置完成