EasyExcel合并单元格:基于 AbstractMergeStrategy 的灵活实现
在生成 Excel 报表时,合并单元格是优化表格可读性的常用手段(如合并相同值的相邻单元格)。EasyExcel 并未直接提供合并单元格的 API,但通过其钩子机制(尤其是 AbstractMergeStrategy 抽象类),可灵活实现自定义合并逻辑。本文将详细讲解如何基于 AbstractMergeStrategy 实现单元格合并,并通过实例演示 “相同值自动合并” 的功能。
合并单元格的核心思路
EasyExcel 合并单元格的核心是通过 单元格钩子(CellWriteHandler) 监听单元格创建过程,在数据写入时动态判断是否需要合并。具体步骤:
- 记录当前单元格的值及其所在行索引;
- 与上一行同列单元格的值对比,若相同则标记为待合并区域;
- 当值发生变化或到达最后一行时,执行合并操作(通过 POI 的
Sheet.addMergedRegion 方法)。
AbstractMergeStrategy 是 EasyExcel 提供的合并策略抽象类,实现了 CellWriteHandler 接口,简化了合并逻辑的开发(只需重写 merge 方法)。
自定义合并策略:相同值自动合并
以下实现一个通用的合并策略:对指定字段(列),自动合并相邻的相同值单元格。
实现自定义合并策略类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| import com.alibaba.excel.metadata.Head; import com.alibaba.excel.write.merge.AbstractMergeStrategy; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.util.CellRangeAddress; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects;
public class SameValueMergeStrategy extends AbstractMergeStrategy {
private final List<String> mergeFieldNames; private final int totalRowCount;
private final Map<String, RowData> lastRowDataMap = new HashMap<>();
private static class RowData { Object value; int rowIndex;
RowData(Object value, int rowIndex) { this.value = value; this.rowIndex = rowIndex; } }
public SameValueMergeStrategy(List<String> mergeFieldNames, int totalRowCount) { this.mergeFieldNames = mergeFieldNames; this.totalRowCount = totalRowCount; }
@Override protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { if (relativeRowIndex == null || relativeRowIndex < 1) { return; }
int currentRowIndex = relativeRowIndex; String currentFieldName = head.getFieldName();
if (mergeFieldNames.contains(currentFieldName)) { Object currentValue = getCellValue(cell);
RowData lastRowData = lastRowDataMap.get(currentFieldName);
if (lastRowData == null) { lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex)); } else { if (!Objects.equals(currentValue, lastRowData.value)) { mergeIfNeeded(sheet, lastRowData, currentRowIndex - 1, cell.getColumnIndex()); lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex)); } else if (currentRowIndex == totalRowCount) { mergeIfNeeded(sheet, lastRowData, currentRowIndex, cell.getColumnIndex()); } } } }
private void mergeIfNeeded(Sheet sheet, RowData lastRowData, int endRowIndex, int columnIndex) { int startRowIndex = lastRowData.rowIndex; if (endRowIndex - startRowIndex > 0) { CellRangeAddress mergeRegion = new CellRangeAddress( startRowIndex, endRowIndex, columnIndex, columnIndex ); sheet.addMergedRegion(mergeRegion); } }
private Object getCellValue(Cell cell) { if (cell == null) { return ""; } CellType cellType = cell.getCellType(); switch (cellType) { case STRING: return cell.getStringCellValue(); case NUMERIC: return cell.getNumericCellValue(); case BOOLEAN: return cell.getBooleanCellValue(); default: return ""; } } }
|
实体类定义
假设需要合并 “部门” 列,实体类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data;
@Data public class Employee { @ExcelProperty("姓名") private String name;
@ExcelProperty("部门") private String department;
@ExcelProperty("职位") private String position; }
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import com.alibaba.excel.EasyExcel; import java.util.ArrayList; import java.util.Arrays; import java.util.List;
public class MergeCellTest { public static void main(String[] args) { List<Employee> dataList = new ArrayList<>(); dataList.add(new Employee("张三", "技术部", "开发工程师")); dataList.add(new Employee("李四", "技术部", "测试工程师")); dataList.add(new Employee("王五", "技术部", "架构师")); dataList.add(new Employee("赵六", "市场部", "市场专员")); dataList.add(new Employee("钱七", "市场部", "市场经理"));
List<String> mergeFields = Arrays.asList("department"); SameValueMergeStrategy mergeStrategy = new SameValueMergeStrategy(mergeFields, dataList.size());
String filePath = "员工信息_合并单元格.xlsx"; EasyExcel.write(filePath, Employee.class) .registerWriteHandler(mergeStrategy) .sheet("员工表") .doWrite(dataList);
System.out.println("文件生成成功:" + filePath); } }
|
实现说明与扩展
核心逻辑解析
- 缓存机制:通过
lastRowDataMap 记录上一行的数据,避免重复读取;
- 合并时机:当当前值与上一行不同,或到达最后一行时,触发合并操作;
- 字段过滤:仅对
mergeFieldNames 中指定的字段执行合并,不影响其他列。
扩展场景
多列合并
只需在 mergeFieldNames 中添加多个字段名即可,例如同时合并 “部门” 和 “分公司”:
1
| List<String> mergeFields = Arrays.asList("department", "company");
|
跨列合并
修改 CellRangeAddress 的列索引范围,例如合并 “部门” 和 “职位” 列(需确保两行数据完全相同):
1 2 3 4 5
| CellRangeAddress mergeRegion = new CellRangeAddress( startRowIndex, endRowIndex, 1, 2 );
|
条件合并
在 merge 方法中添加自定义条件,例如仅合并值为 “技术部” 的单元格:
1 2 3 4
| if (!Objects.equals(currentValue, lastRowData.value) || "技术部".equals(currentValue)) { }
|
注意事项
- 表头行处理:
relativeRowIndex 从 0 开始,其中 0 代表表头行,需跳过(避免合并表头);
- 性能优化:合并操作会触发 Excel 内部重绘,大量合并可能影响性能,建议仅对必要字段合并;
- 合并冲突:同一区域多次合并会导致异常,需确保合并区域不重叠;
- POI 版本兼容:
Sheet.addMergedRegion 是 POI 的方法,需与 EasyExcel 依赖的 POI 版本匹配(推荐使用 EasyExcel 3.x 配套的 POI 5.x)。