前言

使用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. 每周更新 | Verilog测试用例及波形展示图功能上线

    Hi,亲爱的技术伙伴,经过产研团队的努力,本周ShowMeBug有以下4个功能上线啦- 芯片语言 Verilog 支持测试用例 芯片语言 Verilog 支持测试用例,自动评分同步上线- 同时,Ver ...

  2. Java设计模式中的几种常用设计模式总结

    一.设计模式概念 1.定义 ​ Java包含23种设计模式,是一套对代码设计经验的总结,被人们反复利用,多人熟知的代码设计方式. 2.目的 ​ 为了提高代码的可读性,可扩展性以及代码的复用性,为了解决 ...

  3. Galaxy Release 20.05 发布,新增多项可视化体验

    Galaxy Project(https://galaxyproject.org/)是在云计算背景下诞生的一个生物信息学可视化分析开源项目. 该项目由美国国家科学基金会(NSF).美国国家人类基因组研 ...

  4. Windows AD域查询属性-密码过期时间

    Windows AD域查询属性-密码过期时间 Windows PowerShell命令方式查询: net user zhou /domain找出 SamAccountName 的值为zhou的用户部分 ...

  5. k8s实战案例之部署redis单机和redis cluster

    1.在k8s上部署redis单机 1.1.redis简介 redis是一款基于BSD协议,开源的非关系型数据库(nosql数据库),作者是意大利开发者Salvatore Sanfilippo在2009 ...

  6. Hello Welcome to my blog!

    Hello Welcome to my blog!

  7. 从源码级剖析Java类加载原理

    相信大多数熟悉Java的研发工程师,都知道Java类加载原理:Java中的类是由类加载器采用双亲委派机制进行加载.其中,Java核心库中实现了三种类型的类加载器,它们分别是:引导类加载器Bootstr ...

  8. SpringBoot 2 种方式快速实现分库分表,轻松拿捏!

    大家好,我是小富- (一)好好的系统,为什么要分库分表? (二)分库分表的 21 条法则,hold 住! 本文是<分库分表ShardingSphere5.x原理与实战>系列的第三篇文章,本 ...

  9. 企业级logstash简单使用(ELK)

    企业级logstash简单使用(ELK) 要使用logstash收集到Elasticsearch的方式,需确保logstash版本与es版本一致. 由于我也是刚刚研究使用,所以本文暂不会出现原理性的东 ...

  10. Python 爬虫实战:驾驭数据洪流,揭秘网页深处

    爬虫,这个经常被人提到的词,是对数据收集过程的一种形象化描述.特别是在Python语言中,由于其丰富的库资源和良好的易用性,使得其成为编写爬虫的绝佳选择.本文将从基础知识开始,深入浅出地讲解Pytho ...