EasyExcel合并单元格
EasyExcel合并单元格:基于 AbstractMergeStrategy 的灵活实现
在生成 Excel 报表时,合并单元格是优化表格可读性的常用手段(如合并相同值的相邻单元格)。EasyExcel 并未直接提供合并单元格的 API,但通过其钩子机制(尤其是 AbstractMergeStrategy 抽象类),可灵活实现自定义合并逻辑。本文将详细讲解如何基于 AbstractMergeStrategy 实现单元格合并,并通过实例演示 “相同值自动合并” 的功能。
合并单元格的核心思路
EasyExcel 合并单元格的核心是通过 单元格钩子(CellWriteHandler) 监听单元格创建过程,在数据写入时动态判断是否需要合并。具体步骤:
- 记录当前单元格的值及其所在行索引;
- 与上一行同列单元格的值对比,若相同则标记为待合并区域;
- 当值发生变化或到达最后一行时,执行合并操作(通过 POI 的
Sheet.addMergedRegion方法)。
AbstractMergeStrategy 是 EasyExcel 提供的合并策略抽象类,实现了 CellWriteHandler 接口,简化了合并逻辑的开发(只需重写 merge 方法)。
自定义合并策略:相同值自动合并
以下实现一个通用的合并策略:对指定字段(列),自动合并相邻的相同值单元格。
实现自定义合并策略类
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 {
// 需要合并的字段名(与实体类的 @ExcelProperty 字段对应)
private final List<String> mergeFieldNames;
// 数据总条数(用于判断是否为最后一行)
private final int totalRowCount;
// 存储上一行的数据:key=字段名,value=Pair(值, 行索引)
private final Map<String, RowData> lastRowDataMap = new HashMap<>();
// 内部类:存储行数据(值和行索引)
private static class RowData {
Object value; // 单元格值
int rowIndex; // 行索引(从 0 开始)
RowData(Object value, int rowIndex) {
this.value = value;
this.rowIndex = rowIndex;
}
}
public SameValueMergeStrategy(List<String> mergeFieldNames, int totalRowCount) {
this.mergeFieldNames = mergeFieldNames;
this.totalRowCount = totalRowCount;
}
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
// relativeRowIndex:相对行索引(0 表示表头行,1 开始为数据行)
// 跳过表头行(只处理数据行)
if (relativeRowIndex == null || relativeRowIndex < 1) {
return;
}
// 当前数据行的绝对索引(0 开始,表头行占 1 行)
int currentRowIndex = relativeRowIndex;
// 当前字段名(从 Head 中获取)
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)) {
// 值不同:合并上一段相同值的单元格(如果行数 >1)
mergeIfNeeded(sheet, lastRowData, currentRowIndex - 1, cell.getColumnIndex());
// 更新缓存为当前行数据
lastRowDataMap.put(currentFieldName, new RowData(currentValue, currentRowIndex));
} else if (currentRowIndex == totalRowCount) {
// 值相同且为最后一行:合并到最后一行
mergeIfNeeded(sheet, lastRowData, currentRowIndex, cell.getColumnIndex());
}
}
}
}
/**
* 执行合并操作(如果需要)
* @param sheet 工作表
* @param lastRowData 上一行数据
* @param endRowIndex 结束行索引
* @param columnIndex 列索引
*/
private void mergeIfNeeded(Sheet sheet, RowData lastRowData, int endRowIndex, int columnIndex) {
int startRowIndex = lastRowData.rowIndex;
// 只有当行数差 >0 时才合并(至少 2 行)
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 "";
}
}
}
实体类定义
假设需要合并 “部门” 列,实体类如下:
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
public class Employee {
private String name;
// 需要合并的字段
private String department;
private String position;
}
测试代码
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) {
// 1. 准备测试数据(部门相同的相邻行将被合并)
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("钱七", "市场部", "市场经理")); // 同部门
// 2. 配置合并策略:合并“department”字段,数据总条数为 5
List<String> mergeFields = Arrays.asList("department"); // 要合并的字段名
SameValueMergeStrategy mergeStrategy = new SameValueMergeStrategy(mergeFields, dataList.size());
// 3. 写入 Excel 并应用合并策略
String filePath = "员工信息_合并单元格.xlsx";
EasyExcel.write(filePath, Employee.class)
.registerWriteHandler(mergeStrategy) // 注册合并策略
.sheet("员工表")
.doWrite(dataList);
System.out.println("文件生成成功:" + filePath);
}
}
实现说明与扩展
核心逻辑解析
- 缓存机制:通过
lastRowDataMap记录上一行的数据,避免重复读取; - 合并时机:当当前值与上一行不同,或到达最后一行时,触发合并操作;
- 字段过滤:仅对
mergeFieldNames中指定的字段执行合并,不影响其他列。
扩展场景
多列合并
只需在 mergeFieldNames 中添加多个字段名即可,例如同时合并 “部门” 和 “分公司”:
List<String> mergeFields = Arrays.asList("department", "company");
跨列合并
修改 CellRangeAddress 的列索引范围,例如合并 “部门” 和 “职位” 列(需确保两行数据完全相同):
// 在 mergeIfNeeded 方法中调整列范围
CellRangeAddress mergeRegion = new CellRangeAddress(
startRowIndex, endRowIndex,
1, 2 // 合并第 1 列(部门)到第 2 列(职位)
);
条件合并
在 merge 方法中添加自定义条件,例如仅合并值为 “技术部” 的单元格:
// 在对比值时添加条件
if (!Objects.equals(currentValue, lastRowData.value) || "技术部".equals(currentValue)) {
// 执行合并...
}
注意事项
- 表头行处理:
relativeRowIndex从 0 开始,其中 0 代表表头行,需跳过(避免合并表头); - 性能优化:合并操作会触发 Excel 内部重绘,大量合并可能影响性能,建议仅对必要字段合并;
- 合并冲突:同一区域多次合并会导致异常,需确保合并区域不重叠;
- POI 版本兼容:
Sheet.addMergedRegion是 POI 的方法,需与 EasyExcel 依赖的 POI 版本匹配(推荐使用 EasyExcel 3.x 配套的 POI 5.x)。