当前位置: 首页 > news >正文

EasyExcel实现Excel复杂格式导出:合并单元格与样式设置实战

EasyExcel实现Excel复杂格式导出:合并单元格与样式设置实战

背景介绍

项目技术栈

在一次需求的开发过程中,我们需要实现一个具有复杂格式要求的Excel导出功能。项目采用了以下技术栈:

  • Spring Boot: 3.2.0
  • JDK: 21
  • EasyExcel: 3.3.4 (阿里巴巴开源的Excel处理工具)
  • Apache POI: 5.2.4 (EasyExcel底层依赖)
  • MyBatis-Plus: 3.5.5
  • Maven: 3.9.x

业务需求

需要导出的Excel文件具有特定的格式要求:

  1. 第1行:标题行"XXXX台账",需要合并A1:O1单元格
  2. 第2行:15个字段的表头行
  3. 第3行:详细的填写说明
  4. 第4行及以后:实际数据内容

每一行都需要不同的样式设置:

  • 标题行:Calibri字体,16号,加粗,居中对齐
  • 表头行:Calibri字体,11号,加粗,居中对齐
  • 说明行:Calibri字体,10号,斜体,左对齐,自动换行
  • 数据行:Calibri字体,10号,普通,左对齐

技术实现方案

1. 基础架构设计

首先,我们需要禁用EasyExcel的默认表头机制,采用手动控制的方式来精确控制每一行的内容和样式。

@Override
public void exportMarketingPhoneData(MarketingPhoneQueryDTO queryDTO, HttpServletResponse response) throws IOException {log.info("Export marketing phone data with params: {}", queryDTO);// 设置响应头setExcelExportResponseHeaders(response, EXPORT_FILE_NAME_PREFIX);// 初始化Excel写入器,关键:不使用模板类,直接操作工作表ExcelWriter excelWriter = null;try {excelWriter = EasyExcelFactory.write(response.getOutputStream()).excelType(ExcelTypeEnum.XLSX).registerWriteHandler(new TitleMergeStrategy()) // 注册自定义处理器.build();WriteSheet writeSheet = EasyExcelFactory.writerSheet(EXPORT_SHEET_NAME).needHead(false) // 关键:禁用默认表头.build();// 分层写入数据writeTitleRow(excelWriter, writeSheet);      // 写入标题行writeHeaderRow(excelWriter, writeSheet);     // 写入表头行  writeDescriptionRow(excelWriter, writeSheet); // 写入说明行writeDataRows(excelWriter, writeSheet, queryDTO); // 写入数据行log.info("Export marketing phone data completed");} finally {if (excelWriter != null) {excelWriter.finish();}}
}

2. 数据写入方法实现

/*** 写入标题行(第1行)*/
private void writeTitleRow(ExcelWriter excelWriter, WriteSheet writeSheet) {List<List<String>> titleData = new ArrayList<>();List<String> titleRow = new ArrayList<>();titleRow.add("台账");// 为其他14列填充空字符串,确保合并单元格正确for (int i = 1; i < 15; i++) {titleRow.add("");}titleData.add(titleRow);excelWriter.write(titleData, writeSheet);
}/*** 写入表头行(第2行)*/
private void writeHeaderRow(ExcelWriter excelWriter, WriteSheet writeSheet) {List<List<String>> headerData = new ArrayList<>();List<String> headerRow = Arrays.asList("序号", "省份", "地市", "号码类型", "客户名称", "客户编码", "接入时间","客户资质", "使用期限", "线路资源类型", "实际使用客户", "实际使用客户的接入时间", "场景用途", "号码", "办理渠道");headerData.add(headerRow);excelWriter.write(headerData, writeSheet);
}

核心技术难点与解决方案

问题1:单元格合并冲突

遇到的问题

Cannot add merged region A1:O1 to sheet because it overlaps with an existing merged region

问题分析
最初尝试使用AbstractMergeStrategy抽象类时,EasyExcel在某些情况下会重复尝试合并同一区域的单元格,导致冲突。

解决方案
改用SheetWriteHandler接口,在afterSheetCreate方法中执行一次性合并:

