前言

使用spring boot 对excel 进行操作在平时项目中要经常使用。常见通过jxl和poi 的方式进行操作。但他们都存在一个严重的问题就是非常的耗内存。这里介绍一种 Easy Excel 工具来对excel进行操作。

一、Easy Excel是什么?

EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。easyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。

二、使用EasyExcel 实现读操作

从excel 中读取数据,常用的场景就是读取excel的数据,将相应的数据保存到数据库中。需要实现一定的逻辑处理。

1、导入依赖

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>

2、创建读取数据封装类

@Data
public class User { @ExcelProperty(index = 0)
private Integer id; @ExcelProperty(index = 1)
private String name; @ExcelProperty(index = 2)
private Integer age;
}

比如我们要读取两列的数据,就写两个属性。@ExcelProperty(index = 0)来设置要读取的列,index=0表示读取第一列。

3、创建读取excel的监听类

监听器继承 AnalysisEventListener 类

@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> { /**
* 解析excel文档的每一行
* @param user 参数user即是每行读取数据转换的User对象
* @param analysisContext
*/
@Override
public void invoke(User user, AnalysisContext analysisContext){
log.info("excel数据行:{}",user.toString());
} /**
* 整个文档解析完执行
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("文档解析完毕");
}
}

当解析每一条数据时都会调用invoke方法,当所有数据都解析完毕时最后会调用doAfterAllAnalysed方法。可以在监听类内的方法中将每次读取到的数据进行保存或者其他操作处理。

4、接口使用easyExcel读取excel文件调用监听器

/**
* 上传excel文件并读取其中内容
*
* @param file
* @return
*/
@PostMapping("/upload")
public String uploadExcel(MultipartFile file) {
log.info("easyExcel上传文件:{}", file);
try {
InputStream inputStream = file.getInputStream();
EasyExcel.read(inputStream, User.class, new UserExcelListener())
.sheet()
.doRead();
} catch (Exception e) { }
return "表格文件上传成功";
}

三、使用EasyExcel 实现写操作

写操作有两种写法,一种是不创建对象的写入,另一种是根据对象写入。这里主要介绍创建对象写入

创建对象写入

1、创建excel对象类

@Data
public class User { @ExcelProperty(index = 0)
private Integer id; @ExcelProperty(index = 1)
private String name; @ExcelProperty(index = 2)
private Integer age;
}

注意@ExcelProperty(“用户编号”) 会生成相应的列名为 用户编号,如果不设置,则会直接将字段名设置为excel的列名。

2、接口使用测试数据导出(常规导出不合并单元格)

/**
* 输出导出excel
*/
@PostMapping("/export")
public void export() {
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setId(i);
user.setName("测试用户-" + i);
user.setAge(20 + i);
users.add(user);
}
log.info("导出数据结果集:{}", users);
String fileName = "C:\\Users\\pytho\\Desktop\\fsdownload\\用户信息表.xlsx";
EasyExcel.write(fileName, User.class)
.autoCloseStream(true)
.sheet("sheet名称")
.doWrite(users);
}

3、接口测试导出(单列合并单元格)

/**
* 输出导出excel
*/
@PostMapping("/export1")
public void export1() {
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setId(i);
if (i == 3 || i == 4 || i == 5) {
user.setName("测试用户-3");
} else {
user.setName("测试用户-" + i);
}
user.setAge(20 + i);
users.add(user);
}
log.info("导出数据结果集:{}", users);
String fileName = "C:\\Users\\pytho\\Desktop\\fsdownload\\(单列相同内容合并单元格)用户信息表.xlsx";
EasyExcel.write(fileName, User.class)
.registerWriteHandler(new SimpleExcelMergeUtil())
.autoCloseStream(true)
.sheet("sheet名称")
.doWrite(users);
}

如果要对导出的excel进行处理,就需要自定义处理器类进行处理

自定义easyExcel处理器(单列合并:根据用户id相同的列进行合并单元格):

