EasyExcel自定义注解样式被覆盖问题排查

问题描述在 EasyExcel 讨论区有

地址:https://www.yuque.com/easyexcel/topics/80

解决思路

因为不了解 EasyExcel 内部执行逻辑,所以思路就是先扒下源码看看业务逻辑

EasyExcel 执行逻辑分析

对于我们使用者来说,我们只写了这几行代码

1
2
3
4
5
6
7
8
9
10
11
12
@LogT
@PostMapping(value = "/export")
@SneakyThrows
public void export(@RequestBody UserDTO user, HttpServletResponse response){
List<UserVO> userVOList = userService.selectList(user);
ExcelUtil.prepareExport(response, "用户列表");
EasyExcel.write(response.getOutputStream(), UserVO.class)
.registerWriteHandler(ExcelUtil.defaultCellStyle())
.registerWriteHandler(ExcelUtil.defaultWidthStyle())
.sheet("用户列表")
.doWrite(userVOList);
}

但是 EasyExcel 却能帮我们做到新建文件、写入文件流、关闭流等一系列操作,一行行看下

write()

1
2
3
4
5
6
7
8
public static ExcelWriterBuilder write(OutputStream outputStream, Class head) {
ExcelWriterBuilder excelWriterBuilder = new ExcelWriterBuilder();
excelWriterBuilder.file(outputStream);
if (head != null) {
excelWriterBuilder.head(head);
}
return excelWriterBuilder;
}

获取到了一个构造器,大概知道它新建了文件返回了 Builder 对象,所以后面可以使用建造器语法

registerWriteHandler()

注册拦截器,这是 EasyExcel 暴露出来给用户自定义处理逻辑的接口,拦截器顾名思义就是在指定的地方执行我们定义的逻辑,在 EasyExcel 中,留有很多拦截器接口供我们实现,这里只说写入操作
WriteHandler.svg

写入操作的顶层接口是WriteHandler,下面有四个维度的拦截器接口,分别是单元格、行、sheet 页和工作簿,这个很好理解,我所操作的HorizontalCellStyleStrategy是来自于AbstractCellStyleStrategy,而抽象类分别实现了CellWriteHandlerWorkbookWriteHandlerNotRepeatExecutor

归根揭底还是拦截器,看下它做了什么

1
2
3
4
5
6
7
public T registerWriteHandler(WriteHandler writeHandler) {
if (parameter().getCustomWriteHandlerList() == null) {
parameter().setCustomWriteHandlerList(new ArrayList<WriteHandler>());
}
parameter().getCustomWriteHandlerList().add(writeHandler);
return self();
}

点进去可以发现,他将这个拦截器对象放入了WriteBasicParameter维护customWriteHandlerList这个集合中来

sheet()

这个链路很长

  1. ExcelWriterSheetBuilder 的 sheet 方法
  2. sheet 方法中的 build 方法
  3. 一直往下到WriteContextImpl的构造方法中的 initCurrentWorkbookHolder 方法
  4. WriteWorkbookHolder的构造方法
  5. 调用父类的super(writeWorkbook, null, writeWorkbook.getConvertAllFiled());
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
  public AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWriteHolder parentAbstractWriteHolder,
Boolean convertAllFiled) {
//省略...
// Initialization property
this.excelWriteHeadProperty = new ExcelWriteHeadProperty(this, getClazz(), getHead(), convertAllFiled);

// Compatible with old code
compatibleOldCode(writeBasicParameter);

// 新建了拦截器集合
List<WriteHandler> handlerList = new ArrayList<WriteHandler>();

// 将注解的属性解析分别形成不通的拦截器,放入集合
initAnnotationConfig(handlerList, writeBasicParameter);

// 将我们自定义的拦截器也放入集合中来
if (writeBasicParameter.getCustomWriteHandlerList() != null
&& !writeBasicParameter.getCustomWriteHandlerList().isEmpty()) {
handlerList.addAll(writeBasicParameter.getCustomWriteHandlerList());
}

//拦截器处理
// 1. 排序 2.剔除重复拦截器 3.分别将各个拦截器转换为不同级别的拦截器
this.ownWriteHandlerMap = sortAndClearUpHandler(handlerList);

Map<Class<? extends WriteHandler>, List<WriteHandler>> parentWriteHandlerMap = null;
if (parentAbstractWriteHolder != null) {
parentWriteHandlerMap = parentAbstractWriteHolder.getWriteHandlerMap();
} else {
handlerList.addAll(DefaultWriteHandlerLoader.loadDefaultHandler(useDefaultStyle));
}
this.writeHandlerMap = sortAndClearUpAllHandler(handlerList, parentWriteHandlerMap);

}

