tags: [EasyExcel]


categories: [问题排查]


问题描述在EasyExcel讨论区有

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

解决思路

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

EasyExcel执行逻辑分析

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

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

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

write()

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

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

registerWriteHandler()

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

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

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

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

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

sheet()

这个链路很长

  1. ExcelWriterSheetBuilder的sheet方法
  2. sheet方法中的build方法
  3. 一直往下到WriteContextImpl的构造方法中的initCurrentWorkbookHolder方法
  4. WriteWorkbookHolder的构造方法
  5. 调用父类的super(writeWorkbook, null, writeWorkbook.getConvertAllFiled());
  1. public AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWriteHolder parentAbstractWriteHolder,
  2. Boolean convertAllFiled) {
  3. //省略...
  4. // Initialization property
  5. this.excelWriteHeadProperty = new ExcelWriteHeadProperty(this, getClazz(), getHead(), convertAllFiled);
  6. // Compatible with old code
  7. compatibleOldCode(writeBasicParameter);
  8. // 新建了拦截器集合
  9. List<WriteHandler> handlerList = new ArrayList<WriteHandler>();
  10. // 将注解的属性解析分别形成不通的拦截器,放入集合
  11. initAnnotationConfig(handlerList, writeBasicParameter);
  12. // 将我们自定义的拦截器也放入集合中来
  13. if (writeBasicParameter.getCustomWriteHandlerList() != null
  14. && !writeBasicParameter.getCustomWriteHandlerList().isEmpty()) {
  15. handlerList.addAll(writeBasicParameter.getCustomWriteHandlerList());
  16. }
  17. //拦截器处理
  18. // 1. 排序 2.剔除重复拦截器 3.分别将各个拦截器转换为不同级别的拦截器
  19. this.ownWriteHandlerMap = sortAndClearUpHandler(handlerList);
  20. Map<Class<? extends WriteHandler>, List<WriteHandler>> parentWriteHandlerMap = null;
  21. if (parentAbstractWriteHolder != null) {
  22. parentWriteHandlerMap = parentAbstractWriteHolder.getWriteHandlerMap();
  23. } else {
  24. handlerList.addAll(DefaultWriteHandlerLoader.loadDefaultHandler(useDefaultStyle));
  25. }
  26. this.writeHandlerMap = sortAndClearUpAllHandler(handlerList, parentWriteHandlerMap);
  27. }

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

里面有两个方法比较重要

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

write()

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

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

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

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

  1. private void addBasicTypeToExcel(List<Object> oneRowData, Row row, int relativeRowIndex) {
  2. if (CollectionUtils.isEmpty(oneRowData)) {
  3. return;
  4. }
  5. Map<Integer, Head> headMap = writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadMap();
  6. int dataIndex = 0;
  7. int cellIndex = 0;
  8. for (Map.Entry<Integer, Head> entry : headMap.entrySet()) {
  9. if (dataIndex >= oneRowData.size()) {
  10. return;
  11. }
  12. cellIndex = entry.getKey();
  13. Head head = entry.getValue();
  14. doAddBasicTypeToExcel(oneRowData, head, row, relativeRowIndex, dataIndex++, cellIndex);
  15. }
  16. // Finish
  17. if (dataIndex >= oneRowData.size()) {
  18. return;
  19. }
  20. if (cellIndex != 0) {
  21. cellIndex++;
  22. }
  23. int size = oneRowData.size() - dataIndex;
  24. for (int i = 0; i < size; i++) {
  25. doAddBasicTypeToExcel(oneRowData, null, row, relativeRowIndex, dataIndex++, cellIndex++);
  26. }
  27. }

进入doAddBasicTypeToExcel()方法

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

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

回到问题

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

理解

那现在如何解决这个问题

我试着

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

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

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

分析了代码发现自定义注解生成的匿名内部类都会先执行,EasyExcel自定义注解样式被覆盖问题排查 - 图2

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

  1. List<WriteHandler> handlerList = new ArrayList<WriteHandler>();
  2. // Initialization Annotation
  3. initAnnotationConfig(handlerList, writeBasicParameter);
  4. if (writeBasicParameter.getCustomWriteHandlerList() != null
  5. && !writeBasicParameter.getCustomWriteHandlerList().isEmpty()) {
  6. handlerList.addAll(writeBasicParameter.getCustomWriteHandlerList());
  7. }
  8. this.ownWriteHandlerMap = sortAndClearUpHandler(handlerList);
  9. Map<Class<? extends WriteHandler>, List<WriteHandler>> parentWriteHandlerMap = null;
  10. if (parentAbstractWriteHolder != null) {
  11. parentWriteHandlerMap = parentAbstractWriteHolder.getWriteHandlerMap();
  12. } else {
  13. handlerList.addAll(DefaultWriteHandlerLoader.loadDefaultHandler(useDefaultStyle));
  14. }
  15. //重新处理拦截器链
  16. this.writeHandlerMap = sortAndClearUpAllHandler(handlerList, parentWriteHandlerMap);

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

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

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

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

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

问题解决!