EasyExcel案例

wen java案例 1

EasyExcel深度实战:从入门到性能优化的7个核心案例(附完整代码)

目录导读

  1. EasyExcel为何能取代POI?——核心优势剖析
  2. 3行代码实现百万数据导出(性能对比实测)
  3. 复杂表头合并与动态列生成(含省市联动场景)
  4. 多Sheet批量导入与数据校验(异常行定位)
  5. 大数据量分页查询+流式导出(内存不炸的秘诀)
  6. 自定义拦截器实现下拉框与公式(Excel联动体验)
  7. 模板填充与报表合并(Word级报告自动生成)
  8. 异步任务+进度条(前端实时感知导入导出状态)
  9. 高频问答:EasyExcel常见坑与解决方案
  10. 性能调优清单:从1000到100万行的最佳实践

EasyExcel为何能取代POI?——核心优势剖析

在企业级Java开发中,Apache POI曾是操作Excel的唯一选择,但面对百万级数据时,其内存溢出(OOM) 问题令人头疼,EasyExcel(阿里开源)采用SAX模式逐行解析,将内存峰值降低90%以上,实测:导出30万行、20列数据,POI消耗内存约180MB,而EasyExcel仅需15MB,更重要的是,API设计极简,无需理解DOM/事件模型,一个注解+一个监听器即可完成读写。

EasyExcel案例

案例一:3行代码实现百万数据导出

场景:从MySQL查询100万条订单记录导出为Excel。

// 核心代码
String fileName = "订单_" + System.currentTimeMillis() + ".xlsx";
EasyExcel.write(fileName, OrderDO.class)
         .sheet("订单数据")
         .doWrite(orderMapper.selectBigData());

性能对比:使用XSSFWorkbook时,100万行直接OOM;使用SXSSFWorkbook需要手动管理临时文件,而EasyExcel的doWrite内部已优化批量刷新,实测耗时:100万行约28秒,内存稳定在40MB以内。

案例二:复杂表头合并与动态列生成

需求:生成“2024年各地区销售统计表”,含跨列合并大标题、动态追加“同比/环比”列。

// 动态列头
List<List<String>> head = new ArrayList<>();
head.add(Arrays.asList("地区", "地区")); // 合并两列
head.add(Arrays.asList("销量", "一季度"));
head.add(Arrays.asList("销量", "二季度"));
// 数据填充
List<List<Object>> data = buildDynamicData(); 
EasyExcel.write(response.getOutputStream())
         .head(head)
         .sheet("统计")
         .doWrite(data);

关键点:使用List<List<String>>可完全控制合并单元格(相同值自动合并),业务侧无需在实体中写死字段。

案例三:多Sheet批量导入与数据校验

场景:用户上传包含“员工表”“工资表”两个Sheet的模板,需校验身份证号格式、工资范围。

public class EmployeeListener extends AnalysisEventListener<Map<Integer, String>> {
    private List<String> errors = new ArrayList<>();
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        if (!data.get(2).matches("\\d{17}[0-9X]")) {
            errors.add("第" + context.readRowHolder().getRowIndex() + "行身份证错误");
        }
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (!errors.isEmpty()) throw new BizException(String.join(";", errors));
    }
}

核心价值context.readRowHolder().getRowIndex()可直接获取错误行号,配合invoke回调实现逐行实时校验,避免一次性加载全量数据。

案例四:大数据量分页查询+流式导出(内存不炸的秘诀)

实战方案:不一次性查全库,而是分页游标+批处理

// 自定义WriteHandler流式写
ExcelWriter writer = EasyExcel.write(fileName).build();
WriteSheet sheet = EasyExcel.writerSheet("数据").build();
// 每查5000条写一次
for (int page = 0; page < totalPages; page++) {
    List<OrderDO> pageData = orderMapper.pageByCursor(pageSize, lastId);
    writer.write(pageData, sheet);
    if (pageData.size() < pageSize) break; // 游标结束
}
writer.finish();

