尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

EasyExcel多Sheet与多Table处理实战:从原理到企业级数据导入导出

EasyExcel多Sheet与多Table处理实战:从原理到企业级数据导入导出 1. 从单表到多表为什么EasyExcel的多Sheet/多Table处理是刚需如果你做过企业级的数据处理尤其是报表系统或者数据中台肯定遇到过这样的场景业务方甩过来一个Excel文件打开一看里面密密麻麻十几个Sheet页每个Sheet里可能还套着好几个格式迥异的表格。财务要的月度汇总、销售要的客户明细、运营要的活动数据全挤在一个文件里。这时候如果还用传统的POI或者简单的单Sheet导入导出工具光是解析文件结构、区分数据边界就能写到你头皮发麻更别提后续的数据校验和业务逻辑处理了。这就是EasyExcel在处理“多Sheet”和“多Table”场景下价值凸显的地方。它不是一个简单的Excel读写库而是一个面向复杂业务数据交换的解决方案。核心痛点在于数据结构的映射和读写性能的平衡。一个Sheet对应一个Java对象列表这是基础操作。但当多个逻辑上独立的数据集例如订单头、订单明细、商品信息需要放在同一个Sheet的不同区域或者分散到不同Sheet时就需要更精细的控制。EasyExcel通过其WriteSheet、WriteTable以及监听器模型提供了一套声明式的API让我们能用接近描述业务逻辑的方式来处理这些复杂的Excel结构。我经历过一个数据迁移项目源系统导出的Excel包含一个“主数据”Sheet和五个“交易流水”Sheet。初期用简单循环处理内存溢出和类型转换错误频发。切换到EasyExcel的多Sheet读取配合自定义监听器后代码量减少了60%处理速度提升了一倍而且内存使用变得稳定可控。这背后的关键就是吃透了EasyExcel对于复杂结构的抽象能力。2. 核心概念拆解Sheet、Table与Java对象的映射关系很多人刚开始用EasyExcel容易混淆Sheet和Table觉得都是“表”其实它们在EasyExcel的语境下有明确的职责划分。理解这个是玩转多结构导出的前提。Sheet工作表这是Excel文件的一级容器一个文件至少有一个Sheet。在EasyExcel中WriteSheet对象代表一个准备写入的Sheet。它的核心配置包括Sheet名称sheetName、索引sheetNo等。通常一个WriteSheet会写入一组相同结构的数据比如所有的“用户信息”记录。Table表格这是Sheet内部的二级容器。一个Sheet里可以包含多个Table这些Table在物理位置上是上下排列的。WriteTable对象用于向同一个Sheet内写入多组结构不同的数据。这是实现“一个Sheet多个子表”的关键。每个WriteTable需要独立指定表头head和与之匹配的数据。映射关系与代码模型// 假设我们要导出一个包含两种数据的Sheet // 1. 顶部公司概要信息一个Table一行数据多个字段 // 2. 底部员工列表另一个Table多行数据不同字段 // 定义数据对象 Data public class CompanySummary { ExcelProperty(公司名称) private String name; ExcelProperty(统计月份) private String month; ExcelProperty(员工总数) private Integer totalStaff; } Data public class Employee { ExcelProperty(工号) private String id; ExcelProperty(姓名) private String employeeName; ExcelProperty(部门) private String department; } // 在写入时逻辑如下 // 1. 先创建Sheet WriteSheet writeSheet EasyExcel.writerSheet(月度报告).build(); // 2. 创建第一个Table用于写入CompanySummary ListListString companyHead Arrays.asList( Arrays.asList(公司名称), Arrays.asList(统计月份), Arrays.asList(员工总数) ); WriteTable companyTable EasyExcel.writerTable(0).head(companyHead).build(); // 3. 创建第二个Table用于写入Employee列表 ListListString employeeHead Arrays.asList( Arrays.asList(工号), Arrays.asList(姓名), Arrays.asList(部门) ); WriteTable employeeTable EasyExcel.writerTable(1).head(employeeHead).build(); // 4. 按顺序写入 excelWriter.write(companySummaryList, writeSheet, companyTable); excelWriter.write(employeeList, writeSheet, employeeTable);通过这样的设计EasyExcel将文件物理结构Sheet和业务逻辑结构Table解耦。在读取时同样可以通过实现多个AnalysisEventListener来分别监听不同Table的数据或者在一个监听器里根据行号、内容来判断数据属于哪个逻辑表。注意WriteTable的构建顺序就是其在Sheet中的写入顺序。writerTable(0)会先写写在上面writerTable(1)会接着写在下面。中间默认会有一个空行作为分隔这个空行可以通过relativeHeadRowIndex等参数进行微调。3. 实战分步实现多Sheet与多Table的导出理论清楚了我们来看一个完整的导出案例。需求是生成一份项目报告包含两个Sheet。第一个Sheet“项目概览”里上方是项目基本信息一个Table下方是核心成员列表另一个Table。第二个Sheet“任务清单”是一个简单的任务列表。3.1 环境准备与依赖配置首先确保你的Spring Boot项目中引入了EasyExcel的依赖。我强烈建议使用阿里云官方仓库的版本稳定性和兼容性最好。dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version !-- 请使用当前最新稳定版 -- /dependency这里有个小坑如果你的项目还用了老版本的POI可能会引发冲突。EasyExcel底层依赖POI但它已经管理了特定版本的POI依赖。如果出现NoSuchMethodError或ClassNotFoundException检查一下你的依赖树把其他地方引入的POI排除掉。exclusion groupIdorg.apache.poi/groupId artifactIdpoi/artifactId /exclusion exclusion groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId /exclusion3.2 定义数据模型与注解数据模型的定义决定了导出的灵活度。除了基本的ExcelProperty还有几个注解在复杂场景下特别有用。// 项目概览 - 基本信息表 Data public class ProjectInfo { ExcelProperty(value 项目编号, index 0) private String projectCode; ExcelProperty(value 项目名称, index 1) private String projectName; ExcelProperty(value {项目经理, 姓名}, index 2) // 复杂表头占两行 private String managerName; ExcelProperty(value {项目经理, 联系方式}, index 3) private String managerPhone; ExcelProperty(value 立项日期, index 4) DateTimeFormat(yyyy-MM-dd) private Date startDate; // 忽略某些字段不导出到Excel ExcelIgnore private String internalRemark; } // 项目概览 - 核心成员表 Data public class CoreMember { ExcelProperty(value 序号, index 0) private Integer order; ExcelProperty(value 成员姓名, index 1) private String name; ExcelProperty(value 角色, index 2) private String role; ExcelProperty(value 投入工时, index 3) private Double manHours; } // 任务清单Sheet的数据模型 Data public class TaskItem { ExcelProperty(value 任务ID, index 0) private String taskId; ExcelProperty(value 任务描述, index 1) private String description; ExcelProperty(value 状态, index 2) private String status; // 如进行中、已完成、已延期 ExcelProperty(value 截止日期, index 3) DateTimeFormat(yyyy-MM-dd) private Date deadline; }DateTimeFormat注解可以确保日期类型按照指定格式写入Excel单元格避免变成一串数字。ExcelIgnore则非常实用有些内部字段如数据库ID、创建时间不需要导出用这个注解标记即可。3.3 组装数据与执行导出这是最核心的步骤我们需要创建ExcelWriter并管理多个WriteSheet和WriteTable。Service public class ComplexExportService { public void exportComplexReport(HttpServletResponse response) throws IOException { // 1. 设置响应头告诉浏览器这是一个要下载的Excel文件 String fileName URLEncoder.encode(项目综合报告_ System.currentTimeMillis(), UTF-8).replaceAll(\\, %20); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); response.setHeader(Content-disposition, attachment;filename*utf-8 fileName .xlsx); // 2. 获取ExcelWriter对象。这里直接输出到HttpServletResponse的OutputStream。 // 切记最后必须调用finish()并且不要关闭这个OutputStreamSpring MVC会处理。 ExcelWriter excelWriter EasyExcel.write(response.getOutputStream()).build(); // --- 开始构建第一个Sheet: 项目概览 --- WriteSheet projectOverviewSheet EasyExcel.writerSheet(0, 项目概览).build(); // 3. 构建第一个Table项目基本信息 (Table 0) // 注意head(ListListString) 的里层List代表一列外层List代表一行。这里我们是一个简单的单行表头。 ListListString projectInfoHead new ArrayList(); projectInfoHead.add(Collections.singletonList(项目编号)); projectInfoHead.add(Collections.singletonList(项目名称)); projectInfoHead.add(Arrays.asList(项目经理, 姓名)); // 复杂表头两行 projectInfoHead.add(Arrays.asList(项目经理, 联系方式)); projectInfoHead.add(Collections.singletonList(立项日期)); WriteTable projectInfoTable EasyExcel.writerTable(0) .head(projectInfoHead) .relativeHeadRowIndex(0) // 表头从Sheet的第0行开始写 .build(); // 4. 构建第二个Table核心成员列表 (Table 1) // 这个Table会紧接着上一个Table的数据下方开始写入。 ListListString memberHead new ArrayList(); memberHead.add(Collections.singletonList(序号)); memberHead.add(Collections.singletonList(成员姓名)); memberHead.add(Collections.singletonList(角色)); memberHead.add(Collections.singletonList(投入工时)); WriteTable coreMemberTable EasyExcel.writerTable(1) .head(memberHead) // 不指定relativeHeadRowIndexEasyExcel会自动计算位置放在上一个Table的数据下方。 .build(); // 5. 模拟数据 ListProjectInfo projectInfoList Collections.singletonList( new ProjectInfo(PROJ-2023-001, CRM系统重构, 张三, 13800138000, new Date()) ); ListCoreMember memberList Arrays.asList( new CoreMember(1, 李四, 后端开发, 120.5), new CoreMember(2, 王五, 前端开发, 80.0), new CoreMember(3, 赵六, 测试工程师, 60.0) ); // 6. 按顺序写入第一个Sheet excelWriter.write(projectInfoList, projectOverviewSheet, projectInfoTable); excelWriter.write(memberList, projectOverviewSheet, coreMemberTable); // --- 开始构建第二个Sheet: 任务清单 --- WriteSheet taskSheet EasyExcel.writerSheet(1, 任务清单).build(); // 这个Sheet只有一个简单的表所以直接用write方法不需要单独建Table。 ListTaskItem taskList Arrays.asList( new TaskItem(TASK-001, 数据库设计评审, 已完成, parseDate(2023-10-01)), new TaskItem(TASK-002, 用户模块接口开发, 进行中, parseDate(2023-11-15)), new TaskItem(TASK-003, 集成测试, 未开始, parseDate(2023-12-01)) ); excelWriter.write(taskList, taskSheet); // 7. 至关重要的一步完成写入并释放资源 excelWriter.finish(); } private Date parseDate(String dateStr) { try { return new SimpleDateFormat(yyyy-MM-dd).parse(dateStr); } catch (ParseException e) { return new Date(); } } }这段代码有几个关键点ExcelWriter的生命周期必须在所有数据写入后调用finish()它负责将内存中的数据流刷到输出流并关闭一些内部资源。但传入的OutputStream如response.getOutputStream()不要手动关闭。Table的写入顺序writerTable(0),writerTable(1)的编号决定了它们在Sheet中的上下位置。写的时候也要按这个顺序调用write方法。表头定义head参数接受ListListString。这是EasyExcel定义表头最灵活的方式。内层List表示一列的表头内容从上到下外层List表示所有列的集合。对于复杂表头多行只需在内层List中放入多个字符串即可。3.4 样式自定义与高级配置默认导出的Excel是朴素的黑白格。企业报告往往需要加粗标题、添加颜色、设置列宽。EasyExcel通过WriteHandler写入处理器和CellWriteHandler单元格写入处理器来实现。例如我们想给“项目概览”Sheet的第一个Table项目信息的表头加上蓝色背景和加粗字体// 自定义样式处理器 public class ProjectInfoStyleHandler implements CellWriteHandler { Override public void afterCellDispose(CellWriteHandlerContext context) { // 只处理第一个Sheet的第一个Table的表头行 if (context.getSheetIndex() 0 context.getTableNo() ! null context.getTableNo() 0 context.getRowIndex() 0) { // 假设表头在第0行 Cell cell context.getCell(); CellStyle cellStyle context.getWriteWorkbookHolder().getCachedWorkbook().createCellStyle(); // 设置背景色 cellStyle.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 设置字体加粗 Font font context.getWriteWorkbookHolder().getCachedWorkbook().createFont(); font.setBold(true); cellStyle.setFont(font); // 设置居中 cellStyle.setAlignment(HorizontalAlignment.CENTER); cell.setCellStyle(cellStyle); } } } // 在创建ExcelWriter时注册这个处理器 ExcelWriter excelWriter EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new ProjectInfoStyleHandler()) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自动列宽策略 .build();CellWriteHandler提供了丰富的钩子beforeCellCreate,afterCellDispose等允许你在单元格创建前后插入逻辑。通过context对象可以获取当前写入的Sheet索引、Table编号、行号、列号、单元格值等信息从而进行精确的样式控制。实操心得样式处理器的注册顺序有时会影响最终效果。通常全局的处理器如自动列宽先注册针对特定范围的处理器后注册。如果样式没生效检查一下你的判断条件SheetIndex,TableNo,RowIndex是否准确特别是TableNo在非Table写入时为null需要小心处理。4. 逆向操作复杂Excel文件的读取与数据分拣导出搞定了导入才是真正的挑战。业务方发来的Excel千奇百怪合并单元格、多余的空行、表头格式不统一都是家常便饭。EasyExcel通过ReadListener读取监听器模型来处理导入它的核心思想是事件驱动一行一行地处理数据内存友好。4.1 设计多Sheet读取策略对于多个Sheet有两种主流读取方式为每个Sheet定义独立的监听器结构清晰各司其职。适合Sheet之间结构差异大、业务逻辑完全独立的场景。使用一个监听器在内部根据Sheet名或索引进行分发代码更集中适合Sheet结构类似或需要跨Sheet关联数据的场景。这里演示第一种方式假设我们要读取刚才导出的那个“项目综合报告.xlsx”。首先为“项目概览”Sheet定义监听器。由于这个Sheet里有两个Table我们需要在监听器里做数据分拣。// 项目基本信息监听器 public class ProjectInfoListener extends AnalysisEventListenerMapInteger, String { /** * 存储解析到的项目基本信息。 * 因为Sheet里有多个Table我们先用Map接住每一行再根据业务规则判断它属于哪个Table。 */ private ListProjectInfo projectInfoList new ArrayList(); private ListCoreMember coreMemberList new ArrayList(); // 表头行第一行的内容用于判断当前读到哪个Table了 private ListString headerRow; // 标记当前正在读取哪个Table。0-未知/未开始1-项目信息表2-成员表 private int currentTableType 0; Override public void invokeHead(MapInteger, ReadCellData? headMap, AnalysisContext context) { // 读取到表头时触发。这里将表头信息转换为字符串列表保存。 headerRow headMap.values().stream() .map(ReadCellData::getStringValue) .collect(Collectors.toList()); // 根据表头特征判断Table类型 if (headerRow ! null headerRow.contains(项目编号)) { currentTableType 1; // 进入项目信息表区域 } else if (headerRow ! null headerRow.contains(成员姓名)) { currentTableType 2; // 进入核心成员表区域 } } Override public void invoke(MapInteger, String dataRow, AnalysisContext context) { // 每一行数据解析完成后触发。dataRow的key是列索引value是单元格字符串值。 if (currentTableType 1) { // 处理项目信息表数据通常只有一行 ProjectInfo info new ProjectInfo(); info.setProjectCode(dataRow.get(0)); info.setProjectName(dataRow.get(1)); // 复杂表头项目经理信息在索引2和3 // 这里假设“姓名”和“联系方式”在同一个对象里实际可能需要更复杂的映射 projectInfoList.add(info); // 通常项目信息只有一行读到下一行表头时就会切换currentTableType } else if (currentTableType 2) { // 处理核心成员表数据 CoreMember member new CoreMember(); member.setOrder(Integer.valueOf(dataRow.get(0))); member.setName(dataRow.get(1)); member.setRole(dataRow.get(2)); member.setManHours(Double.valueOf(dataRow.get(3))); coreMemberList.add(member); } // 如果遇到空行或不符合任何Table特征的行可以选择跳过 } Override public void doAfterAllAnalysed(AnalysisContext context) { // 整个Sheet解析完毕 System.out.println(项目信息读取完成: projectInfoList); System.out.println(核心成员读取完成共 coreMemberList.size() 条); // 这里可以将数据存入数据库或进行后续业务处理 } public ListProjectInfo getProjectInfoList() { return projectInfoList; } public ListCoreMember getCoreMemberList() { return coreMemberList; } }这个监听器的关键在于invokeHead方法。我们通过读取到的表头内容来判断数据区域的类型。这是一种基于内容规则的分拣策略非常灵活。当然如果文件格式非常规范你也可以通过ReadSheet的headRowNumber和tableNo等参数进行更精确的绑定但对于混合Table的Sheet内容判断往往是更可靠的方式。然后为“任务清单”Sheet定义一个更简单的监听器public class TaskItemListener extends AnalysisEventListenerTaskItem { // 直接使用TaskItem对象接收需要ExcelProperty注解的索引与文件列序严格对应 private ListTaskItem taskList new ArrayList(); Override public void invoke(TaskItem taskItem, AnalysisContext context) { taskList.add(taskItem); } Override public void doAfterAllAnalysed(AnalysisContext context) { System.out.println(任务清单读取完成: taskList); } public ListTaskItem getTaskList() { return taskList; } }4.2 执行读取与监听器绑定最后编写读取代码将监听器绑定到对应的Sheet。public void importComplexReport(MultipartFile file) throws IOException { // 1. 创建第一个Sheet的监听器实例 ProjectInfoListener projectInfoListener new ProjectInfoListener(); // 读取第一个Sheet。注意sheetNo从0开始。 ReadSheet readSheet1 EasyExcel.readSheet(0) .head(Map.class) // 对于混合Table先用Map接收原始数据 .registerReadListener(projectInfoListener) .build(); // 2. 创建第二个Sheet的监听器实例 TaskItemListener taskItemListener new TaskItemListener(); // 读取第二个Sheet直接映射到TaskItem对象 ReadSheet readSheet2 EasyExcel.readSheet(1) .head(TaskItem.class) .registerReadListener(taskItemListener) .build(); // 3. 执行同步读取doReadSync或异步读取read // 这里使用doReadSync它会阻塞直到所有Sheet读完适合文件不大、逻辑简单的场景。 // 对于大文件建议使用read方法配合监听器异步处理。 EasyExcel.read(file.getInputStream()) .doReadAll(Arrays.asList(readSheet1, readSheet2)); // 传入所有要读的Sheet // 4. 从监听器中获取解析结果 ListProjectInfo importedProjectInfo projectInfoListener.getProjectInfoList(); ListCoreMember importedMembers projectInfoListener.getCoreMemberList(); ListTaskItem importedTasks taskItemListener.getTaskList(); // ... 后续业务处理如数据校验、入库等 }重要提示使用Map.class作为head类型时invoke方法参数就是MapInteger, Stringkey是列索引从0开始value是单元格字符串。这种方式给了我们最大的灵活性来处理不规则数据但代价是需要自己写逻辑进行字段映射和类型转换。对于结构规整的Sheet直接绑定Java对象如TaskItem.class更省事。4.3 处理导入中的常见“坑点”空行和空值业务Excel里经常有空白行做分隔。在监听器的invoke方法里如果dataRow为空或所有值都为空可以直接return跳过避免插入空数据。数据类型转换错误这是最常出问题的地方。比如Excel里看起来是数字“001”读进来可能是字符串也可能是整数1。在invoke里做转换时如Integer.valueOf一定要用try-catch包裹并记录错误行号和原因给用户友好的提示而不是让整个导入崩溃。表头行数不固定有时表头占两行有时三行。headRowNumber参数可以指定从第几行开始读数据表头行数。但像我们之前那种混合Table可能需要动态判断。这时可以在监听器的invokeHead里更精细地控制。大文件内存溢出AnalysisEventListener是逐行解析的本身不会内存溢出。问题往往出在invoke方法里如果把所有数据都先加到List里文件超大时这个List就会撑爆内存。解决方案是分批次处理每积累一定数量如2000条就执行一次批量入库或处理然后清空临时列表。private static final int BATCH_COUNT 2000; private ListObject cachedDataList new ArrayList(BATCH_COUNT); Override public void invoke(Object data, AnalysisContext context) { cachedDataList.add(data); if (cachedDataList.size() BATCH_COUNT) { saveData(); // 批量处理 cachedDataList.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { saveData(); // 处理最后一批数据 }5. 性能调优与生产环境实践当数据量达到十万甚至百万级时一些默认配置就需要调整了。EasyExcel的默认设置偏向于功能性和兼容性在极端性能场景下可以优化。5.1 导出性能优化关闭自动列宽计算LongestMatchColumnWidthStyleStrategy会自动遍历所有数据计算最合适的列宽数据量大时非常耗时。如果对列宽要求不严格可以不注册这个处理器或者手动设置固定列宽。使用useDefaultStyle(false)如果不需要任何单元格样式创建ExcelWriter时可以传入此配置能减少一些样式对象的创建开销。分批次查询和写入这是最重要的优化点。不要一次性从数据库查询百万数据放到内存里再写入。应该使用分页查询查一批如5000条写一批。int pageSize 5000; int pageNum 1; ExcelWriter excelWriter ...; WriteSheet writeSheet ...; while (true) { ListYourData batchList dataService.getDataByPage(pageNum, pageSize); if (batchList.isEmpty()) { break; } excelWriter.write(batchList, writeSheet); pageNum; batchList.clear(); // 及时释放当前批次引用帮助GC } excelWriter.finish();谨慎使用inMemory模式默认情况下EasyExcel会使用SXSSFWorkbook流式写入数据先写到临时文件再合并到最终输出流内存占用小。除非有特殊需求如大量使用样式导致SXSSF受限否则不要轻易切换到完全在内存中操作的XSSFWorkbook模式。5.2 导入性能优化调整ReadCacheSize在构建ExcelReader时可以设置读取缓存大小。默认值通常够用但在极端情况下可以微调。EasyExcel.read(inputStream) .readCache(new MapCache()) // 使用Map缓存默认 .readCacheSize(1000) // 调整缓存行数 .sheet() .doRead();异步处理与批量提交如前所述在监听器的invoke方法中实现批量处理并考虑将处理逻辑如数据校验、转换、入库放入单独的线程池避免阻塞读取线程。但要注意事务边界和错误回滚。文件格式选择.xlsx文件基于XML的读取效率通常比旧的.xls二进制格式要高尤其是大文件。5.3 稳定性保障与监控超时与中断处理对于HTTP导出接口要设置合理的响应超时时间。对于非常耗时的导出可以考虑改为异步任务生成文件后提供下载链接。在导入时也要有机制能中断长时间运行的读取任务。内存监控在生产环境需要监控导入导出服务的内存使用情况。特别是导出如果采用分页查询要确保每一批数据写出后Java堆中的对象能被及时垃圾回收。可以观察GC日志避免出现Full GC。错误隔离与重试对于导入任务某一行数据的格式错误不应该导致整个任务失败。应该在监听器里捕获每行的异常记录到错误列表并继续处理后续行。任务结束后将成功数据和错误报告一并返回给用户。6. 进阶动态表头、模板导出与数据校验对于更复杂的业务比如报表格式经常变动或者需要导出带有复杂公式、图表、固定样式的文件还有更高级的用法。6.1 动态表头生成有时导出的列是不固定的由用户在前端选择决定。这时就不能用固定的ExcelProperty注解了。我们需要动态构建表头。public void exportWithDynamicHeaders(HttpServletResponse response, ListString selectedFields) throws IOException { // selectedFields 例如: [姓名, 部门, 销售额] // 1. 动态构建表头 ListListString head new ArrayList(); for (String field : selectedFields) { head.add(Collections.singletonList(field)); } // 2. 动态查询数据。这里假设数据是ListMapString, Object格式Map的key是字段名。 ListMapString, Object dataList dataService.getDataByFields(selectedFields); // 3. 将数据Map按表头顺序转换为ListListObject ListListObject excelData new ArrayList(); for (MapString, Object rowMap : dataList) { ListObject rowData new ArrayList(); for (String field : selectedFields) { rowData.add(rowMap.get(field)); } excelData.add(rowData); } // 4. 写入Excel。注意这里没有对应的Java对象所以用WriteCellData直接写值。 ExcelWriter excelWriter EasyExcel.write(response.getOutputStream()).build(); WriteSheet writeSheet EasyExcel.writerSheet(动态报表) .head(head) // 设置动态表头 .build(); // 使用write方法的重载版本直接写入ListListObject excelWriter.write(excelData, writeSheet); excelWriter.finish(); }这种方式完全脱离了固定的数据模型非常灵活但代价是需要手动处理数据组装和类型转换。6.2 基于模板的导出当Excel文件有严格的格式要求如公司LOGO、固定的说明文字、复杂的单元格样式和公式时最好使用模板导出。先让业务人员在Excel中设计好模板文件.xlsx程序中只负责向特定位置填充数据。准备模板在Excel中设计好样式和固定内容在需要填充数据的地方用{}占位例如{projectName}。使用模板写入// 获取模板文件的输入流 InputStream templateInputStream this.getClass().getClassLoader().getResourceAsStream(templates/project_report_template.xlsx); ExcelWriter excelWriter EasyExcel.write(response.getOutputStream()) .withTemplate(templateInputStream) .build(); WriteSheet writeSheet EasyExcel.writerSheet().build(); // 准备填充数据这是一个Mapkey是模板中的占位符 MapString, Object fillData new HashMap(); fillData.put(projectName, CRM系统重构); fillData.put(reportDate, new Date()); // 对于列表数据key对应模板中表格区域的占位符 fillData.put(memberList, coreMemberList); // coreMemberList是ListCoreMember // 执行填充 FillConfig fillConfig FillConfig.builder().forceNewRow(Boolean.TRUE).build(); // 列表数据强制新行 excelWriter.fill(fillData, writeSheet); excelWriter.fill(new FillWrapper(member, coreMemberList), fillConfig, writeSheet); excelWriter.finish(); templateInputStream.close();模板导出能完美保留格式但需要确保模板中的占位符和程序中的key一致并且列表填充的区域要预留足够的行。6.3 数据校验与错误反馈导入的数据必须经过校验。EasyExcel本身提供了简单的校验注解如NotNull但业务校验往往更复杂。我推荐在监听器的invoke方法中或在doAfterAllAnalysed之后进行集中校验。public class ValidatingListener extends AnalysisEventListenerYourData { private ListYourData successList new ArrayList(); private ListRowError errorList new ArrayList(); // 自定义错误信息类 Override public void invoke(YourData data, AnalysisContext context) { // 基本格式校验 if (StringUtils.isBlank(data.getName())) { errorList.add(new RowError(context.readRowHolder().getRowIndex(), 姓名不能为空)); return; } // 业务逻辑校验 if (data.getAge() ! null data.getAge() 18) { errorList.add(new RowError(context.readRowHolder().getRowIndex(), 年龄必须大于等于18岁)); return; } successList.add(data); } Override public void doAfterAllAnalysed(AnalysisContext context) { // 所有行解析完毕可以进行跨行校验比如唯一性检查 SetString nameSet new HashSet(); for (YourData data : successList) { if (!nameSet.add(data.getName())) { // 找到重复项从successList移除加入errorList // 这里需要记录行号可能需要额外维护一个映射 } } // 最终successList是校验通过的数据errorList包含所有错误信息 } }对于错误可以生成一个详细的错误报告Excel列出错误行号、列名和原因供用户下载修正。这比直接抛出一个异常要友好得多。从简单的单表导出导入到复杂的多Sheet多Table处理再到动态表头、模板填充和严格的数据校验EasyExcel提供了一套渐进式的工具集。关键在于理解其核心抽象Writer/Reader, Sheet, Table, Listener并灵活组合。在实际项目中我建议从最简单的场景开始随着业务复杂度的提升逐步引入更高级的特性并始终把数据准确性、处理性能和用户体验放在首位。
返回列表