正如我上面所述,它做了一系列的准备工作,最终获取到的是类似不同级别的拦截器集合,类似为行拦截器四个,单元格拦截器 5 个之类的,这些东西都准备好了,放入 context 对象中来,也就是WriteContext,在实际写入的时候会用到

里面有两个方法比较重要

  1. initAnnotationConfig()
  2. sortAndClearUpHandler()
    和这个问题关系较大的是第二种
    1. 其中如果实现了 NotRepeatExecutor 接口,那么同一个唯一值指挥保留一个拦截器
    2. 如果实现了 Order 接口,那么会按照优先级排序,而根据注解生成的以及没有实现 Order 接口的默认都是 INTERGER 最小值,也就是说,不论怎么样,默认的处理器逻辑一定先执行

write()

同样点进入,直接定位到 addContent()这个方法中的excelWriteAddExecutor.add(data)这个方法代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void add(List data) {
if (CollectionUtils.isEmpty(data)) {
data = new ArrayList();
}
WriteSheetHolder writeSheetHolder = writeContext.writeSheetHolder();
int newRowIndex = writeSheetHolder.getNewRowIndexAndStartDoWrite();
if (writeSheetHolder.isNew() && !writeSheetHolder.getExcelWriteHeadProperty().hasHead()) {
newRowIndex += writeContext.currentWriteHolder().relativeHeadRowIndex();
}
// BeanMap is out of order,so use sortedAllFiledMap
Map<Integer, Field> sortedAllFiledMap = new TreeMap<Integer, Field>();
int relativeRowIndex = 0;
for (Object oneRowData : data) {
int n = relativeRowIndex + newRowIndex;
addOneRowOfDataToExcel(oneRowData, n, relativeRowIndex, sortedAllFiledMap);
relativeRowIndex++;
}
}

起始在观察源码的时候,就发现有一个WriteHandlerUtils这个类特别显眼,他和我们的拦截器实现很像,在 EasyExcel 中就是用来对原生拦截器的匿名实现做执行用的

直接定位到addOneRowOfDataToExcel()这个方法,他是对行的处理,我要的单元格样式,应该在单元格处理那块,而且我 DEBUG 显示,我注解@ContentStyle 只创建了工作簿拦截器和单元格拦截器,至于为什么是这两个就不说了 ,看看源码就会明白,所以这里继续看单元解析逻辑,addBasicTypeToExcel()方法

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
private void addBasicTypeToExcel(List<Object> oneRowData, Row row, int relativeRowIndex) {
if (CollectionUtils.isEmpty(oneRowData)) {
return;
}
Map<Integer, Head> headMap = writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadMap();
int dataIndex = 0;
int cellIndex = 0;
for (Map.Entry<Integer, Head> entry : headMap.entrySet()) {
if (dataIndex >= oneRowData.size()) {
return;
}
cellIndex = entry.getKey();
Head head = entry.getValue();
doAddBasicTypeToExcel(oneRowData, head, row, relativeRowIndex, dataIndex++, cellIndex);
}
// Finish
if (dataIndex >= oneRowData.size()) {
return;
}
if (cellIndex != 0) {
cellIndex++;
}
int size = oneRowData.size() - dataIndex;
for (int i = 0; i < size; i++) {
doAddBasicTypeToExcel(oneRowData, null, row, relativeRowIndex, dataIndex++, cellIndex++);
}
}

进入doAddBasicTypeToExcel()方法

