性能背景
简略的说下这个性能的背景需要吧,有相似需要的能够复用
- 实现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 */@Datapublic 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 */@Datapublic 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 */@Datapublic 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 */@Slf4jpublic 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