EasyExcel - 行合并策略(二级列表)

😼前言 :博主在工作中又遇到了新的excel导出挑战:需要导出多条文章及其下联合作者的信息,简单的来说是一个二级列表 的数据结构。

🕵️‍♂️思路:excel导出实际上是一行一行的记录,再根据条件对其进行合并。

目录

最终效果图📌

一、数据格式及处理📚

首先,需要先将一条文章 按联合作者数量,拆分为指定数量的文章及作者导出记录的集合。

以文章《牧区歌与马》为例,一篇文章有三名联合作者,生成三条导出记录。

1.文章对象(处理前)

javascript 复制代码
// 文章记录对象 Acticle.class
[{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
	"authorList": [{
			"userName": "小A ",
			"orgName": "单位A"
		},
		{
			"userName": "小B ",
			"orgName": "/"
		},
		{
			"userName": "小C",
			"orgName": "单位C"
		}
	]
}]

2.文章及作者对象(处理后)

可以看到记录由一条变为三条,除了作者名称和单位,其余字段内容均一致。

javascript 复制代码
// 文章处理后记录对象 ActicleAuthor.class
[{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
    "author":"小A",
    "authorUnit":"单位A"
},{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
    "author":"小B",
    "authorUnit":"/"
},{
	"contentId": "1",
	"contentTitle": "牧区歌与马",
	"contentCount": 940,
	"releaseTime": "2025-01-09 11:21:16",
	"readNum": 1,
	"auditor": "小李",
	"orgName": "办公室",
    "author":"小C",
    "authorUnit":"单位C"
}]

3.未合并的效果图

未设置行合并策略直接导出时,表格的格式内容如下:

👆图中的E、F列示例错误,应分别为6行记录

二、通用行合并策略🔍

此处学习了csdn博主xiao谢同学 分享的通用行合并策略源码

链接:EasyExcel 通用行合并策略实现

1.源码学习

🐱‍👓该策略以列的行数作为主键,每次遍历记录列的最新合并区域信息。将同列且相邻行 的单元格内容进行比较:

(1)一致 :则仅更新endRow和endCell,继续遍历;

(2)不一致:则将已有区域进行合并,再将MergeRange所有字段进行更新。

MergeRange.class

  • startRow :合并开始行
  • endRow:合并结束行
  • startCell:合并开始单元格
  • endCell:合并结束单元格
  • lastValue:列最新单元格内容
javascript 复制代码
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.*;


public class MergeStrategy extends AbstractMergeStrategy {

    // 合并的列编号,从0开始,指定的index或自己按字段顺序数
    private Set<Integer> mergeCellIndex = new HashSet<>();

    // 数据集大小,用于区别结束行位置
    private Integer maxRow = 0;

    // 禁止无参声明
    private MergeStrategy() {
    }

    public MergeStrategy(Integer maxRow, int... mergeCellIndex) {
        Arrays.stream(mergeCellIndex).forEach(item -> {
            this.mergeCellIndex.add(item);
        });
        this.maxRow = maxRow;
    }

    // 记录上一次合并的信息
    private Map<Integer, MergeRange> lastRow = new HashedMap();

    // 每行每列都会进入,绝对不要在这写循环
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        int currentCellIndex = cell.getColumnIndex();
        // 判断该列是否需要合并
        if (mergeCellIndex.contains(currentCellIndex)) {
            String currentCellValue = cell.getStringCellValue();
            int currentRowIndex = cell.getRowIndex();
            if (!lastRow.containsKey(currentCellIndex)) {
                // 记录首行起始位置
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
                return;
            }
            //有上行这列的值了,拿来对比.
            MergeRange mergeRange = lastRow.get(currentCellIndex);
            if (!(mergeRange.lastValue != null && mergeRange.lastValue.equals(currentCellValue))) {
                // 结束的位置触发下合并.
                // 同行同列不能合并,会抛异常
                if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));
                }
                // 更新当前列起始位置
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex));
            }
            // 合并行 + 1
            mergeRange.endRow += 1;
            // 结束的位置触发下最后一次没完成的合并
            if (relativeRowIndex.equals(maxRow - 1)) {
                MergeRange lastMergeRange = lastRow.get(currentCellIndex);
                // 同行同列不能合并,会抛异常
                if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));
                }
            }
        }
    }
}

class MergeRange {
    public int startRow;
    public int endRow;
    public int startCell;
    public int endCell;
    public String lastValue;

    public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell) {
        this.startRow = startRow;
        this.endRow = endRow;
        this.startCell = startCell;
        this.endCell = endCell;
        this.lastValue = lastValue;
    }
}

2.通用行合并后的效果图

可以发现,这种仅根据相邻行单元格内容进行合并的方式,还未能完全满足博主想要二级列表的效果。

👆黄色代表非理想合并的区域

三、二级通用行合并策略✍

🐱‍💻改造思路:列A是文章标题,以列A的内容作为第一层级的标识(tip:不如contentId准确)。即使相邻行单元格内容相等,对应行的A列内容不相等也不能合并。

处理步骤:

(1)在合并区域对象类MergeRange中,增加A列内容的值字段lastValueRowa

