0%

EasyExcel合并单元格

EasyExcel合并单元格:基于 AbstractMergeStrategy 的灵活实现

在生成 Excel 报表时,合并单元格是优化表格可读性的常用手段(如合并相同值的相邻单元格)。EasyExcel 并未直接提供合并单元格的 API,但通过其钩子机制(尤其是 AbstractMergeStrategy 抽象类),可灵活实现自定义合并逻辑。本文将详细讲解如何基于 AbstractMergeStrategy 实现单元格合并,并通过实例演示 “相同值自动合并” 的功能。

合并单元格的核心思路

EasyExcel 合并单元格的核心是通过 单元格钩子(CellWriteHandler) 监听单元格创建过程,在数据写入时动态判断是否需要合并。具体步骤:

  1. 记录当前单元格的值及其所在行索引;
  2. 与上一行同列单元格的值对比,若相同则标记为待合并区域;
  3. 当值发生变化或到达最后一行时,执行合并操作(通过 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 {

// 需要合并的字段名(与实体类的 @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;
}

@Override
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 "";
}
}
}

实体类定义

假设需要合并 “部门” 列,实体类如下:

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) {
// 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 中添加多个字段名即可,例如同时合并 “部门” 和 “分公司”:

1
List<String> mergeFields = Arrays.asList("department", "company");  
跨列合并

修改 CellRangeAddress 的列索引范围,例如合并 “部门” 和 “职位” 列(需确保两行数据完全相同):

1
2
3
4
5
// 在 mergeIfNeeded 方法中调整列范围  
CellRangeAddress mergeRegion = new CellRangeAddress(
startRowIndex, endRowIndex,
1, 2 // 合并第 1 列(部门)到第 2 列(职位)
);
条件合并

merge 方法中添加自定义条件,例如仅合并值为 “技术部” 的单元格:

1
2
3
4
// 在对比值时添加条件  
if (!Objects.equals(currentValue, lastRowData.value) || "技术部".equals(currentValue)) {
// 执行合并...
}

注意事项

  1. 表头行处理relativeRowIndex 从 0 开始,其中 0 代表表头行,需跳过(避免合并表头);
  2. 性能优化:合并操作会触发 Excel 内部重绘,大量合并可能影响性能,建议仅对必要字段合并;
  3. 合并冲突:同一区域多次合并会导致异常,需确保合并区域不重叠;
  4. POI 版本兼容Sheet.addMergedRegion 是 POI 的方法,需与 EasyExcel 依赖的 POI 版本匹配(推荐使用 EasyExcel 3.x 配套的 POI 5.x)。

欢迎关注我的其它发布渠道