/**
* @version 1.0
* @Package: com.stech.bms.buss.utils
* @ClassName: ExcelMergeUtil
* @Author: sgq
* @Date: 2023/7/28 13:29
* @Description: 仅处理单列数据相同合并单元格
*/
public class SimpleExcelMergeUtil implements CellWriteHandler { public SimpleExcelMergeUtil() {
} /**
* 创建每个单元格之前执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param row
* @param head
* @param columnIndex
* @param relativeRowIndex
* @param isHead
*/
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { } /**
* 创建每个单元格之后执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } /**
* 每个单元格数据内容渲染之后执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param cellData
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } /**
* 每个单元格完全创建完之后执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param cellDataList
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 当前行
int curRowIndex = cell.getRowIndex();
// 当前列
int curColIndex = cell.getColumnIndex(); if (!isHead) {
if (curRowIndex > 1 && curColIndex == 1) {
// 从第二行数据行开始,获取当前行第二列数据
Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
// 获取上一行第二列数据
Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
if (curData.equals(preData)) {
Sheet sheet = writeSheetHolder.getSheet();
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
boolean isMerged = false;
for (int i = 0; i < mergedRegions.size() && !isMerged; i++) {
CellRangeAddress cellRangeAddr = mergedRegions.get(i);
// 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
sheet.removeMergedRegion(i);
cellRangeAddr.setLastRow(curRowIndex);
sheet.addMergedRegion(cellRangeAddr);
isMerged = true;
}
}
// 若上一个单元格未被合并,则新增合并单元
if (!isMerged) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
sheet.addMergedRegion(cellRangeAddress);
}
}
}
}
}
}

4、接口测试导出(通用合并单元格)

/**
* 输出导出excel
*/
@PostMapping("/export2")
public void export2() {
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setId(i);
if (i == 3 || i == 4 || i == 5) {
user.setName("测试用户-3");
} else {
user.setName("测试用户-" + i);
}
user.setAge(20 + i);
users.add(user);
}
log.info("导出数据结果集:{}", users);
// 从第几行开始合并
int mergeStartRowIndex = 5;
// 需要合并哪些列
int[] mergeColumns = {1};
String fileName = "C:\\Users\\pytho\\Desktop\\fsdownload\\(单列相同内容合并单元格-通用版)用户信息表.xlsx";
EasyExcel.write(fileName, User.class)
.registerWriteHandler(new SimpleCommonExcelMergeUtil(mergeStartRowIndex,mergeColumns))
.autoCloseStream(true)
.sheet("sheet名称")
.doWrite(users);
}

excel处理器类:

/**
* @version 1.0
* @Package: com.stech.bms.buss.utils
* @ClassName: ExcelMergeUtil
* @Author: sgq
* @Date: 2023/7/28 13:29
* @Description: 仅处理单列数据相同合并单元格
*/
public class SimpleCommonExcelMergeUtil implements CellWriteHandler { private int mergeStartRowIndex;
private int[] mergeColumns;
private List<Integer> mergeColumnList; public SimpleCommonExcelMergeUtil() {
} public SimpleCommonExcelMergeUtil(int mergeStartRowIndex, int[] mergeColumns) {
this.mergeStartRowIndex = mergeStartRowIndex;
this.mergeColumns = mergeColumns;
mergeColumnList = new ArrayList<>();
for (int i : mergeColumns) {
mergeColumnList.add(i);
}
} /**
* 创建每个单元格之前执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param row
* @param head
* @param columnIndex
* @param relativeRowIndex
* @param isHead
*/
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { } /**
* 创建每个单元格之后执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } /**
* 每个单元格数据内容渲染之后执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param cellData
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } /**
* 每个单元格完全创建完之后执行
*
* @param writeSheetHolder
* @param writeTableHolder
* @param cellDataList
* @param cell
* @param head
* @param relativeRowIndex
* @param isHead
*/
@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
// 当前行
int curRowIndex = cell.getRowIndex();
// 当前列
int curColIndex = cell.getColumnIndex(); if (!isHead) {
if (curRowIndex > mergeStartRowIndex && mergeColumnList.contains(curColIndex)) {
// 从第二行数据行开始,获取当前行第二列数据
Object curData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
// 获取上一行第二列数据
Cell preCell = cell.getSheet().getRow(curRowIndex - 1).getCell(curColIndex);
Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
if (curData.equals(preData)) {
Sheet sheet = writeSheetHolder.getSheet();
List<CellRangeAddress> mergedRegions = sheet.getMergedRegions();
boolean isMerged = false;
for (int i = 0; i < mergedRegions.size() && !isMerged; i++) {
CellRangeAddress cellRangeAddr = mergedRegions.get(i);
// 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
if (cellRangeAddr.isInRange(curRowIndex - 1, curColIndex)) {
sheet.removeMergedRegion(i);
cellRangeAddr.setLastRow(curRowIndex);
sheet.addMergedRegion(cellRangeAddr);
isMerged = true;
}
}
// 若上一个单元格未被合并,则新增合并单元
if (!isMerged) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(curRowIndex - 1, curRowIndex, curColIndex, curColIndex);
sheet.addMergedRegion(cellRangeAddress);
}
}
}
}
}
}

这只是简单的合并单元格例子,抛砖引玉的作用。工作中可能会遇到很多情况:合并单元格后第一列序列号也需要根据其他列进行合并单元格且序列号还必须保持连续,根据部分列合并单元格,隔行合并单元格等等情况,这就需要开发者对easyExcel的处理器类里面的api比较了解才能完成。遇到的问题也可以留言,看到也会尝试一起处理解决。