(2)遍历单元格构造合并区域对象时,记录A列内容值 以此来作为附加的合并条件。

(3)当同一列字段相邻行内容相等且A列内容值相等时,再进行合并。

1.源码改造

javascript 复制代码
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.map.HashedMap;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.*;

/**
 * 行合并策略
 */
@Slf4j
public class MergeStrategy extends AbstractMergeStrategy {

    // 合并的列编号,从0开始,指定的index或自己按字段顺序数
    private Set<Integer> mergeCellIndex = new HashSet<>();

    // 数据集大小,用于区别结束行位置
    private Integer maxRow = 0;

    // 禁止无参声明
    private MergeStrategy() {
    }

    public MergeStrategy(Integer maxRow, int... mergeCellIndex) {
        Arrays.stream(mergeCellIndex).forEach(item -> {
            this.mergeCellIndex.add(item);
        });
        this.maxRow = maxRow;
    }

    // 记录上一次合并的信息
    private Map<Integer, MergeRange> lastRow = new HashedMap();

    // 每行每列都会进入,绝对不要在这写循环
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        // 获取单元格当前列
        int currentCellIndex = cell.getColumnIndex();

        log.info("遍历单元格:{}行,{}列 >>>>>>>>>>>>>>>>>", cell.getRowIndex(), currentCellIndex);
        // 判断该列是否需要合并
        if (mergeCellIndex.contains(currentCellIndex)) {
            // 获取当前单元格内容值
            String currentCellValue = new DataFormatter().formatCellValue(cell);
            // 获取当前单元格的行row
            int currentRowIndex = cell.getRowIndex();
            // 获取当前行A列单元格内容 new
            String currentValueRowa = "";
            // 如果最后合并行的map,不包括当前列
            if (!lastRow.containsKey(currentCellIndex)) {
                log.info("lastRow添加第{}列【前】,当前lastRow={}", currentCellIndex, JSONUtil.toJsonStr(lastRow));
                // 获取当前行的列A内容 new
                if (currentCellIndex == 0) {
                    currentValueRowa = currentCellValue;
                } else {
                    currentValueRowa = getRowaValue();
                }

                // 记录首行起始位置? 记录当前列及合并范围
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));
                log.info("lastRow添加第{}列【后】,当前行列A内容={},新lastRow={}", currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));
                return;
            } else {
            	// 该列已存在lastRow中,则取最新的列A内容值 new
                currentValueRowa = getRowaValue();
            }

            //有上行这列的值了,拿来对比.
            MergeRange mergeRange = lastRow.get(currentCellIndex);
            // 判断条件:增加A列内容判断 new
            log.info("合并比对1>>>>>>>>>:第{}列最新内容lastValue = {},当前内容currentCellValue={},"
                    , currentCellIndex, mergeRange.lastValue, currentCellValue);
            log.info("合并比对2>>>>>>>>>第{}列最新列A内容lastValueRowa={}, 当前列A内容currentValueRowa={}"
                    , currentCellIndex, mergeRange.lastValueRowa, currentValueRowa);
            if (!(mergeRange.lastValue != null
                    && mergeRange.lastValue.equals(currentCellValue) && mergeRange.lastValueRowa.equals(currentValueRowa))) {
                // 结束的位置触发下合并.
                // 同行同列不能合并,会抛异常
                if (mergeRange.startRow != mergeRange.endRow || mergeRange.startCell != mergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(mergeRange.startRow, mergeRange.endRow, mergeRange.startCell, mergeRange.endCell));
                }
                // 更新当前列起始位置
                lastRow.put(currentCellIndex, new MergeRange(currentCellValue, currentRowIndex, currentRowIndex, currentCellIndex, currentCellIndex, currentValueRowa));
                log.info("比对不一致,确认合并!!>>>>>>>>>:第{}列最新列A内容={},最新lastRow = {}"
                        , currentCellIndex, currentValueRowa, JSONUtil.toJsonStr(lastRow));
            }
            // 合并行 + 1
            mergeRange.endRow += 1;
            // 结束的位置触发下最后一次没完成的合并
            if (relativeRowIndex.equals(maxRow - 1)) {
                MergeRange lastMergeRange = lastRow.get(currentCellIndex);
                // 同行同列不能合并,会抛异常
                if (lastMergeRange.startRow != lastMergeRange.endRow || lastMergeRange.startCell != lastMergeRange.endCell) {
                    sheet.addMergedRegionUnsafe(new CellRangeAddress(lastMergeRange.startRow, lastMergeRange.endRow, lastMergeRange.startCell, lastMergeRange.endCell));
                }
            }
        }
    }

	/**
	* 获取列A最新一行的内容值 new
	*/
    private String getRowaValue() {
        // 获取当前行A列单元格内容
        String currentValueRowa = "";
        if (lastRow.get(0) != null) {
            currentValueRowa = lastRow.get(0).lastValue;
        }
        return currentValueRowa;
    }
}

class MergeRange {
    public int startRow;
    public int endRow;
    public int startCell;
    public int endCell;
    public String lastValue;
    // 最后一个合并值得A列值 new
    public String lastValueRowa;

