当前位置: 首页 > article >正文

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

😼前言:博主在工作中又遇到了新的excel导出挑战:需要导出多条文章及其下联合作者的信息,简单的来说是一个二级列表的数据结构。
🕵️‍♂️思路:excel导出实际上是一行一行的记录,再根据条件对其进行合并。

目录

  • 最终效果图📌
  • 一、数据格式及处理📚
    • 1.文章对象(处理前)
    • 2.文章及作者对象(处理后)
    • 3.未合并的效果图
  • 二、通用行合并策略🔍
    • 1.源码学习
    • 2.通用行合并后的效果图
  • 三、二级通用行合并策略✍
    • 1.源码改造
    • 2.设置excel输出策略
    • 3.延伸
  • 四、问题☔
    • 1.问题描述
    • 2.问题原因
    • 3.解决办法
  • 参考文章📒

最终效果图📌

最终导出格式

一、数据格式及处理📚

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

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

1.文章对象(处理前)

// 文章记录对象 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.文章及作者对象(处理后)

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

// 文章处理后记录对象 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:列最新单元格内容
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.源码改造

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输出策略

  // 设置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内容的方法进行替代。

/**
	* 从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。

😸文中提供的解决方法:

将获取首行代码:
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)设置:

  // 设置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文件的时候添加水印,要怎么做,请给出代码示例和解释


http://www.kler.cn/a/504216.html

相关文章:

  • Grails应用http.server.requests指标数据采集问题排查及解决
  • 数据挖掘实训:天气数据分析与机器学习模型构建
  • Qt 自动根据编译的dll或exe 将相关dll文件复制到目标文件夹
  • SpringBoot-Day1
  • thinkphp 5.0 结合redis 做延迟队列,队列无法被消费
  • hutool糊涂工具通过注解设置excel宽度
  • Elasticsearch:向量数据库基础设施类别的兴衰
  • 易我视频照片修复EaseUS Fixo Technician
  • 简聊MySQL并发事务中幻读、虚读问题的解决方案
  • GPU算力平台|在GPU算力平台部署Qwen-2通义千问大模型的教程
  • sniffer 日志分析吞吐问题
  • 结合night compute分析 利用tensor core 优化K值较大的矩阵乘(超过cublas50%)
  • vue的KeepAlive应用(针对全部页面及单一页面进行缓存)
  • 精通Python (10)
  • 【Linux】8.Linux基础开发工具使用(2)
  • React中的key有什么作用?
  • RabbitMQ-消息入队
  • HarmonyOS NEXT应用开发边学边玩系列:从零实现一影视APP (二、首页轮播图懒加载的实现)
  • SQL刷题快速入门(二)
  • ClickHouse-CPU、内存参数设置
  • 在Linux系统中无网络安装Nginx并配置负载均衡
  • 41_Lua函数
  • uniapp小程序开发,配置开启小程序右上角三点的分享功能
  • 【搭建JavaEE】(1)maven仓库安装配置
  • Vue.js前端框架教程16:Element UI的el-dialog组件
  • WordEmbeddingPositionEmbedding