性能背景
简略的说下这个性能的背景需要吧,有相似需要的能够复用
- 实现 excel 导入(废话 …)
- 多个 sheet 页一起导入
- 第一个 sheet 页数据表头信息有两行,但只需依据第二行导入
- 如果报错,依据不同的 sheet 页返回多个 List 记录报错起因
- 数据量略微有些大(多个 sheet 页总量 50w 左右)
我的项目引入依赖
gradle:
compile "com.alibaba:easyexcel:3.1.0"
maven:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>
留神:3+ 版本的的 easyexcel,应用 poi 5+ 版本时,须要手动排除:poi-ooxml-schemas,例如:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
<exclusions>
<exclusion>
<artifactId>poi-ooxml-schemas</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
</exclusions>
</dependency>
Excel 模板
这里演示一下两个 sheet 页,第一个 sheet 页取第二行题目,第二个 sheet 页取第一行题目的 excel 操作,只为演示,非凡的能够依据这个理论状况进行拓展。🐾
点击下载模板(http://file.xiaoxiaofeng.site/blog/image/ 笑小枫测试导入.xls)
我的项目编码
在 config.bean 包下新建 excel
包,用于寄存 excel 解决相干的代码
- 在 excel 包下定义通用的
CommonExcel.java
对象,只有用于记录行号
package com.maple.demo.config.bean.excel;
import com.alibaba.excel.annotation.ExcelIgnore;
import lombok.Data;
/**
* @author 笑小枫
* @date 2022/7/22
*/
@Data
public class CommonExcel {
/**
* 行号
*/
@ExcelIgnore
private Integer rowIndex;
}
- 在 excel 包下定义经销商信息对象
ImportCompany.java
,代码如下:
@ExcelProperty 对用的是 excel 的题目名称,如果不加 @ExcelProperty,默认对应列号
package com.maple.demo.config.bean.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.util.Date;
/**
* @author 笑小枫
* @date 2022/7/22
*/
@Data
public class ImportCompany {
// -------------------- 根本信息 start -------------
@ExcelProperty("公司名称")
private String companyName;
@ExcelProperty("省份")
private String province;
@ExcelProperty("成立工夫")
private Date startDate;
@ExcelProperty("企业状态")
private String entStatus;
@ExcelProperty("注册地址")
private String registerAddress;
// ---------------- 根本信息 end ---------------------
// ---------------- 经营信息 start ---------------------
@ExcelProperty("员工数")
private String employeeMaxCount;
@ExcelProperty("经营规模")
private String newManageScaleName;
@ExcelProperty("所属区域省")
private String businessProvinceName;
@ExcelProperty("所属区域市")
private String businessCityName;
@ExcelProperty("所属区域区县")
private String businessAreaName;
// ---------------- 经营信息 end ---------------------
}
- 在 excel 包下定义联系人信息对象
ImportContact.java
,代码如下:
package com.maple.demo.config.bean.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* @author 笑小枫
* @date 2022/7/22
*/
@Data
public class ImportContact {@ExcelProperty("公司名称")
private String companyName;
@ExcelProperty("姓名")
private String name;
@ExcelProperty("身份证号码")
private String idCard;
@ExcelProperty("电话号码")
private String mobile;
@ExcelProperty("职位")
private String contactPostName;
}
- 在 listener 包下定义 excel 解决的监听器
ImportExcelListener.java
,代码如下:
package com.maple.demo.listener;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.read.metadata.holder.ReadRowHolder;
import com.alibaba.excel.util.ListUtils;
import com.maple.demo.config.bean.excel.CommonExcel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.util.List;
import java.util.function.Consumer;
/**
* @author 笑小枫
* @date 2022/7/22
*/
@Slf4j
public class ImportExcelListener<T> implements ReadListener<T> {
/**
* 默认一次读取 1000 条,可依据理论业务和服务器调整
*/
private static final int BATCH_COUNT = 1000;
/**
* Temporary storage of data
*/
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
private final List<String> errorMsgList;
/**
* consumer
*/
private final Consumer<List<T>> consumer;
public ImportExcelListener(Consumer<List<T>> consumer, List<String> errorMsgList) {
this.consumer = consumer;
this.errorMsgList = errorMsgList;
}
@Override
public void invoke(T data, AnalysisContext context) {
// 记录行号
if (data instanceof CommonExcel) {ReadRowHolder readRowHolder = context.readRowHolder();
((CommonExcel) data).setRowIndex(readRowHolder.getRowIndex() + 1);
}
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {consumer.accept(cachedDataList);
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {if (CollectionUtils.isNotEmpty(cachedDataList)) {consumer.accept(cachedDataList);
}
}
/**
* 在转换异样 获取其余异样下会调用本接口。抛出异样则进行读取。如果这里不抛出异样则 持续读取下一行。*/
@Override
public void onException(Exception exception, AnalysisContext context) {
// 如果是某一个单元格的转换异样 能获取到具体行号
String errorMsg = String.format("%s,第 %d 行解析异样", context.readSheetHolder().getReadSheet().getSheetName(),
context.readRowHolder().getRowIndex() + 1);
if (exception instanceof ExcelDataConvertException) {ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
errorMsg = String.format("第 %d 行,第 %d 列数据解析异样",
excelDataConvertException.getRowIndex() + 1,
excelDataConvertException.getColumnIndex() + 1);
log.error("{},第 {} 行,第 {} 列解析异样,数据为:{}",
context.readSheetHolder().getReadSheet().getSheetName(),
excelDataConvertException.getRowIndex() + 1,
excelDataConvertException.getColumnIndex() + 1,
excelDataConvertException.getCause().getMessage());
} else {log.error(errorMsg + exception.getMessage());
}
errorMsgList.add(errorMsg);
}
}
- 编写 controller 进行测试,代码如下:
.readSheet(0) 读取哪个 sheet 页,默认从 0 开始
.head(ExcelCompany.class) 对应定义的 sheet 页对象,不同的 sheet 页应用对应的对象
.registerReadListener 应用的监听器,这里定义的时通用的,依据不同的业务逻辑,能够定义不同的监听器解决,如需非凡的返回解决,能够定义多个参数的结构器,在监听器外面解决返回
.headRowNumber(2) 题目行在第几行
package com.maple.demo.controller;
import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.fastjson.JSON;
import com.maple.demo.config.bean.excel.ImportCompany;
import com.maple.demo.config.bean.excel.ImportContact;
import com.maple.demo.listener.ImportExcelListener;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 笑小枫
* @date 2022/7/22
*/
@Slf4j
@RestController
@RequestMapping("/example")
@Api(tags = "实例演示 - 导入 Excel")
public class TestImportExcelController {@PostMapping("/importExcel")
public Map<String, List<String>> importExcel(@RequestParam(value = "file") MultipartFile file) {List<String> companyErrorList = new ArrayList<>();
List<String> contactErrorList = new ArrayList<>();
try (ExcelReader excelReader = EasyExcelFactory.read(file.getInputStream()).build()) {
// 公司信息结构器
ReadSheet dealerSheet = EasyExcelFactory
.readSheet(0)
.head(ImportCompany.class)
.registerReadListener(new ImportExcelListener<ImportCompany>(data -> {
// 解决你的业务逻辑,最好抽出一个办法独自解决逻辑
log.info("公司信息数据 ----------------------------------------------");
log.info("公司信息数据:" + JSON.toJSONString(data));
log.info("公司信息数据 ----------------------------------------------");
}, companyErrorList))
.headRowNumber(2)
.build();
// 联系人信息结构器
ReadSheet contactSheet = EasyExcelFactory
.readSheet(1)
.head(ImportContact.class)
.registerReadListener(new ImportExcelListener<ImportContact>(data -> {
// 解决你的业务逻辑,最好抽出一个办法独自解决逻辑
log.info("联系人信息数据 ------------------------------------------");
log.info("联系人信息数据:" + JSON.toJSONString(data));
log.info("联系人信息数据 ------------------------------------------");
}, contactErrorList))
.build();
// 这里留神 肯定要把 sheet1 sheet2 一起传进去,不然有个问题就是 03 版的 excel 会读取屡次,节约性能
excelReader.read(dealerSheet, contactSheet);
} catch (IOException e) {log.error("解决 excel 失败," + e.getMessage());
}
Map<String, List<String>> result = new HashMap<>(16);
result.put("company", companyErrorList);
result.put("contact", contactErrorList);
log.info("导入 excel 实现,返回后果如下:" + JSON.toJSONString(result));
return result;
}
}
测试后果
因为须要上传 excel 文件,这里通过 postman 进行调用,idea 控制台打印后果如下:
postman 返回的后果数据如下:
{"msg": "","obj": {"contact": [],"company": []},
"result": "0000",
"serverTime": 1654569757952
}
模仿一下,数据转换谬误的场景,成心把工夫写错,如下图:
通过 postman 进行调用,idea 控制台打印后果如下:
在 controller 增加
@RequestMapping("/example")
能够防止 token 校验,在拦截器外面曾经放开了/example/**
的申请。
postman 返回的后果数据如下:
相干属性解读
注解
ExcelProperty
指定以后字段对应 excel 中的那一列。能够依据名字或者 Index 去匹配。当然也能够不写,默认第一个字段就是 index=0,以此类推。千万留神,要么全副不写,要么全副用 index,要么全副用名字去匹配。千万别三个混着用,除非你十分理解源代码中三个混着用怎么去排序的。ExcelIgnore
默认所有字段都会和 excel 去匹配,加了这个注解会疏忽该字段DateTimeFormat
日期转换,用String
去接管 excel 日期格局的数据会调用这个注解。外面的value
参照java.text.SimpleDateFormat
NumberFormat
数字转换,用String
去接管 excel 数字格局的数据会调用这个注解。外面的value
参照java.text.DecimalFormat
ExcelIgnoreUnannotated
默认不加ExcelProperty
的注解的都会参加读写,加了不会参加
参数
通用参数
ReadWorkbook
,ReadSheet
都会有的参数,如果为空,默认应用下级。
converter
转换器,默认加载了很多转换器。也能够自定义。readListener
监听器,在读取数据的过程中会一直的调用监听器。headRowNumber
须要读的表格有几行头数据。默认有一行头,也就是认为第二行开始起为数据。head
与clazz
二选一。读取文件头对应的列表,会依据列表匹配数据,倡议应用 class。clazz
与head
二选一。读取文件的头对应的 class,也能够应用注解。如果两个都不指定,则会读取全副数据。autoTrim
字符串、表头等数据主动 trimpassword
读的时候是否须要应用明码
ReadWorkbook(了解成 excel 对象)参数
excelType
以后 excel 的类型 默认会主动判断inputStream
与file
二选一。读取文件的流,如果接管到的是流就只用,不必流倡议应用file
参数。因为应用了inputStream
easyexcel 会帮忙创立临时文件,最终还是file
file
与inputStream
二选一。读取文件的文件。autoCloseStream
主动敞开流。readCache
默认小于 5M 用 内存,超过 5M 会应用EhCache
, 这里不倡议应用这个参数。useDefaultListener
@since 2.1.4
默认会退出ModelBuildEventListener
来帮忙转换成传入class
的对象,设置成false
后将不会帮助转换对象,自定义的监听器会接管到Map<Integer,CellData>
对象,如果还想持续接听到class
对象,请调用readListener
办法,退出自定义的beforeListener
、ModelBuildEventListener
、自定义的afterListener
即可。
ReadSheet(就是 excel 的一个 Sheet)参数
sheetNo
须要读取 Sheet 的编码,倡议应用这个来指定读取哪个 SheetsheetName
依据名字去匹配 Sheet,excel 2003 不反对依据名字去匹配
写在最初
本文只是用到局部性能,简略的做了一下总结,更多的性能,能够去官网查阅。
官网文档:https://www.yuque.com/easyexcel/doc/read
应用 EasyExcel 导出 excel:https://www.xiaoxiaofeng.com/archives/springboot13
对于笑小枫💕
本章到这里完结了,喜爱的敌人关注一下我呦😘😘,大伙的反对,就是我保持写下去的能源。
老规矩,懂了就点赞珍藏;不懂就问,日常在线,我会就会回复哈~🤪
后续文章会陆续更新,文档会同步在微信公众号、集体博客、CSDN 和 GitHub 放弃同步更新。😬
微信公众号:笑小枫
笑小枫集体博客:https://www.xiaoxiaofeng.com
CSDN:https://zhangfz.blog.csdn.net
本文源码:https://github.com/hack-feng/maple-demo