easyExcel自定义合并单元格
easyExcel本身支持简单的单元格合并,一共有两种合并策略:
- 一次性的合并策略(OnceAbsoluteMergeStrategy)
- 循环的合并策略(LoopMergeStrategy)
/**
* 合并单元格
* <p>
* 1. 创建excel对应的实体对象 参照{@link DemoData} {@link DemoMergeData}
* <p>
* 2. 创建一个merge策略 并注册
* <p>
* 3. 直接写即可
*
* @since 2.2.0-beta1
*/
@Test
public void mergeWrite() {
// 方法1 注解
String fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
// 在DemoStyleData里面加上ContentLoopMerge注解
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoMergeData.class).sheet("模板").doWrite(data());
// 方法2 自定义合并单元格策略
fileName = TestFileUtil.getPath() + "mergeWrite" + System.currentTimeMillis() + ".xlsx";
// 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
}
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
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
但是往往实际应用中,我们并不能立马指定合并的单元格,就比如我这次需要根据订单id相同的合并单元格,但是数据还没查询出来时,我是不知道哪些要合并的,而我只知道两个要求:
- 订单id相同的单元格需要合并,
- 前七列的单元格需要合并,后面的不需要。
好了既然,easyExcel原有的不能满足我的要求,那我们可以自己写一个嘛。
先引入excel依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.2.1</version>
</dependency>
1
2
3
4
5
6
2
3
4
5
6
首先我们可以发现EasyExcel有一个抽象类AbstractMergeStrategy,这不是摆明要我们去实现它嘛,就它了!
/**
* Merge strategy
*
* @author Jiaju Zhuang
*/
public abstract class AbstractMergeStrategy implements CellWriteHandler {
/**
*单元格创建之前
*/
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
}
/**
*单元格创建之后
*/
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
Head head, Integer relativeRowIndex, Boolean isHead) {}
/**
*单元数据转换后
*/
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder,
WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex,
Boolean isHead) {
}
/**
*单元处理后
*/
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (isHead) {
return;
}
merge(writeSheetHolder.getSheet(), cell, head, relativeRowIndex);
}
/**
* merge 合并方法
*
* @param sheet
* @param cell
* @param head
* @param relativeRowIndex
*/
protected abstract void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex);
}
// 上面的方法作用我也是看方法名猜测的,具体作用稍后再试
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
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
在写之前,我看了下作者写的两个合并方法,看看他是怎么写合并的
if (isHead) {
return;
}
if (relativeRowIndex % eachRow == 0) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum(), row.getRowNum() + eachRow - 1,
columnIndex, columnIndex + columnExtend - 1);
writeSheetHolder.getSheet().addMergedRegionUnsafe(cellRangeAddress);
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
关键看CellRangeAddress
单元格范围地址对象,他的构造方法允许传入四个参数,分别对应要合并的单元格的四个角的点的坐标(单元格的首行数,单元格的尾行数,单元格的首列数,单元格的尾列数),意思就是确定要合并单元格的地址,交给writeSheetHolder
去合并。
现在知道如何合并单元格了,接下来就是计算需要合并的单元格的位置了
- 将查询出来的的列表数据,存入list集合,循环遍历,计算订单id相同的首行数和下行的行数,这样我们就知道哪几行需要合并了
- 再利用自定义注解,标明需要合并的列,计算单元格的首列数,尾列数
自定义合并策略
package com.lcxx.lucky.store.common.easyexcel;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.lcxx.lucky.store.common.exception.impl.CommonException;
import com.lcxx.lucky.store.common.exception.impl.CommonExceptionEnum;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* @title: MyMergeStrategy
* @Author ggball
* @Date: 2021/9/27 21:09
* @Version 1.0
*/
@Slf4j
public class MyMergeStrategy<T> extends AbstractMergeStrategy {
private int firstRowIndex;
private int lastRowIndex;
private int firstColumnIndex;
private int lastColumnIndex;
// 主键值集合
private List<String> primaryIdList = new ArrayList<>();
// 需要合并的列index
Set<Integer> colIndexSet = new HashSet<>();
public MyMergeStrategy(int firstRowIndex, int lastRowIndex, int firstColumnIndex, int lastColumnIndex) {
if (firstRowIndex >= 0 && lastRowIndex >= 0 && firstColumnIndex >= 0 && lastColumnIndex >= 0) {
this.firstRowIndex = firstRowIndex;
this.lastRowIndex = lastRowIndex;
this.firstColumnIndex = firstColumnIndex;
this.lastColumnIndex = lastColumnIndex;
} else {
throw new IllegalArgumentException("All parameters must be greater than 0");
}
}
/**
*从数据list
*/
public MyMergeStrategy(List<T> data) throws CommonException, IllegalAccessException {
if (data.size() == 0){
throw new CommonException(CommonExceptionEnum.HAS_NO_DATA);
}
for (T row : data) {
Class<?> type = row.getClass();
Field[] fields = getAllDeclaredFields(type);
for (Field field : fields) {
ExcelKey key = field.getDeclaredAnnotation(ExcelKey.class);
if (null != key && field.getName().equals(key.value()) ) {
field.setAccessible(true);
Object filedValue = field.get(row);
field.setAccessible(false);
if (null != filedValue) {
// 添加合并主键值
primaryIdList.add(String.valueOf(filedValue));
}
break;
}
}
}
// 获取需要合并的列
T row = data.get(0);
Class<?> aClass = row.getClass();
Field[] fields = getAllDeclaredFields(aClass);
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
ExcelMerge merge = field.getDeclaredAnnotation(ExcelMerge.class);
if (null != merge && merge.isMerge()) {
colIndexSet.add(i);
}
}
}
/**
* 获取合并的首行坐标和下行行数
* @return map<合并的首行坐标,下行行数>
*/
public Map<Integer,Integer> getMergeRowMap() {
Map<Integer, Integer> mergeRowMap = new HashMap<>();
if (CollectionUtil.isEmpty(primaryIdList)) {
return mergeRowMap;
}
// 主键索引
int idIndex = 0;
// 主键临时值
String tempValue = null;
// 如果主键都相同
HashSet<String> idSet = new HashSet<>(primaryIdList);
if (idSet.size() == 1) {
mergeRowMap.put(0,primaryIdList.size()-1);
return mergeRowMap;
}
// 主键不相同
for (int i = 0; i < primaryIdList.size(); i++) {
if (null == tempValue) {
tempValue = primaryIdList.get(i);
}
String id = primaryIdList.get(i);
if (!id.equals(tempValue)) {
mergeRowMap.put(idIndex,(i-1)-idIndex);
idIndex = i;
tempValue = null;
}
if (primaryIdList.size()-1 == i) {
mergeRowMap.put(idIndex,i-idIndex);
}
}
return mergeRowMap;
}
/**
*获取类的属性和方法
*/
public static Field[] getAllDeclaredFields(Class<?> clazz) {
Class<?> superclass;
List<Field> fieldList = new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()));
while ((superclass = clazz.getSuperclass()) != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(superclass.getDeclaredFields())));
clazz = superclass;
}
Field[] res = new Field[fieldList.size()];
res = fieldList.toArray(res);
return res;
}
/**
*合并单元格方法
*/
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
Map<Integer, Integer> mergeRowMap = getMergeRowMap();
// 行坐标
int rowIndex = cell.getRowIndex();
// 列坐标
int columnIndex = cell.getColumnIndex();
// 下行行数
Integer downRows = mergeRowMap.get(rowIndex-head.getHeadNameList().size());
// 和并列坐标
boolean mergeContains = colIndexSet.contains(columnIndex);
if (null != downRows && mergeContains) {
// 创建单元格范围地址
CellRangeAddress cellRangeAddress = new CellRangeAddress(rowIndex, rowIndex+downRows, columnIndex, columnIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 如果字符类型的单元格值为空字符串,那就设置成“/”
if (cellData.getType().equals(CellDataTypeEnum.STRING) && StringUtils.isBlank(cellData.getStringValue())) {
cellData.setStringValue("/");
}
// 如果数字类型的单元格值为空,那就设置成“/”
if (cellData.getType().equals(CellDataTypeEnum.NUMBER) && null == cellData.getNumberValue()) {
cellData.setStringValue("/");
}
super.afterCellDataConverted(writeSheetHolder, writeTableHolder, cellData, cell, head, relativeRowIndex, isHead);
}
}
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
上面计算订单id相同的首行数和下行的行数,如果大家有不一样的方法,欢迎大家提出想法
合并主键注解
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelKey {
String value() default "";
}
1
2
3
4
5
2
3
4
5
合并列注解
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelMerge {
boolean isMerge() default true;
}
1
2
3
4
5
2
3
4
5
excel导出VO
@Data
public class AfterSaleExcelListVo implements Serializable {
/**
* 售后订单编号
*/
@ExcelProperty(value = {"销售售后单/铺货售后单导出","售后订单编号"})
@ExcelKey("saleBillId")
@ExcelMerge
private String saleBillId;
/**
* 售后类型 1=退货退款,2=仅退款,3=补寄
*/
@ExcelProperty(value = {"销售售后单/铺货售后单导出","售后类型"},converter = SaleTypeConverter.class)
@ExcelMerge
private Integer saleType;
****
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
easyExcel添加策略
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), clazz)
.sheet("sheet1")
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())//设置导出单元格宽度自适应
.registerWriteHandler(new OnceMergeStrategy(data)) // 设置合并策略
.registerWriteHandler(setSheetStyle())
.doWrite(data);
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
效果图
总结:活不多,要勤于思考!
上次更新: 2023/03/08, 14:19:30