EasyExcel实战:高效处理Excel导入导出的进阶技巧
1. 为什么选择EasyExcel处理Excel文件第一次接触Excel导入导出功能时我尝试过Apache POI。当时为了处理一个20MB的Excel文件内存直接飙到2GB服务器差点崩溃。后来发现了EasyExcel这个神器同样的文件内存占用只有200MB左右处理速度还快了三倍。这就是为什么现在Java开发者都在用EasyExcel来处理Excel文件。EasyExcel是阿里巴巴开源的一个基于Java的Excel处理工具底层还是用的POI但通过创新的设计解决了POI最致命的内存问题。它采用逐行解析的模式不像POI那样需要把整个文件加载到内存。我做过测试处理10万行数据时EasyExcel的内存占用只有POI的1/10。在实际项目中EasyExcel特别适合这些场景需要处理大数据量Excel文件10万行以上对内存敏感的服务端应用需要频繁导入导出Excel的业务系统对Excel格式有特殊要求的场景2. 环境准备与基础配置2.1 依赖引入与版本选择我建议使用Maven管理依赖在pom.xml中添加以下配置dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency这里有个坑要注意EasyExcel 2.x和3.x的API有不兼容的改动。我刚开始升级时踩过坑比如3.x版本的WriteSheet和WriteTable需要分开构建。如果是从旧项目迁移建议先看下官方升级指南。2.2 基础实体类设计实体类是与Excel映射的核心设计时要注意这些点Data public class UserData { ExcelProperty(用户ID) private Long userId; ExcelProperty(value 用户名, index 1) private String username; ExcelProperty(value 注册时间, converter LocalDateTimeConverter.class) private LocalDateTime registerTime; ExcelIgnore private String secretField; }几个实用技巧ExcelProperty的index属性可以明确指定列顺序避免因字段顺序变动导致问题使用ExcelIgnore标注不需要映射到Excel的字段日期等特殊类型建议配合自定义转换器使用后面会详细讲3. 高级导入功能实战3.1 大数据量导入的内存优化处理10万行以上的数据时直接全量读取会OOM。正确的做法是使用监听器模式public class UserDataListener extends AnalysisEventListenerUserData { private static final int BATCH_SIZE 1000; private ListUserData cachedList new ArrayList(BATCH_SIZE); Override public void invoke(UserData data, AnalysisContext context) { cachedList.add(data); if (cachedList.size() BATCH_SIZE) { saveData(); cachedList.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!cachedList.isEmpty()) { saveData(); } } private void saveData() { // 批量入库逻辑 userRepository.saveAll(cachedList); } }使用时这样调用String fileName large_file.xlsx; EasyExcel.read(fileName, UserData.class, new UserDataListener()) .sheet() .doRead();这种模式下内存中最多只保留BATCH_SIZE条数据完美解决内存问题。我在处理50万行数据时内存占用稳定在200MB以内。3.2 复杂表头与多级表头处理遇到合并单元格等复杂表头时可以这样处理Getter Setter public class MultiHeaderData { ExcelProperty({主分类, 子分类, 字段名}) private String field1; ExcelProperty({主分类, 子分类, 另一个字段}) private String field2; }读取时指定头行数EasyExcel.read(fileName, MultiHeaderData.class, listener) .headRowNumber(3) // 三级表头 .sheet() .doRead();4. 高级导出技巧4.1 动态列导出有时需要根据条件动态决定导出哪些列。我的实现方案public void dynamicExport(HttpServletResponse response) { SetString includeFields getFieldsToExport(); // 动态获取需要导出的字段 ExcelWriter writer EasyExcel.write(response.getOutputStream()) .registerWriteHandler(new AbstractColumnWidthStyleStrategy() { Override protected void setColumnWidth(WriteSheetHolder writeSheetHolder, ListCellData cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 动态设置列宽 sheet.setColumnWidth(cell.getColumnIndex(), 20 * 256); } }) .build(); WriteSheet sheet EasyExcel.writerSheet(数据) .includeColumnFiledNames(includeFields) .build(); writer.write(queryData(), sheet); writer.finish(); }4.2 百万级数据导出优化导出超大数据量时我推荐使用分页查询多次写入的方式try (ExcelWriter writer EasyExcel.write(fileName).build()) { WriteSheet sheet EasyExcel.writerSheet(数据).build(); int page 1; while (true) { PageUserData pageData userService.getByPage(page, 5000); if (pageData.isEmpty()) { break; } writer.write(pageData.getContent(), sheet); page; } }配合web下载时记得设置响应头response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(fileName, UTF-8));5. 性能调优实战5.1 缓存与复用优化创建ExcelWriter的开销较大在高并发场景下可以这样优化// 使用对象池复用Writer private final GenericObjectPoolExcelWriter writerPool; public void initPool() { writerPool new GenericObjectPool(new BasePooledObjectFactoryExcelWriter() { Override public ExcelWriter create() { return EasyExcel.write().build(); } }); } public void exportData(ListData list) { ExcelWriter writer writerPool.borrowObject(); try { writer.write(list, EasyExcel.writerSheet(Sheet1).build()); } finally { writer.reset(); // 重置状态 writerPool.returnObject(writer); } }5.2 多线程处理技巧对于CPU密集型的转换操作可以使用并行流ListData processedData rawData.parallelStream() .map(data - { // 复杂转换逻辑 return convertData(data); }) .collect(Collectors.toList());但要注意写Excel时不要用多线程EasyExcel本身不是线程安全的线程数不要超过CPU核心数对于IO密集型操作效果不明显6. 常见问题解决方案6.1 日期格式混乱问题我遇到最常见的坑就是日期格式问题。推荐这样统一处理public class LocalDateTimeConverter implements ConverterLocalDateTime { Override public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { if (cellData.getType() CellDataTypeEnum.NUMBER) { return LocalDateTime.ofInstant( Instant.ofEpochMilli((long)(cellData.getNumberValue().doubleValue() * 24 * 3600 * 1000)), ZoneId.systemDefault()); } return LocalDateTime.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); } }6.2 大数据量导出内存溢出如果必须全量数据在内存中处理可以调整JVM参数-XX:UseG1GC -Xms512m -Xmx2g -XX:MaxGCPauseMillis200但更好的办法还是采用前面提到的分批次处理方案。7. 企业级应用实践7.1 与Spring Boot深度集成在我的项目中是这样封装的RestController RequestMapping(/excel) public class ExcelController { GetMapping(/export) public void export(HttpServletResponse response, RequestParam MultiValueMapString, String queryParams) { // 1. 设置响应头 ExcelResponseUtil.prepareResponse(response, 导出数据.xlsx); // 2. 查询数据 ListData data dataService.queryByParams(queryParams); // 3. 导出 EasyExcel.write(response.getOutputStream(), Data.class) .registerConverter(new CustomConverter()) .sheet(数据) .doWrite(data); } }7.2 分布式环境下的导出方案对于超大数据量千万级的导出我的架构方案是前端触发导出请求后端生成任务ID提交到消息队列异步任务处理数据上传到OSS前端轮询或接收通知下载文件核心代码片段Async public void asyncExport(Long taskId, ExportParams params) { String tempFile /tmp/ UUID.randomUUID() .xlsx; try { // 分页查询写入 try (ExcelWriter writer EasyExcel.write(tempFile).build()) { int page 1; while (true) { PageData pageData dataRepository.findByParams(params, PageRequest.of(page, 5000)); if (pageData.isEmpty()) break; writer.write(pageData.getContent(), EasyExcel.writerSheet(数据).build()); page; } } // 上传到OSS String ossUrl ossClient.upload(tempFile); taskRepository.updateStatus(taskId, SUCCESS, ossUrl); } catch (Exception e) { taskRepository.updateStatus(taskId, FAILED, e.getMessage()); } finally { Files.deleteIfExists(Paths.get(tempFile)); } }