private static class TitleMergeStrategy implements SheetWriteHandler, RowWriteHandler {@Overridepublic void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {Sheet sheet = writeSheetHolder.getSheet();// 合并第一行的A1到O1(行索引0,列索引0-14)CellRangeAddress cellRangeAddress = new CellRangeAddress(0, 0, 0, 14);sheet.addMergedRegion(cellRangeAddress);log.info("Successfully merged title row: A1:O1");}
}

问题2:autoSizeColumn列宽自适应失败

遇到的问题

IllegalStateException: Could not auto-size column. Make sure the column was tracked prior to auto-sizing the column.

问题分析
EasyExcel基于Apache POI的SXSSFSheet实现,为了内存效率,默认不跟踪所有列的内容,因此无法自动计算列宽。

解决方案
放弃autoSizeColumn,在sheet创建时预设固定列宽:

@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {Sheet sheet = writeSheetHolder.getSheet();// 合并标题行CellRangeAddress cellRangeAddress = new CellRangeAddress(0, 0, 0, 14);sheet.addMergedRegion(cellRangeAddress);// 预设列宽,避免autoSizeColumn的追踪问题int[] columnWidths = {2500,  // 序号2500,  // 省份  3000,  // 地市3500,  // 号码类型6000,  // 客户名称4000,  // 客户编码3500,  // 接入时间8000,  // 客户资质3500,  // 使用期限4000,  // 线路资源类型6000,  // 实际使用客户4000,  // 实际使用客户的接入时间8000,  // 场景用途4000,  // 号码3000   // 办理渠道};for (int i = 0; i < 15; i++) {sheet.setColumnWidth(i, columnWidths[i]);}
}

问题3:样式设置时机和作用域

遇到的问题
样式设置不生效,特别是合并单元格的样式无法正确应用。

解决方案
afterRowDispose方法中设置样式,确保在行数据完全写入后再应用样式:

@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {if (row != null) {Workbook workbook = writeSheetHolder.getParentWriteWorkbookHolder().getWorkbook();int rowIndex = row.getRowNum();if (rowIndex == 0) {// 标题行样式setTitleRowStyle(row, workbook);} else if (rowIndex == 1) {// 表头行样式setHeaderRowStyle(row, workbook);} else if (rowIndex == 2) {// 说明行样式setDescriptionRowStyle(row, workbook);} else {// 数据行样式setDataRowStyle(row, workbook);}}
}

样式设置的最佳实践

1. 样式创建方法

为了确保合并单元格的样式正确应用,需要对所有相关单元格都设置样式:

/*** 设置标题行样式*/
private void setTitleRowStyle(Row row, Workbook workbook) {CellStyle titleStyle = workbook.createCellStyle();Font titleFont = workbook.createFont();// 设置字体titleFont.setFontName("Calibri");titleFont.setFontHeightInPoints((short) 16);titleFont.setBold(true);// 设置样式titleStyle.setFont(titleFont);titleStyle.setAlignment(HorizontalAlignment.CENTER);titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);// 关键:对合并区域的所有单元格都设置样式for (int i = 0; i < 15; i++) {Cell cell = row.getCell(i);if (cell == null) {cell = row.createCell(i);}cell.setCellStyle(titleStyle);}
}/*** 设置说明行样式(支持自动换行)*/
private void setDescriptionRowStyle(Row row, Workbook workbook) {CellStyle descStyle = workbook.createCellStyle();Font descFont = workbook.createFont();// 设置字体descFont.setFontName("Calibri");descFont.setFontHeightInPoints((short) 10);descFont.setItalic(true);// 设置样式descStyle.setFont(descFont);descStyle.setAlignment(HorizontalAlignment.LEFT);descStyle.setVerticalAlignment(VerticalAlignment.CENTER);descStyle.setWrapText(true); // 启用自动换行// 应用到所有说明单元格for (int i = 0; i < 15; i++) {Cell cell = row.getCell(i);if (cell == null) {cell = row.createCell(i);}cell.setCellStyle(descStyle);}
}

2. 行高设置

afterRowCreate方法中设置不同行的高度:

@Override
public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {if (row != null) {int rowIndex = row.getRowNum();if (rowIndex == 0) {row.setHeightInPoints(23.2f); // 标题行高度} else if (rowIndex == 1) {row.setHeightInPoints(16.8f); // 表头行高度} else if (rowIndex == 2) {row.setHeightInPoints(107.0f); // 说明行高度(因为内容较多)} else {row.setHeightInPoints(20); // 数据行高度}}
}

代码优化与最佳实践

1. 常量提取,消除魔法值

// Excel样式常量
private static final String EXCEL_FONT_NAME = "Calibri";
private static final short TITLE_FONT_SIZE = 16;
private static final short HEADER_FONT_SIZE = 11;
private static final short DESCRIPTION_FONT_SIZE = 10;
private static final short DATA_FONT_SIZE = 10;// 行高常量
private static final float TITLE_ROW_HEIGHT = 23.2f;
private static final float HEADER_ROW_HEIGHT = 16.8f;
private static final float DESCRIPTION_ROW_HEIGHT = 107.0f;
private static final float DATA_ROW_HEIGHT = 20f;// Excel结构常量
private static final int EXCEL_COLUMN_COUNT = 15;
private static final int TITLE_START_COLUMN = 0;
private static final int TITLE_END_COLUMN = 14;// 行索引常量
private static final class RowIndex {static final int TITLE = 0;static final int HEADER = 1;static final int DESCRIPTION = 2;static final int DATA_START = 3;
}

2. 方法职责单一化

/*** 创建标题行单元格样式*/
private CellStyle createTitleCellStyle(Workbook workbook) {CellStyle titleStyle = workbook.createCellStyle();Font titleFont = workbook.createFont();titleFont.setFontName(EXCEL_FONT_NAME);titleFont.setFontHeightInPoints(TITLE_FONT_SIZE);titleFont.setBold(true);titleStyle.setFont(titleFont);titleStyle.setAlignment(HorizontalAlignment.CENTER);titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);return titleStyle;
}/*** 获取或创建单元格*/
private Cell getOrCreateCell(Row row, int columnIndex) {Cell cell = row.getCell(columnIndex);if (cell == null) {cell = row.createCell(columnIndex);}return cell;
}

性能优化策略

1. 分批查询避免内存溢出

private void writeDataRows(ExcelWriter excelWriter, WriteSheet writeSheet, MarketingPhoneQueryDTO queryDTO) {final int PAGE_SIZE = 1000; // 每批处理1000条记录int currentPage = 1;while (true) {Page<MarketingPhone> page = new Page<>(currentPage, PAGE_SIZE);LambdaQueryWrapper<MarketingPhone> queryWrapper = buildCountWrapper(queryDTO);IPage<MarketingPhone> pageResult = marketingPhoneMapper.selectPage(page, queryWrapper);if (pageResult.getRecords().isEmpty()) {break;}// 转换为导出数据格式List<List<String>> dataRows = convertToDataRows(pageResult.getRecords(), currentPage, PAGE_SIZE);excelWriter.write(dataRows, writeSheet);currentPage++;// 如果当前页记录数小于页大小,说明已经是最后一页if (pageResult.getRecords().size() < PAGE_SIZE) {break;}}
}

2. 使用流式处理转换数据

private List<List<String>> convertToDataRows(List<MarketingPhone> records, int currentPageNum, int pageSize) {if (records.isEmpty()) {return List.of();}final int startIndex = (currentPageNum - 1) * pageSize;return IntStream.range(0, records.size()).mapToObj(i -> {MarketingPhone record = records.get(i);List<String> row = new ArrayList<>(15);row.add(String.valueOf(startIndex + i + 1)); // 序号row.add(MarketingPhoneUtil.getProvinceName()); // 省份row.add(MarketingPhoneUtil.getCityNameById(record.getCity())); // 地市// ... 其他字段return row;}).toList();
}

总结

通过本次Excel复杂格式导出功能的实现,我们掌握了以下关键技术点:

成功要素

  1. 正确的Handler选择:使用SheetWriteHandler而非AbstractMergeStrategy避免合并冲突
  2. 样式应用时机:在afterRowDispose中设置样式确保数据写入完成
  3. 列宽管理策略:预设固定宽度替代autoSizeColumn避免追踪问题
  4. 分批处理机制:避免大数据量导致的内存溢出

技术收益

  1. 专业的Excel输出:实现了完全符合业务要求的复杂格式
  2. 良好的性能表现:支持大数据量导出而不会内存溢出
  3. 高可维护性:通过常量提取和方法拆分提高代码质量
  4. 可扩展性:架构设计支持未来的格式变更需求

注意事项

  1. 版本兼容性:EasyExcel 3.3.4与Apache POI 5.2.4的兼容性良好
  2. 内存管理:大文件导出时建议使用SXSSFWorkbook的流式写入
  3. 样式复用:避免重复创建相同的CellStyle对象,可以提高性能
  4. 异常处理:确保ExcelWriter在finally块中正确关闭

这个实现方案在生产环境中稳定运行,成功解决了复杂Excel格式导出的技术难题,为类似需求提供了可靠的技术参考。

http://www.dtcms.com/a/264248.html

相关文章:

  • web开发,旅游景点管理系统推荐算法版本demo,基于asp.net,mvc,c#,sql server
  • 编写shell脚本扫描工具,扫描服务器开放了哪些端口(再尝试用python编写一个)
  • Set和Map的解析与应用场景
  • OSPF虚拟链路术语一览:快速掌握网络路由
  • 【字符串方法】split使用介绍
  • Android NDK探索之旅(一)
  • 中心效应:多中心临床试验的关键考量
  • 【科研绘图系列】基于R语言的复杂热图绘制教程:环境因素与染色体效应的可视化
  • 图神经网络(篇二)-基础知识
  • MySQL处理并发访问和高负载的关键技术和策略
  • 设置linux静态IP
  • 创建和连接Vue应用程序实例
  • AI的未来:人类会被取代,还是变得更强大?
  • Go语言的Map
  • 【仿muduo库实现并发服务器】Poller模块
  • Adobe AI高效设计秘籍与创新思维进阶
  • WebSocket扫盲
  • 7 项目立项管理
  • MYSQL-JAVAweb1
  • 华为设备 QoS 流分类与流标记深度解析及实验脚本
  • Ubuntu+Nginx+php+SQLite3+typecho手动搭建个人博客
  • 什么是消息队列?
  • 21.合并两个有序链表
  • android RecyclerView隐藏整个Item后,该Item还占位留白问题
  • 变幻莫测:CoreData 中 Transformable 类型面面俱到(七)
  • IDE全家桶专用快捷键----------个人独家分享!!
  • 计算机网络(三)传输层TCP
  • 630,百度文心大模型4.5系列开源!真香
  • Spring Boot 启动加载执行链路分析
  • Spring Boot 集成 Dufs 通过 WebDAV 实现文件管理