1
2
3
4
5
6
7
8
9
10
private void doAddBasicTypeToExcel(List<Object> oneRowData, Head head, Row row, int relativeRowIndex, int dataIndex,
int cellIndex) {
WriteHandlerUtils.beforeCellCreate(writeContext, row, head, cellIndex, relativeRowIndex, Boolean.FALSE);
Cell cell = WorkBookUtil.createCell(row, cellIndex);
WriteHandlerUtils.afterCellCreate(writeContext, cell, head, relativeRowIndex, Boolean.FALSE);
Object value = oneRowData.get(dataIndex);
CellData cellData = converterAndSet(writeContext.currentWriteHolder(), value == null ? null : value.getClass(),
cell, value, null, head, relativeRowIndex);
WriteHandlerUtils.afterCellDispose(writeContext, cellData, cell, head, relativeRowIndex, Boolean.FALSE);
}

在 WriteHandlerUtils.afterCellDispose 执行了对于注解单元格渲染

回到问题

OK,我们为什么碰到那个问题,是因为基于AbstractCellStyleStrategy而实现的样式都实现了 NotRepeatExecutor 接口,所以当唯一值一样(我没设那就是默认值,CellStyleStrategy),所以会被默认的样式给覆盖掉

理解

那现在如何解决这个问题

我试着

  1. 覆写了 NotRepeatExecutor 的 uniqueValue 方法,设置别的值
  2. 实现了 Order 接口设置为 Integer 最小值

结果我的样式覆盖掉了注解的样式,虽然美观了一点,但是还是无法达到我的需求,最完美的情况是,通过自定义样式策略做到全局的样式统一,然后局部的样式调整,通过样式注解来实现

Debug 代码发现造成这样的原因是我们的拦截器优先级低于自定义样式的匿名实现类拦截器,所以解决的思路是调整拦截器的顺序,当二者都是 Order 最小值时,那个后加入那个就是后执行

分析了代码发现自定义注解生成的匿名内部类都会先执行,

但是无意之间将 sheet()方法和 registerHandler()方法调换却达到了我的效果,继续去源码看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
      List<WriteHandler> handlerList = new ArrayList<WriteHandler>();

// Initialization Annotation
initAnnotationConfig(handlerList, writeBasicParameter);

if (writeBasicParameter.getCustomWriteHandlerList() != null
&& !writeBasicParameter.getCustomWriteHandlerList().isEmpty()) {
handlerList.addAll(writeBasicParameter.getCustomWriteHandlerList());
}

this.ownWriteHandlerMap = sortAndClearUpHandler(handlerList);

Map<Class<? extends WriteHandler>, List<WriteHandler>> parentWriteHandlerMap = null;
if (parentAbstractWriteHolder != null) {
parentWriteHandlerMap = parentAbstractWriteHolder.getWriteHandlerMap();
} else {
handlerList.addAll(DefaultWriteHandlerLoader.loadDefaultHandler(useDefaultStyle));
}
//重新处理拦截器链
this.writeHandlerMap = sortAndClearUpAllHandler(handlerList, parentWriteHandlerMap);

定位到sortAndClearUpAllHandler方法,是我之前没看仔细,对于两种处理的拦截器,在这个方法种会进行一次,重新处理,进去看看

1
2
3
4
5
6
7
8
9
10
11
protected Map<Class<? extends WriteHandler>, List<WriteHandler>> sortAndClearUpAllHandler(
List<WriteHandler> handlerList, Map<Class<? extends WriteHandler>, List<WriteHandler>> parentHandlerMap) {
// add
if (parentHandlerMap != null) {
List<WriteHandler> parentWriteHandler = parentHandlerMap.get(WriteHandler.class);
if (!CollectionUtils.isEmpty(parentWriteHandler)) {
handlerList.addAll(parentWriteHandler);
}
}
return sortAndClearUpHandler(handlerList);
}

可以看到它重新从 parentHandlerMap 取出来了拦截器,然后然放到了当前拦截器集合中了,这么一取一放,就是将之前的拦截器追加到了新的集合中来,重置了它的顺序,所以达到了我之前的效果
image-20210301162330715.png
把顺序调整回去重新 Debug,发现在前一步就会将三个视为同一级别来做处理,同取同放,所以顺序不变

image-20210301161944567.png
OK,解决办法找到了

  1. 自定义样式策略需要覆写 NotRepeatExecutor 的 uniqueValue 方法,设置别的值
  2. 实现了 Order 接口设置为 Integer 最小值
  3. 调用导出代码时,将 sheet 方法写在 registerWriteHandler 方法前面

问题解决!

作者

孙博文

发布于

2021-03-01

更新于

2021-07-18

许可协议

评论