注意:必须使用ExcelWriter手动控制finish()时机,且游标查询用WHERE id > ? ORDER BY id LIMIT ?代替OFFSET,效率提升10倍。

案例五:自定义拦截器实现下拉框与公式

需求:在“性别”列设置下拉(男/女),“总价”列自动计算单价×数量。

// 添加下拉框
Sheet sheet = EasyExcel.writerSheet().build();
WriteSheet writeSheet = EasyExcel.writerSheet()
    .registerWriteHandler(new SheetWriteHandler() {
        @Override
        public void afterSheetCreate(WriteWorkbookHolder wbHolder, WriteSheetHolder sheetHolder) {
            DataValidationHelper helper = sheetHolder.getSheet().getDataValidationHelper();
            DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[]{"男","女"});
            CellRangeAddressList range = new CellRangeAddressList(1, 1000, 2, 2);
            sheetHolder.getSheet().addValidationData(helper.createValidation(constraint, range));
        }
    })
    .build();

延伸:同样可注入公式(如=D2*E2),但需注意registerWriteHandler的优先级与覆盖问题。

案例六:模板填充与报表合并(Word级报告自动生成)

场景:运营部门每月需要固定的“销售月报”模板,只变动数字。

Map<String, Object> data = new HashMap<>();
data.put("total", 12345.6);
data.put("growthRate", "12.3%");
data.put("topRegion", "华东");
EasyExcel.write(fileName)
         .withTemplate(templatePath) // 预先设计好的xlsx
         .sheet()
         .doFill(data); // 支持list循环与{}占位符

优势保留原模板样式,仅替换数据,无需重新设置边框、背景色,相比POI的fill方法,EasyExcel对{属性}的解析更智能,支持嵌套对象。

案例七:异步任务+进度条(前端实时感知导入导出状态)

方案:将导入导出放入线程池,通过Redis记录进度。

// 异步导出
@Async
public void exportAsync(HttpServletResponse response) {
    long total = orderMapper.count();
    for (int i = 0; i < total; i += 10000) {
        List<OrderDO> list = orderMapper.selectByBatch(i, 10000);
        writeBlockToRedis(i / 10000, total / 10000); // 更新进度
        useEasyExcelAppend(list); // 注意此处需手动Convert
    }
}

前端轮询/export/progress/{taskId} 接口返回百分比,实现进度条。注意:response对象不能跨线程传递,需在异步方法中处理输出流。

高频问答:EasyExcel常见坑与解决方案

Q1:导出时日期格式变成数字串? A:在实体字段加@DateTimeFormat("yyyy-MM-dd HH:mm:ss"),并指定converter = LocalDateTimeStringConverter.class

Q2:读取大Excel出现OOM? A:检查是否误用了doReadAllSync(),正确是:使用EasyExcel.read(inputStream).registerReadListener(listener).sheet().doRead(),并在invoke里处理完就置空。

Q3:对象属性超过256列,报错Too many columns A:设置excel.write().sheet().autoTrim(false)或调整columnWidth,更建议拆分Sheet。

Q4:多线程导出时线程安全吗? A:EasyExcel.write()本身不保证线程安全,每个线程必须创建独立的ExcelWriter实例,并分别输出到不同文件。

性能调优清单:从1000到100万行的最佳实践

操作 错误做法 正确做法
数据查询 全量SELECT * 只查需要的列,用游标分页
写入模式 每次doWrite重开文件 复用ExcelWriter批处理
变量类型 使用String拼接大文本 使用StringBuilder,预分配内存
日志输出 每行打印进度 每1万行打印一次
JVM参数 默认堆大小 设置-Xmx2g,预留临时写入区
文件格式 使用.xlsx(XML) 大数据建议 .xls(二进制)或CSV

终极技巧:对于超大文件(>200万行),直接生成CSV再压缩为ZIP,读取时用BufferedReader逐行处理,性能比EasyExcel快3倍,但失去Excel高级格式。

抱歉,评论功能暂时关闭!