[EasyExcel] 导出合并单元格的更多相关文章

  1. C#导出Excel,并且设置Excel单元格格式,合并单元格.

    注:要添加COM组件 Microsoft Excel 11.0 Object Library  引用. 具体代码如下: using System; using System.Collections.G ...

  2. 带复杂表头合并单元格的HtmlTable转换成DataTable并导出Excel

    步骤: 一.前台JS取HtmlTable数据,根据设定的分隔符把数据拼接起来 <!--导出Excel--> <script type="text/javascript&qu ...

  3. poi导出Excel报表多表头双层表头、合并单元格

    效果图: controller层方法: /**     *      * 导出Excel报表     * @param request     * @return     *      */    @ ...

  4. poi合并单元格同时导出excel

    poi合并单元格同时导出excel POI进行跨行需要用到对象HSSFSheet对象,现在就当我们程序已经定义了一个HSSFSheet对象sheet. 跨第1行第1个到第2个单元格的操作为 sheet ...

  5. java使用freemarker模板导出word(带有合并单元格)文档

    来自:https://blog.csdn.net/qq_33195578/article/details/73790283 前言:最近要做一个导出word功能,其实网上有很多的例子,但是我需要的是合并 ...

  6. poi导出excel合并单元格(包括列合并、行合并)

    1 工程所需jar包如下:commons-codec-1.5.jarcommons-logging-1.1.jarlog4j-1.2.13.jarjunit-3.8.1.jarpoi-3.9-2012 ...

  7. 在Asp.Net MVC中使用NPOI插件实现对Excel的操作(导入,导出,合并单元格,设置样式,输入公式)

    前言 NPOI 是 POI 项目的.NET版本,它不使用 Office COM 组件,不需要安装 Microsoft Office,目前支持 Office 2003 和 2007 版本. 1.整个Ex ...

  8. WPF 导出Excel(合并单元格)

    WPF 导出Excel(合并单元格) DataTable 导出Excel(导出想要的列,不想要的去掉) ,B1,B2,B3,B4,B5} MisroSoft.Office.Interop.Excel. ...

  9. 复杂的POI导出Excel表格(多行表头、合并单元格)

    poi导出excel有两种方式: 第一种:从无到有的创建整个excel,通过HSSFWorkbook,HSSFSheet HSSFCell, 等对象一步一步的创建出工作簿,sheet,和单元格,并添加 ...

  10. java导出标题多行且合并单元格的EXCEL

    场景:项目中遇到有需要导出Excel的需求,并且是多行标题且有合并单元格的,参考网上的文章,加上自己的理解,封装成了可自由扩展的导出工具 先上效果,再贴代码: 调用工具类进行导出: public st ...

随机推荐

  1. Python潮流周刊#3:PyPI 的安全问题

    你好,我是豌豆花下猫.这里记录每周值得分享的 Python 及通用技术内容,部分为英文,已在小标题注明.(标题取自其中一则分享,不代表全部内容都是该主题,特此声明.) 文章&教程 1.掌握Py ...

  2. 【VS Code+Qt6】拖放操作

    由于老周的示例代码都是用 VS Code + CMake + Qt 写的,为了不误导人,在标题中还是加上"VS Code"好一些. 上次咱们研究了剪贴板的基本用法,也了解了叫 QM ...

  3. Vue——属性指令、style和class、条件渲染、列表渲染、事件处理、数据双向绑定、过滤案例

    vm对象 <body> <div id="app"> <h1>{{name}}</h1> <button @click=&qu ...

  4. 基于ChatGPT函数调用来实现C#本地函数逻辑链式调用助力大模型落地

    6 月 13 日 OpenAI 官网突然发布了重磅的 ChatGPT 更新,我相信大家都看到了 ,除了调用降本和增加更长的上下文版本外,开发者们最关心的应该还是新的函数调用能力.通过这项能力模型在需要 ...

  5. C++面试八股文:std::array如何实现编译器排序?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第25面: 面试官:array熟悉吗? 二师兄:你说的是原生数组还是std::array? 面试官:你觉得两者有什么区别? 二师兄:区别不是很大,原生 ...

  6. 基于JavaFX的扫雷游戏实现(二)——游戏界面

      废话环节:看过上期文章的小伙伴现在可能还是一头雾水,怎么就完成了核心内容,界面呢?哎我说别急让我先急,博主这不夜以继日地肝出了界面部分嘛.还是老规矩,不会把所有地方都照顾到,只挑一些有代表性的内容 ...

  7. LLaMA模型指令微调 字节跳动多模态视频大模型 Valley 论文详解

    Valley: Video Assistant with Large Language model Enhanced abilitY 大家好,我是卷了又没卷,薛定谔的卷的AI算法工程师「陈城南」~ 担 ...

  8. 详解nvim内建LSP体系与基于nvim-cmp的代码补全体系

    2023年,nvim以及其生态已经发展的愈来愈完善了.nvim内置的LSP(以及具体的语言服务)加上众多插件,可以搭建出支持各种类型语法检查.代码补全.代码格式化等功能的IDE.网络上关于如何配置的文 ...

  9. centos系统给centos-root硬盘扩容

    此服务器为虚拟机,通过lsblk命令查看当前虚拟机硬盘: 其中一块硬盘大小为100G,已作为系统盘使用,但是只分配了15G的空间使用,需要对剩余空间进行分区,并扩容到对应centos卷组的root目录 ...

  10. AcWing 4486. 数字操作题解

    题目描述 给定一个整数 \(n\),你可以对该数进行任意次(可以是 \(0\) 次)变换操作. 每次操作为以下两种之一: 将整数 \(n\) 乘以任意一个正整数 \(x\). 将整数 \(n\) 替换 ...