    public MergeRange(String lastValue, int startRow, int endRow, int startCell, int endCell, String lastValueRowa) {
        this.startRow = startRow;
        this.endRow = endRow;
        this.startCell = startCell;
        this.endCell = endCell;
        this.lastValue = lastValue;
        this.lastValueRowa = lastValueRowa;
    }
}

2.设置excel输出策略

javascript 复制代码
  // 设置excel输出策略
 EasyExcel.write(fileName, ActicleAuthor.class)
 			// 0,1 表示 对1,2列启用合并策略
          .registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) 
          .sheet(0)
          .doWrite(dataList);

3.延伸

🐱‍🚀如果需要做三级、四级等列表,可以将指定多个字段的拼接值当作列A来处理。可修改getRowaValue()方法实现逻辑。

四、问题☔

在开发的过程中,不可避免地碰到了一些问题......

1.问题描述

💁‍♀️在获取列A内容值时,曾尝试从Sheet对象中获取。

因为存在Sheet.getRow(0)获取第一行的row对象是null的问题,所以用从lastRow中获取列A内容的方法进行替代。

javascript 复制代码
/**
	* 从sheet.getRow(0)中获取列A内容值
	*/
    private String getRowaValue(Sheet sheet, int rowId) {
        // 获取当前行A列单元格内容
        String currentValueRowa = "";
        Row row = sheet.getRow(rowId);
        if (row != null) {
            Cell cell = row.getCell(0);
            if (cell != null) {
                currentValueRowa = cell.getStringCellValue();
            }
        }
        return currentValueRowa;
    }

2.问题原因

👩‍💻经过面向百度查询,从csdn博主吾乃南华老仙 分享的文章sheet.getRow(0)获取的row为null?中得知:

new SXSSFWorkbook(new XSSFWorkbook(inputStream)) 创建Workbook的时候,

SXSSFWorkbook对象内部会维护一个HashMap(反编译后的名称为_xFromSxHash)。
而当使用workBook.getSheetAt(0)的时候,其实是从_xFromSxHash中获取新创建的Sheet对象,从而导致sheet.getRow(0)获取的row为null。

😸文中提供的解决方法:

javascript 复制代码
将获取首行代码:
Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet = workBook.getSheetAt(0);
Row row = sheet.getRow(0);

修改为👇:

Workbook workBook = new SXSSFWorkbook(new XSSFWorkbook(inputStream));
Sheet sheet;
if (workBook instanceof SXSSFWorkbook) {
   SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) workBook;
   sheet = sxssfWorkbook.getXSSFWorkbook().getSheetAt(sheetIndex);
} else {
   sheet = workBook.getSheetAt(sheetIndex);
}
Row row = sheet.getRow(0);

😧而我们使用的导出是基于EasyExcel的,并没有单独的使用流去创建对象,应该怎么办呢?

👇可以看到debug过程中,显示sheet对象类型是SXSSFSheet。

3.解决办法

😾再次经过一番查询,在EasyExcel语雀文档 的QA:EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释中找到了解释:

  • inMemory(true):
    EasyExcel默认使用SXSSFWorkbook以减少内存消耗,但它不支持复杂的样式设置(如水印)。通过设置inMemory(true),我们改用XSSFWorkbook,它提供了更全面的样式支持。

在设置excel输出策略时加上inMemory(true)设置:

javascript 复制代码
  // 设置excel输出策略
 EasyExcel.write(fileName, ActicleAuthor.class)
 		 // 必须设置,以便使用XSSFWorkbook而非SXSSFWorkbook new
           .inMemory(true)
 		 // 0,1 表示 对1,2列启用合并策略
          .registerWriteHandler(new MergeStrategy(dataList.size(),0,1)) 
          .sheet(0)
          .doWrite(dataList);

🙆‍♀️执行debug后,发现可以获取到sheet.getRow(0)的第一行对象了,切换类型的问题成功解决。

参考文章📒

EasyExcel 通用行合并策略实现-xiao谢同学
sheet.getRow(0)获取的row为null?
EasyExcel 我想在导出excel文件的时候添加水印,要怎么做,请给出代码示例和解释

相关推荐
酒鬼猿1 小时前
C++进阶(四)--set和map的介绍与使用
开发语言·c++
我是單身你是狗1 小时前
token
开发语言·lua
Alvin's Tech Blog1 小时前
C++单例模式的设计
开发语言·c++·单例模式
??? Meggie3 小时前
【Python】使用python 对excel文件进行加密
开发语言·python·excel
飞的肖4 小时前
使用中间件自动化部署java应用
java·中间件·自动化
程序员沉梦听雨4 小时前
【IDEA】快捷键篇
java·ide·intellij-idea
Q_27437851096 小时前
django基于Python的智能停车管理系统
java·数据库·python·django
大风起兮127 小时前
ESP32,uart安装驱动uart_driver_install函数剖析,以及intr_alloc_flags 参数的意义
开发语言·单片机·嵌入式硬件
不是AI7 小时前
【C语言】【C++】Curl库的安装
c语言·开发语言·c++